# frozen_string_literal: true require 'sinatra/json' require 'sinatra/base' module Sinatra # # = Sinatra::RespondWith # # These extensions let Sinatra automatically choose what template to render or # action to perform depending on the request's Accept header. # # Example: # # # Without Sinatra::RespondWith # get '/' do # data = { :name => 'example' } # request.accept.each do |type| # case type.to_s # when 'text/html' # halt haml(:index, :locals => data) # when 'text/json' # halt data.to_json # when 'application/atom+xml' # halt nokogiri(:'index.atom', :locals => data) # when 'application/xml', 'text/xml' # halt nokogiri(:'index.xml', :locals => data) # when 'text/plain' # halt 'just an example' # end # end # error 406 # end # # # With Sinatra::RespondWith # get '/' do # respond_with :index, :name => 'example' do |f| # f.txt { 'just an example' } # end # end # # Both helper methods +respond_to+ and +respond_with+ let you define custom # handlers like the one above for +text/plain+. +respond_with+ additionally # takes a template name and/or an object to offer the following default # behavior: # # * If a template name is given, search for a template called # +name.format.engine+ (+index.xml.nokogiri+ in the above example). # * If a template name is given, search for a templated called +name.engine+ # for engines known to result in the requested format (+index.haml+). # * If a file extension associated with the mime type is known to Sinatra, and # the object responds to +to_extension+, call that method and use the result # (+data.to_json+). # # == Security # # Since methods are triggered based on client input, this can lead to security # issues (but not as severe as those might appear in the first place: keep in # mind that only known file extensions are used). You should limit # the possible formats you serve. # # This is possible with the +provides+ condition: # # get '/', :provides => [:html, :json, :xml, :atom] do # respond_with :index, :name => 'example' # end # # However, since you have to set +provides+ for every route, this extension # adds an app global (class method) `respond_to`, that lets you define content # types for all routes: # # respond_to :html, :json, :xml, :atom # get('/a') { respond_with :index, :name => 'a' } # get('/b') { respond_with :index, :name => 'b' } # # == Custom Types # # Use the +on+ method for defining actions for custom types: # # get '/' do # respond_to do |f| # f.xml { nokogiri :index } # f.on('application/custom') { custom_action } # f.on('text/*') { data.to_s } # f.on('*/*') { "matches everything" } # end # end # # Definition order does not matter. module RespondWith class Format def initialize(app) @app = app @map = {} @generic = {} @default = nil end def on(type, &block) @app.settings.mime_types(type).each do |mime| case mime when '*/*' then @default = block when %r{^([^/]+)/\*$} then @generic[$1] = block else @map[mime] = block end end end def finish yield self if block_given? mime_type = @app.content_type || @app.request.preferred_type(@map.keys) || @app.request.preferred_type || 'text/html' type = mime_type.split(/\s*;\s*/, 2).first handlers = [@map[type], @generic[type[%r{^[^/]+}]], @default].compact handlers.each do |block| if (result = block.call(type)) @app.content_type mime_type @app.halt result end end @app.halt 500, 'Unknown template engine' end def method_missing(method, *args, &block) return super if args.any? || block.nil? || !@app.mime_type(method) on(method, &block) end end module Helpers include Sinatra::JSON def respond_with(template, object = nil, &block) unless Symbol === template object = template template = nil end format = Format.new(self) format.on '*/*' do |type| exts = settings.ext_map[type] exts << :xml if type.end_with? '+xml' if template args = template_cache.fetch(type, template) { template_for(template, exts) } if args.any? locals = { object: object } locals.merge! object.to_hash if object.respond_to? :to_hash renderer = args.first options = args[1..] + [{ locals: locals }] halt send(renderer, *options) end end if object exts.each do |ext| halt json(object) if ext == :json next unless object.respond_to? method = "to_#{ext}" halt(*object.send(method)) end end false end format.finish(&block) end def respond_to(&block) Format.new(self).finish(&block) end private def template_for(name, exts) # in production this is cached, so don't worry too much about runtime possible = [] settings.template_engines[:all].each do |engine| exts.each { |ext| possible << [engine, "#{name}.#{ext}"] } end exts.each do |ext| settings.template_engines[ext].each { |e| possible << [e, name] } end possible.each do |engine, template| klass = Tilt.default_mapping.template_map[engine.to_s] || Tilt.lazy_map[engine.to_s].fetch(0, [])[0] find_template(settings.views, template, klass) do |file| next unless File.exist? file return settings.rendering_method(engine) << template.to_sym end end [] # nil or false would not be cached end end def remap_extensions ext_map.clear Rack::Mime::MIME_TYPES.each { |e, t| ext_map[t] << e[1..].to_sym } ext_map['text/javascript'] << 'js' ext_map['text/xml'] << 'xml' end def mime_type(*) result = super remap_extensions result end def respond_to(*formats) @respond_to ||= nil if formats.any? @respond_to ||= [] @respond_to.concat formats elsif @respond_to.nil? && superclass.respond_to?(:respond_to) superclass.respond_to else @respond_to end end def rendering_method(engine) return [engine] if Sinatra::Templates.method_defined? engine return [:mab] if engine.to_sym == :markaby %i[render engine] end private def compile!(verb, path, block, **options) options[:provides] ||= respond_to if respond_to super end def self.jrubyify(engs) not_supported = [:markdown] engs.each_key do |key| engs[key].collect! { |eng| eng == :yajl ? :json_pure : eng } engs[key].delete_if { |eng| not_supported.include?(eng) } end engs end def self.engines engines = { xml: %i[builder nokogiri], html: %i[erb erubi haml hamlit slim liquid mab markdown rdoc], all: (Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] - %i[find_template markaby]), json: [:yajl] } engines.default = [] defined?(JRUBY_VERSION) ? jrubyify(engines) : engines end def self.registered(base) base.set :ext_map, Hash.new { |h, k| h[k] = [] } base.set :template_engines, engines base.remap_extensions base.helpers Helpers end end register RespondWith Delegator.delegate :respond_to end