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, @map, @generic, @default = app, {}, {}, nil end def on(type, &block) @app.settings.mime_types(type).each do |mime| case mime when '*/*' then @default = block when /^([^\/]+)\/\*$/ 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[/^[^\/]+/]], @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? or block.nil? or not @app.mime_type(method) on(method, &block) end end module Helpers include Sinatra::JSON def respond_with(template, object = nil, &block) object, template = template, nil unless Symbol === template 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..-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| # not exactly like Tilt[engine], but does not trigger a require if Tilt.respond_to?(:mappings) klass = Tilt.mappings[Tilt.normalize(engine)].first else begin klass = Tilt[engine] rescue LoadError next end end 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..-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? and 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 [: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.keys.each 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 = { :css => [:less, :sass, :scss], :xml => [:builder, :nokogiri], :js => [:coffee], :html => [:erb, :erubis, :haml, :slim, :liquid, :radius, :mab, :markdown, :textile, :rdoc], :all => (Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] - [: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