274 lines
7.9 KiB
Ruby
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
|