add sinatra-respond-with

This commit is contained in:
Konstantin Haase 2011-03-26 00:02:03 +01:00
parent ec248a8b5a
commit e6f15a2888
8 changed files with 406 additions and 1 deletions

View File

@ -10,6 +10,7 @@ module Sinatra
register :ConfigFile
register :Decompile
register :Namespace
register :RespondWith
helpers :LinkHeader
end

View File

@ -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

View File

@ -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"

View File

@ -0,0 +1 @@
Girl! I wanna take you to a ... bar!

View File

@ -0,0 +1 @@
json!

View File

@ -0,0 +1 @@
Hello <%= name %>!

View File

@ -0,0 +1,2 @@
body
color: red

View File

@ -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