add sinatra-respond-with
This commit is contained in:
parent
ec248a8b5a
commit
e6f15a2888
|
@ -10,6 +10,7 @@ module Sinatra
|
|||
register :ConfigFile
|
||||
register :Decompile
|
||||
register :Namespace
|
||||
register :RespondWith
|
||||
helpers :LinkHeader
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
require 'sinatra/base'
|
||||
require 'json' unless String.method_defined? :to_json
|
||||
|
||||
module Sinatra
|
||||
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 406
|
||||
end
|
||||
|
||||
def method_missing(meth, *args, &block)
|
||||
return super if args.any? or block.nil? or not @app.mime_type(meth)
|
||||
on(meth, &block)
|
||||
end
|
||||
end
|
||||
|
||||
module Helpers
|
||||
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
|
||||
args << { :locals => locals }
|
||||
halt send(*args)
|
||||
end
|
||||
end
|
||||
if object
|
||||
exts.each do |ext|
|
||||
next unless meth = "to_#{ext}" and object.respond_to? meth
|
||||
halt(*object.send(meth))
|
||||
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 to 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|
|
||||
find_template(settings.views, template, Tilt[engine]) 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
|
||||
|
||||
attr_accessor :ext_map
|
||||
|
||||
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, &block)
|
||||
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
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
ENGINES.default = []
|
||||
|
||||
def self.registered(base)
|
||||
base.ext_map = Hash.new { |h,k| h[k] = [] }
|
||||
base.set :template_engines, ENGINES.dup
|
||||
base.remap_extensions
|
||||
base.helpers Helpers
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|||
s.require_paths = ["lib"]
|
||||
s.summary = s.description
|
||||
|
||||
s.add_dependency "sinatra", "~> 1.2.0"
|
||||
s.add_dependency "sinatra", "~> 1.2.2"
|
||||
s.add_dependency "backports", ">= 2.0"
|
||||
|
||||
s.add_development_dependency "rspec", "~> 2.3"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Girl! I wanna take you to a ... bar!
|
|
@ -0,0 +1 @@
|
|||
json!
|
|
@ -0,0 +1 @@
|
|||
Hello <%= name %>!
|
|
@ -0,0 +1,2 @@
|
|||
body
|
||||
color: red
|
|
@ -0,0 +1,245 @@
|
|||
require 'backports'
|
||||
require_relative 'spec_helper'
|
||||
|
||||
describe Sinatra::RespondWith do
|
||||
def provides(*args)
|
||||
@provides = args
|
||||
end
|
||||
|
||||
def respond_app(&block)
|
||||
types = @provides
|
||||
mock_app do
|
||||
set :app_file, __FILE__
|
||||
set :views, root + '/respond_with'
|
||||
register Sinatra::RespondWith
|
||||
respond_to(*types) if types
|
||||
class_eval(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to(*args, &block)
|
||||
respond_app { get('/') { respond_to(*args, &block) } }
|
||||
end
|
||||
|
||||
def respond_with(*args, &block)
|
||||
respond_app { get('/') { respond_with(*args, &block) } }
|
||||
end
|
||||
|
||||
def req(*types)
|
||||
p = types.shift if types.first.is_a? String and types.first.start_with? '/'
|
||||
accept = types.map { |t| Sinatra::Base.mime_type(t).to_s }.join ','
|
||||
get (p || '/'), {}, 'HTTP_ACCEPT' => accept
|
||||
end
|
||||
|
||||
describe "Helpers#respond_to" do
|
||||
it 'allows defining handlers by file extensions' do
|
||||
respond_to do |format|
|
||||
format.html { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "html!"
|
||||
req(:json).body.should == "json!"
|
||||
end
|
||||
|
||||
it 'respects quality' do
|
||||
respond_to do |format|
|
||||
format.html { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req("text/html;q=0.7, application/json;q=0.3").body.should == "html!"
|
||||
req("text/html;q=0.3, application/json;q=0.7").body.should == "json!"
|
||||
end
|
||||
|
||||
it 'allows using mime types' do
|
||||
respond_to do |format|
|
||||
format.on('text/html') { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "html!"
|
||||
end
|
||||
|
||||
it 'allows using wildcards in format matchers' do
|
||||
respond_to do |format|
|
||||
format.on('text/*') { "text!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "text!"
|
||||
end
|
||||
|
||||
it 'allows using catch all wildcards in format matchers' do
|
||||
respond_to do |format|
|
||||
format.on('*/*') { "anything!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "anything!"
|
||||
end
|
||||
|
||||
it 'prefers concret over generic' do
|
||||
respond_to do |format|
|
||||
format.on('text/*') { "text!" }
|
||||
format.on('*/*') { "anything!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:json).body.should == "json!"
|
||||
req(:html).body.should == "text!"
|
||||
end
|
||||
|
||||
it 'does not set up default handlers' do
|
||||
respond_to
|
||||
req.should_not be_ok
|
||||
status.should == 406
|
||||
end
|
||||
end
|
||||
|
||||
describe "Helpers#respond_with" do
|
||||
describe "matching" do
|
||||
it 'allows defining handlers by file extensions' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.html { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "html!"
|
||||
req(:json).body.should == "json!"
|
||||
end
|
||||
|
||||
it 'respects quality' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.html { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req("text/html;q=0.7, application/json;q=0.3").body.should == "html!"
|
||||
req("text/html;q=0.3, application/json;q=0.7").body.should == "json!"
|
||||
end
|
||||
|
||||
it 'allows using mime types' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.on('text/html') { "html!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "html!"
|
||||
end
|
||||
|
||||
it 'allows using wildcards in format matchers' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.on('text/*') { "text!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "text!"
|
||||
end
|
||||
|
||||
it 'allows using catch all wildcards in format matchers' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.on('*/*') { "anything!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:html).body.should == "anything!"
|
||||
end
|
||||
|
||||
it 'prefers concret over generic' do
|
||||
respond_with(:ignore) do |format|
|
||||
format.on('text/*') { "text!" }
|
||||
format.on('*/*') { "anything!" }
|
||||
format.json { "json!" }
|
||||
end
|
||||
|
||||
req(:json).body.should == "json!"
|
||||
req(:html).body.should == "text!"
|
||||
end
|
||||
end
|
||||
|
||||
describe "default behavior" do
|
||||
it 'converts objects to json out of the box' do
|
||||
respond_with 'a' => 'b'
|
||||
req(:json).body.should == {'a' => 'b'}.to_json
|
||||
end
|
||||
|
||||
it 'handles multiple routes correctly' do
|
||||
respond_app do
|
||||
get('/') { respond_with 'a' => 'b' }
|
||||
get('/:name') { respond_with 'a' => params[:name] }
|
||||
end
|
||||
req('/', :json).body.should == {'a' => 'b'}.to_json
|
||||
req('/b', :json).body.should == {'a' => 'b'}.to_json
|
||||
req('/c', :json).body.should == {'a' => 'c'}.to_json
|
||||
end
|
||||
|
||||
it "calls to_EXT if available" do
|
||||
respond_with Struct.new(:to_pdf).new("hello")
|
||||
req(:pdf).body.should == "hello"
|
||||
end
|
||||
|
||||
it 'results in a 406 if format cannot be produced' do
|
||||
respond_with({})
|
||||
req(:html).should_not be_ok
|
||||
status.should == 406
|
||||
end
|
||||
end
|
||||
|
||||
describe 'templates' do
|
||||
it 'looks for templates with name.target.engine' do
|
||||
respond_with :foo, :name => 'World'
|
||||
req(:html).should be_ok
|
||||
body.should == "Hello World!"
|
||||
end
|
||||
|
||||
it 'looks for templates with name.engine for specific engines' do
|
||||
respond_with :bar
|
||||
req(:html).should be_ok
|
||||
body.should == "Girl! I wanna take you to a ... bar!"
|
||||
end
|
||||
|
||||
it 'does not use name.engine for engines producing other formats' do
|
||||
respond_with :not_html
|
||||
req(:html).should_not be_ok
|
||||
status.should == 406
|
||||
body.should be_empty
|
||||
end
|
||||
|
||||
it 'falls back to to_EXT if no template is found' do
|
||||
respond_with :foo, :name => 'World'
|
||||
req(:json).should be_ok
|
||||
body.should == {:name => 'World'}.to_json
|
||||
end
|
||||
|
||||
it 'favors templates over to_EXT' do
|
||||
respond_with :bar, :name => 'World'
|
||||
req(:json).should be_ok
|
||||
body.should == 'json!'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'customizing' do
|
||||
it 'allows customizing' do
|
||||
respond_with(:foo, :name => 'World') { |f| f.html { 'html!' }}
|
||||
req(:html).should be_ok
|
||||
body.should == "html!"
|
||||
end
|
||||
|
||||
it 'falls back to default behavior if none matches' do
|
||||
respond_with(:foo, :name => 'World') { |f| f.json { 'json!' }}
|
||||
req(:html).should be_ok
|
||||
body.should == "Hello World!"
|
||||
end
|
||||
|
||||
it 'favors generic rule over default behavior' do
|
||||
respond_with(:foo, :name => 'World') { |f| f.on('*/*') { 'generic!' }}
|
||||
req(:html).should be_ok
|
||||
body.should == "generic!"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe :respond_to do
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue