sinatra/sinatra-contrib/lib/sinatra/respond_with.rb

274 lines
7.9 KiB
Ruby

# 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