Skip to content
Browse files

add sinatra-respond-with

  • Loading branch information...
1 parent 095acdc commit 0895ddf78d9e653827fdcc9b23a6dcf3602e424b @rkh committed Mar 26, 2011
View
1 lib/sinatra/contrib.rb
@@ -10,6 +10,7 @@ module Common
register :ConfigFile
register :Decompile
register :Namespace
+ register :RespondWith
helpers :LinkHeader
end
View
154 lib/sinatra/respond_with.rb
@@ -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
2 sinatra-contrib.gemspec
@@ -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
1 spec/respond_with/bar.erb
@@ -0,0 +1 @@
+Girl! I wanna take you to a ... bar!
View
1 spec/respond_with/bar.json.erb
@@ -0,0 +1 @@
+json!
View
1 spec/respond_with/foo.html.erb
@@ -0,0 +1 @@
+Hello <%= name %>!
View
2 spec/respond_with/not_html.sass
@@ -0,0 +1,2 @@
+body
+ color: red
View
245 spec/respond_with_spec.rb
@@ -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

0 comments on commit 0895ddf

Please sign in to comment.
Something went wrong with that request. Please try again.