├── compat ├── public │ └── foo.xml ├── views │ ├── foo.erb │ ├── foo.haml │ ├── layout_test │ │ ├── foo.erb │ │ ├── foo.haml │ │ ├── layout.erb │ │ ├── layout.haml │ │ ├── layout.sass │ │ ├── foo.builder │ │ ├── foo.sass │ │ └── layout.builder │ ├── no_layout │ │ ├── no_layout.haml │ │ └── no_layout.builder │ ├── foo.sass │ ├── foo.builder │ ├── foo_layout.erb │ └── foo_layout.haml ├── compat_test.rb ├── sym_params_test.rb ├── template_test.rb ├── helper.rb ├── filter_test.rb ├── use_in_file_templates_test.rb ├── sessions_test.rb ├── pipeline_test.rb ├── custom_error_test.rb ├── mapped_error_test.rb ├── sass_test.rb ├── builder_test.rb ├── events_test.rb ├── erb_test.rb ├── streaming_test.rb ├── haml_test.rb ├── application_test.rb └── app_test.rb ├── test ├── views │ ├── hello.test │ ├── layout2.test │ ├── hello.erb │ ├── hello.haml │ ├── layout2.erb │ ├── layout2.haml │ ├── hello.sass │ ├── hello.builder │ └── layout2.builder ├── data │ └── reload_app_file.rb ├── sinatra_test.rb ├── request_test.rb ├── server_test.rb ├── sass_test.rb ├── route_added_hook_test.rb ├── response_test.rb ├── builder_test.rb ├── haml_test.rb ├── middleware_test.rb ├── erb_test.rb ├── reload_test.rb ├── helper.rb ├── result_test.rb ├── filter_test.rb ├── static_test.rb ├── templates_test.rb ├── extensions_test.rb ├── base_test.rb ├── test_test.rb ├── mapped_error_test.rb ├── options_test.rb ├── helpers_test.rb └── routing_test.rb ├── lib ├── sinatra │ ├── images │ │ ├── 404.png │ │ └── 500.png │ ├── test │ │ ├── spec.rb │ │ ├── rspec.rb │ │ ├── unit.rb │ │ └── bacon.rb │ ├── main.rb │ ├── test.rb │ ├── compat.rb │ └── base.rb └── sinatra.rb ├── .gitignore ├── LICENSE ├── AUTHORS ├── sinatra.gemspec ├── Rakefile ├── CHANGES └── README.rdoc /compat/public/foo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/views/hello.test: -------------------------------------------------------------------------------- 1 | Hello World! 2 | -------------------------------------------------------------------------------- /test/views/layout2.test: -------------------------------------------------------------------------------- 1 | Layout 2! 2 | -------------------------------------------------------------------------------- /compat/views/foo.erb: -------------------------------------------------------------------------------- 1 | You rock <%= @name %>! -------------------------------------------------------------------------------- /compat/views/foo.haml: -------------------------------------------------------------------------------- 1 | == You rock #{@name}! -------------------------------------------------------------------------------- /compat/views/layout_test/foo.erb: -------------------------------------------------------------------------------- 1 | This is foo! -------------------------------------------------------------------------------- /compat/views/layout_test/foo.haml: -------------------------------------------------------------------------------- 1 | This is foo! -------------------------------------------------------------------------------- /test/views/hello.erb: -------------------------------------------------------------------------------- 1 | Hello <%= 'World' %> 2 | -------------------------------------------------------------------------------- /test/views/hello.haml: -------------------------------------------------------------------------------- 1 | %h1 Hello From Haml 2 | -------------------------------------------------------------------------------- /compat/views/no_layout/no_layout.haml: -------------------------------------------------------------------------------- 1 | %h1 No Layout! -------------------------------------------------------------------------------- /compat/views/foo.sass: -------------------------------------------------------------------------------- 1 | #sass 2 | :background_color #FFF -------------------------------------------------------------------------------- /compat/views/layout_test/layout.erb: -------------------------------------------------------------------------------- 1 | x <%= yield %> x 2 | -------------------------------------------------------------------------------- /compat/views/layout_test/layout.haml: -------------------------------------------------------------------------------- 1 | == x #{yield} x 2 | -------------------------------------------------------------------------------- /compat/views/layout_test/layout.sass: -------------------------------------------------------------------------------- 1 | b0rked! 2 | = yield -------------------------------------------------------------------------------- /test/views/layout2.erb: -------------------------------------------------------------------------------- 1 | ERB Layout! 2 | <%= yield %> 3 | -------------------------------------------------------------------------------- /test/views/layout2.haml: -------------------------------------------------------------------------------- 1 | %h1 HAML Layout! 2 | %p= yield 3 | -------------------------------------------------------------------------------- /compat/views/foo.builder: -------------------------------------------------------------------------------- 1 | xml.exclaim "You rock #{@name}!" 2 | -------------------------------------------------------------------------------- /compat/views/layout_test/foo.builder: -------------------------------------------------------------------------------- 1 | xml.this "is foo!" 2 | -------------------------------------------------------------------------------- /test/views/hello.sass: -------------------------------------------------------------------------------- 1 | #sass 2 | :background-color #FFF 3 | -------------------------------------------------------------------------------- /compat/views/foo_layout.erb: -------------------------------------------------------------------------------- 1 | <%= @title %> 2 | Hi <%= yield %> 3 | -------------------------------------------------------------------------------- /compat/views/foo_layout.haml: -------------------------------------------------------------------------------- 1 | == #{@title} 2 | == Hi #{yield} 3 | -------------------------------------------------------------------------------- /compat/views/layout_test/foo.sass: -------------------------------------------------------------------------------- 1 | #sass 2 | :background_color #FFF -------------------------------------------------------------------------------- /compat/views/no_layout/no_layout.builder: -------------------------------------------------------------------------------- 1 | xml.foo "No Layout!" 2 | -------------------------------------------------------------------------------- /test/views/hello.builder: -------------------------------------------------------------------------------- 1 | xml.exclaim "You're my boy, #{@name}!" 2 | -------------------------------------------------------------------------------- /test/views/layout2.builder: -------------------------------------------------------------------------------- 1 | xml.layout do 2 | xml << yield 3 | end 4 | -------------------------------------------------------------------------------- /compat/views/layout_test/layout.builder: -------------------------------------------------------------------------------- 1 | xml.layout do 2 | xml << yield 3 | end 4 | -------------------------------------------------------------------------------- /lib/sinatra/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b/sinatra/master/lib/sinatra/images/404.png -------------------------------------------------------------------------------- /lib/sinatra/images/500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b/sinatra/master/lib/sinatra/images/500.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | /dist 4 | /book 5 | /doc/api 6 | /doc/*.html 7 | .#* 8 | \#* 9 | .emacs* -------------------------------------------------------------------------------- /test/data/reload_app_file.rb: -------------------------------------------------------------------------------- 1 | $reload_count += 1 2 | 3 | $reload_app.get('/') { 'Hello from reload file' } 4 | -------------------------------------------------------------------------------- /lib/sinatra.rb: -------------------------------------------------------------------------------- 1 | libdir = File.dirname(__FILE__) 2 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 3 | 4 | require 'sinatra/base' 5 | require 'sinatra/main' 6 | require 'sinatra/compat' 7 | 8 | use_in_file_templates! 9 | -------------------------------------------------------------------------------- /lib/sinatra/test/spec.rb: -------------------------------------------------------------------------------- 1 | require 'test/spec' 2 | require 'sinatra/test' 3 | require 'sinatra/test/unit' 4 | 5 | Sinatra::Test.deprecate('test/spec') 6 | 7 | module Sinatra::Test 8 | def should 9 | @response.should 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/sinatra/test/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/test' 2 | require 'sinatra/test/unit' 3 | require 'spec' 4 | require 'spec/interop/test' 5 | 6 | Sinatra::Test.deprecate('RSpec') 7 | 8 | Sinatra::Default.set( 9 | :environment => :test, 10 | :run => false, 11 | :raise_errors => true, 12 | :logging => false 13 | ) 14 | -------------------------------------------------------------------------------- /compat/compat_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Compat" do 4 | setup do 5 | Sinatra.application = nil 6 | @app = Sinatra.application 7 | end 8 | 9 | specify "makes EventContext available" do 10 | assert_same Sinatra::Default, Sinatra::EventContext 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/sinatra/test/unit.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/test' 2 | require 'test/unit' 3 | 4 | Sinatra::Test.deprecate('test/unit') 5 | 6 | Test::Unit::TestCase.send :include, Sinatra::Test 7 | 8 | Sinatra::Default.set( 9 | :environment => :test, 10 | :run => false, 11 | :raise_errors => true, 12 | :logging => false 13 | ) 14 | -------------------------------------------------------------------------------- /test/sinatra_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Sinatra' do 4 | it 'creates a new Sinatra::Base subclass on new' do 5 | app = 6 | Sinatra.new do 7 | get '/' do 8 | 'Hello World' 9 | end 10 | end 11 | assert_same Sinatra::Base, app.superclass 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/sinatra/test/bacon.rb: -------------------------------------------------------------------------------- 1 | require 'bacon' 2 | require 'sinatra/test' 3 | 4 | Sinatra::Test.deprecate('Bacon') 5 | 6 | Sinatra::Default.set( 7 | :environment => :test, 8 | :run => false, 9 | :raise_errors => true, 10 | :logging => false 11 | ) 12 | 13 | module Sinatra::Test 14 | def should 15 | @response.should 16 | end 17 | end 18 | 19 | Bacon::Context.send(:include, Sinatra::Test) 20 | -------------------------------------------------------------------------------- /compat/sym_params_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Symbol Params" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | specify "should be accessable as Strings or Symbols" do 10 | get '/' do 11 | params[:foo] + params['foo'] 12 | end 13 | 14 | get_it '/', :foo => "X" 15 | assert_equal('XX', body) 16 | end 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /test/request_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Sinatra::Request' do 4 | it 'responds to #user_agent' do 5 | request = Sinatra::Request.new({'HTTP_USER_AGENT' => 'Test'}) 6 | assert request.respond_to?(:user_agent) 7 | assert_equal 'Test', request.user_agent 8 | end 9 | 10 | it 'parses POST params when Content-Type is form-dataish' do 11 | request = Sinatra::Request.new( 12 | 'REQUEST_METHOD' => 'PUT', 13 | 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 14 | 'rack.input' => StringIO.new('foo=bar') 15 | ) 16 | assert_equal 'bar', request.params['foo'] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /compat/template_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Templates" do 4 | 5 | specify "are read from files if Symbols" do 6 | 7 | get '/from_file' do 8 | @name = 'Alena' 9 | erb :foo, :views_directory => File.dirname(__FILE__) + "/views" 10 | end 11 | 12 | get_it '/from_file' 13 | 14 | body.should.equal 'You rock Alena!' 15 | 16 | end 17 | 18 | specify "use layout.ext by default if available" do 19 | 20 | get '/layout_from_file' do 21 | erb :foo, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 22 | end 23 | 24 | get_it '/layout_from_file' 25 | should.be.ok 26 | body.should.equal "x This is foo! x \n" 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /compat/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'mocha' 3 | 4 | # disable warnings in compat specs. 5 | $VERBOSE = nil 6 | 7 | $:.unshift File.dirname(File.dirname(__FILE__)) + "/lib" 8 | 9 | ENV['RACK_ENV'] ||= 'test' 10 | 11 | require 'sinatra' 12 | require 'sinatra/test' 13 | require 'sinatra/test/unit' 14 | require 'sinatra/test/spec' 15 | 16 | module Sinatra::Test 17 | # we need to remove the new test helper methods since they conflict with 18 | # the top-level methods of the same name. 19 | %w(get head post put delete).each do |verb| 20 | remove_method verb 21 | end 22 | include Sinatra::Delegator 23 | end 24 | 25 | class Test::Unit::TestCase 26 | include Sinatra::Test 27 | def setup 28 | @app = lambda { |env| Sinatra::Application.call(env) } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /compat/filter_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "before filters" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | @app = Sinatra.application 8 | end 9 | 10 | specify "should be executed in the order defined" do 11 | invoked = 0x0 12 | @app.before { invoked = 0x01 } 13 | @app.before { invoked |= 0x02 } 14 | @app.get('/') { 'Hello World' } 15 | get_it '/' 16 | should.be.ok 17 | body.should.be == 'Hello World' 18 | invoked.should.be == 0x03 19 | end 20 | 21 | specify "should be capable of modifying the request" do 22 | @app.get('/foo') { 'foo' } 23 | @app.get('/bar') { 'bar' } 24 | @app.before { request.path_info = '/bar' } 25 | get_it '/foo' 26 | should.be.ok 27 | body.should.be == 'bar' 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /compat/use_in_file_templates_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Rendering in file templates" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | use_in_file_templates! 8 | end 9 | 10 | specify "should set template" do 11 | assert Sinatra.application.templates[:foo] 12 | end 13 | 14 | specify "should set layout" do 15 | assert Sinatra.application.templates[:layout] 16 | end 17 | 18 | specify "should render without layout if specified" do 19 | get '/' do 20 | haml :foo, :layout => false 21 | end 22 | 23 | get_it '/' 24 | assert_equal "this is foo\n", body 25 | end 26 | 27 | specify "should render with layout if specified" do 28 | get '/' do 29 | haml :foo 30 | end 31 | 32 | get_it '/' 33 | assert_equal "X\nthis is foo\nX\n", body 34 | end 35 | 36 | end 37 | 38 | __END__ 39 | 40 | @@ foo 41 | this is foo 42 | 43 | @@ layout 44 | X 45 | = yield 46 | X 47 | 48 | -------------------------------------------------------------------------------- /test/server_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class Rack::Handler::Mock 4 | extend Test::Unit::Assertions 5 | 6 | def self.run(app, options={}) 7 | assert(app < Sinatra::Base) 8 | assert_equal 9001, options[:Port] 9 | assert_equal 'foo.local', options[:Host] 10 | yield new 11 | end 12 | 13 | def stop 14 | end 15 | end 16 | 17 | describe 'Sinatra::Base.run!' do 18 | before do 19 | mock_app { 20 | set :server, 'mock' 21 | set :host, 'foo.local' 22 | set :port, 9001 23 | } 24 | $stdout = File.open('/dev/null', 'wb') 25 | end 26 | 27 | after { $stdout = STDOUT } 28 | 29 | it "locates the appropriate Rack handler and calls ::run" do 30 | @app.run! 31 | end 32 | 33 | it "sets options on the app before running" do 34 | @app.run! :sessions => true 35 | assert @app.sessions? 36 | end 37 | 38 | it "falls back on the next server handler when not found" do 39 | @app.run! :server => %w[foo bar mock] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/sass_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "Sass Templates" do 4 | def sass_app(&block) 5 | mock_app { 6 | set :views, File.dirname(__FILE__) + '/views' 7 | get '/', &block 8 | } 9 | get '/' 10 | end 11 | 12 | it 'renders inline Sass strings' do 13 | sass_app { sass "#sass\n :background-color #FFF\n" } 14 | assert ok? 15 | assert_equal "#sass {\n background-color: #FFF; }\n", body 16 | end 17 | 18 | it 'renders .sass files in views path' do 19 | sass_app { sass :hello } 20 | assert ok? 21 | assert_equal "#sass {\n background-color: #FFF; }\n", body 22 | end 23 | 24 | it 'ignores the layout option' do 25 | sass_app { sass :hello, :layout => :layout2 } 26 | assert ok? 27 | assert_equal "#sass {\n background-color: #FFF; }\n", body 28 | end 29 | 30 | it "raises error if template not found" do 31 | mock_app { 32 | get('/') { sass :no_such_template } 33 | } 34 | assert_raise(Errno::ENOENT) { get('/') } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/sinatra/main.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | module Sinatra 4 | class Default < Base 5 | 6 | # we assume that the first file that requires 'sinatra' is the 7 | # app_file. all other path related options are calculated based 8 | # on this path by default. 9 | set :app_file, caller_files.first || $0 10 | 11 | set :run, Proc.new { $0 == app_file } 12 | 13 | if run? && ARGV.any? 14 | require 'optparse' 15 | OptionParser.new { |op| 16 | op.on('-x') { set :mutex, true } 17 | op.on('-e env') { |val| set :environment, val.to_sym } 18 | op.on('-s server') { |val| set :server, val } 19 | op.on('-p port') { |val| set :port, val.to_i } 20 | }.parse!(ARGV.dup) 21 | end 22 | end 23 | end 24 | 25 | include Sinatra::Delegator 26 | 27 | def mime(ext, type) 28 | ext = ".#{ext}" unless ext.to_s[0] == ?. 29 | Rack::Mime::MIME_TYPES[ext.to_s] = type 30 | end 31 | 32 | at_exit do 33 | raise $! if $! 34 | Sinatra::Application.run! if Sinatra::Application.run? 35 | end 36 | -------------------------------------------------------------------------------- /compat/sessions_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Sessions" do 4 | 5 | setup { Sinatra.application = nil } 6 | 7 | specify "should be off by default" do 8 | get '/asdf' do 9 | session[:test] = true 10 | "asdf" 11 | end 12 | 13 | get '/test' do 14 | session[:test] == true ? "true" : "false" 15 | end 16 | 17 | get_it '/asdf', {}, 'HTTP_HOST' => 'foo.sinatrarb.com' 18 | assert ok? 19 | assert !include?('Set-Cookie') 20 | end 21 | 22 | specify "should be able to store data accross requests" do 23 | set_option :sessions, true 24 | set_option :environment, :not_test # necessary because sessions are disabled 25 | 26 | get '/foo' do 27 | session[:test] = true 28 | "asdf" 29 | end 30 | 31 | get '/bar' do 32 | session[:test] == true ? "true" : "false" 33 | end 34 | 35 | get_it '/foo', :env => { :host => 'foo.sinatrarb.com' } 36 | assert ok? 37 | assert include?('Set-Cookie') 38 | 39 | set_option :environment, :test 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Blake Mizerany 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/route_added_hook_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | module RouteAddedTest 4 | @routes = [] 5 | def self.routes ; @routes ; end 6 | def self.route_added(verb, path) 7 | @routes << [verb, path] 8 | end 9 | end 10 | 11 | describe "route_added Hook" do 12 | 13 | before { RouteAddedTest.routes.clear } 14 | 15 | it "should be notified of an added route" do 16 | mock_app(Class.new(Sinatra::Base)) { 17 | register RouteAddedTest 18 | get('/') {} 19 | } 20 | 21 | assert_equal [["GET", "/"], ["HEAD", "/"]], 22 | RouteAddedTest.routes 23 | end 24 | 25 | it "should include hooks from superclass" do 26 | a = Class.new(Class.new(Sinatra::Base)) 27 | b = Class.new(a) 28 | 29 | a.register RouteAddedTest 30 | b.class_eval { post("/sub_app_route") {} } 31 | 32 | assert_equal [["POST", "/sub_app_route"]], 33 | RouteAddedTest.routes 34 | end 35 | 36 | it "should only run once per extension" do 37 | mock_app(Class.new(Sinatra::Base)) { 38 | register RouteAddedTest 39 | register RouteAddedTest 40 | get('/') {} 41 | } 42 | 43 | assert_equal [["GET", "/"], ["HEAD", "/"]], 44 | RouteAddedTest.routes 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /compat/pipeline_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class UpcaseMiddleware 4 | def initialize(app, *args, &block) 5 | @app = app 6 | @args = args 7 | @block = block 8 | end 9 | def call(env) 10 | env['PATH_INFO'] = env['PATH_INFO'].to_s.upcase 11 | @app.call(env) 12 | end 13 | end 14 | 15 | context "Middleware Pipelines" do 16 | 17 | setup do 18 | Sinatra.application = nil 19 | @app = Sinatra.application 20 | end 21 | 22 | teardown do 23 | Sinatra.application = nil 24 | end 25 | 26 | specify "should add middleware with use" do 27 | block = Proc.new { |env| } 28 | @app.use UpcaseMiddleware 29 | @app.use UpcaseMiddleware, "foo", "bar" 30 | @app.use UpcaseMiddleware, "foo", "bar", &block 31 | @app.send(:middleware).should.include([UpcaseMiddleware, [], nil]) 32 | @app.send(:middleware).should.include([UpcaseMiddleware, ["foo", "bar"], nil]) 33 | @app.send(:middleware).should.include([UpcaseMiddleware, ["foo", "bar"], block]) 34 | end 35 | 36 | specify "should run middleware added with use" do 37 | get('/foo') { "FAIL!" } 38 | get('/FOO') { "PASS!" } 39 | use UpcaseMiddleware 40 | get_it '/foo' 41 | should.be.ok 42 | body.should.equal "PASS!" 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /compat/custom_error_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Custom Errors" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | specify "override the default 404" do 10 | 11 | get_it '/' 12 | should.be.not_found 13 | body.should.equal '

Not Found

' 14 | 15 | error Sinatra::NotFound do 16 | 'Custom 404' 17 | end 18 | 19 | get_it '/' 20 | should.be.not_found 21 | body.should.equal 'Custom 404' 22 | 23 | end 24 | 25 | specify "override the default 500" do 26 | Sinatra.application.options.raise_errors = false 27 | 28 | get '/' do 29 | raise 'asdf' 30 | end 31 | 32 | get_it '/' 33 | status.should.equal 500 34 | body.should.equal '

Internal Server Error

' 35 | 36 | 37 | error do 38 | 'Custom 500 for ' + request.env['sinatra.error'].message 39 | end 40 | 41 | get_it '/' 42 | 43 | get_it '/' 44 | status.should.equal 500 45 | body.should.equal 'Custom 500 for asdf' 46 | 47 | Sinatra.application.options.raise_errors = true 48 | end 49 | 50 | class UnmappedError < RuntimeError; end 51 | 52 | specify "should bring unmapped error back to the top" do 53 | get '/' do 54 | raise UnmappedError, 'test' 55 | end 56 | 57 | assert_raises(UnmappedError) do 58 | get_it '/' 59 | end 60 | end 61 | 62 | end 63 | -------------------------------------------------------------------------------- /test/response_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.dirname(__FILE__) + '/helper' 4 | 5 | describe 'Sinatra::Response' do 6 | before do 7 | @response = Sinatra::Response.new 8 | end 9 | 10 | it "initializes with 200, text/html, and empty body" do 11 | assert_equal 200, @response.status 12 | assert_equal 'text/html', @response['Content-Type'] 13 | assert_equal [], @response.body 14 | end 15 | 16 | it 'uses case insensitive headers' do 17 | @response['content-type'] = 'application/foo' 18 | assert_equal 'application/foo', @response['Content-Type'] 19 | assert_equal 'application/foo', @response['CONTENT-TYPE'] 20 | end 21 | 22 | it 'writes to body' do 23 | @response.body = 'Hello' 24 | @response.write ' World' 25 | assert_equal 'Hello World', @response.body 26 | end 27 | 28 | [204, 304].each do |status_code| 29 | it "removes the Content-Type header and body when response status is #{status_code}" do 30 | @response.status = status_code 31 | @response.body = ['Hello World'] 32 | assert_equal [status_code, {}, []], @response.finish 33 | end 34 | end 35 | 36 | it 'Calculates the Content-Length using the bytesize of the body' do 37 | @response.body = ['Hello', 'World!', '✈'] 38 | status, headers, body = @response.finish 39 | assert_equal '14', headers['Content-Length'] 40 | assert_equal @response.body, body 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /compat/mapped_error_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class FooError < RuntimeError; end 4 | 5 | context "Mapped errors" do 6 | 7 | setup do 8 | Sinatra.application = nil 9 | Sinatra.application.options.raise_errors = false 10 | end 11 | 12 | specify "are rescued and run in context" do 13 | 14 | error FooError do 15 | 'MAPPED ERROR!' 16 | end 17 | 18 | get '/' do 19 | raise FooError 20 | end 21 | 22 | get_it '/' 23 | 24 | should.be.server_error 25 | body.should.equal 'MAPPED ERROR!' 26 | 27 | end 28 | 29 | specify "renders empty if no each method on result" do 30 | 31 | error FooError do 32 | nil 33 | end 34 | 35 | get '/' do 36 | raise FooError 37 | end 38 | 39 | get_it '/' 40 | 41 | should.be.server_error 42 | body.should.be.empty 43 | 44 | end 45 | 46 | specify "doesn't override status if set" do 47 | 48 | error FooError do 49 | status(200) 50 | end 51 | 52 | get '/' do 53 | raise FooError 54 | end 55 | 56 | get_it '/' 57 | 58 | should.be.ok 59 | 60 | end 61 | 62 | specify "raises errors when the raise_errors option is set" do 63 | Sinatra.application.options.raise_errors = true 64 | error FooError do 65 | end 66 | get '/' do 67 | raise FooError 68 | end 69 | assert_raises(FooError) { get_it('/') } 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /compat/sass_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Sass" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | context "Templates (in general)" do 10 | 11 | setup do 12 | Sinatra.application = nil 13 | end 14 | 15 | specify "are read from files if Symbols" do 16 | 17 | get '/from_file' do 18 | sass :foo, :views_directory => File.dirname(__FILE__) + "/views" 19 | end 20 | 21 | get_it '/from_file' 22 | should.be.ok 23 | body.should.equal "#sass {\n background_color: #FFF; }\n" 24 | 25 | end 26 | 27 | specify "raise an error if template not found" do 28 | get '/' do 29 | sass :not_found 30 | end 31 | 32 | lambda { get_it '/' }.should.raise(Errno::ENOENT) 33 | end 34 | 35 | specify "ignore default layout file with .sass extension" do 36 | get '/' do 37 | sass :foo, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 38 | end 39 | 40 | get_it '/' 41 | should.be.ok 42 | body.should.equal "#sass {\n background_color: #FFF; }\n" 43 | end 44 | 45 | specify "ignore explicitly specified layout file" do 46 | get '/' do 47 | sass :foo, :layout => :layout, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 48 | end 49 | 50 | get_it '/' 51 | should.be.ok 52 | body.should.equal "#sass {\n background_color: #FFF; }\n" 53 | end 54 | 55 | end 56 | 57 | end 58 | -------------------------------------------------------------------------------- /test/builder_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "Builder Templates" do 4 | def builder_app(&block) 5 | mock_app { 6 | set :views, File.dirname(__FILE__) + '/views' 7 | get '/', &block 8 | } 9 | get '/' 10 | end 11 | 12 | it 'renders inline Builder strings' do 13 | builder_app { builder 'xml.instruct!' } 14 | assert ok? 15 | assert_equal %{\n}, body 16 | end 17 | 18 | it 'renders inline blocks' do 19 | builder_app { 20 | @name = "Frank & Mary" 21 | builder do |xml| 22 | xml.couple @name 23 | end 24 | } 25 | assert ok? 26 | assert_equal "Frank & Mary\n", body 27 | end 28 | 29 | it 'renders .builder files in views path' do 30 | builder_app { 31 | @name = "Blue" 32 | builder :hello 33 | } 34 | assert ok? 35 | assert_equal %(You're my boy, Blue!\n), body 36 | end 37 | 38 | it "renders with inline layouts" do 39 | mock_app { 40 | layout do 41 | %(xml.layout { xml << yield }) 42 | end 43 | get('/') { builder %(xml.em 'Hello World') } 44 | } 45 | get '/' 46 | assert ok? 47 | assert_equal "\nHello World\n\n", body 48 | end 49 | 50 | it "renders with file layouts" do 51 | builder_app { 52 | builder %(xml.em 'Hello World'), :layout => :layout2 53 | } 54 | assert ok? 55 | assert_equal "\nHello World\n\n", body 56 | end 57 | 58 | it "raises error if template not found" do 59 | mock_app { 60 | get('/') { builder :no_such_template } 61 | } 62 | assert_raise(Errno::ENOENT) { get('/') } 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/haml_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "HAML Templates" do 4 | def haml_app(&block) 5 | mock_app { 6 | set :views, File.dirname(__FILE__) + '/views' 7 | get '/', &block 8 | } 9 | get '/' 10 | end 11 | 12 | it 'renders inline HAML strings' do 13 | haml_app { haml '%h1 Hiya' } 14 | assert ok? 15 | assert_equal "

Hiya

\n", body 16 | end 17 | 18 | it 'renders .haml files in views path' do 19 | haml_app { haml :hello } 20 | assert ok? 21 | assert_equal "

Hello From Haml

\n", body 22 | end 23 | 24 | it "renders with inline layouts" do 25 | mock_app { 26 | layout { %q(%h1= 'THIS. IS. ' + yield.upcase) } 27 | get('/') { haml '%em Sparta' } 28 | } 29 | get '/' 30 | assert ok? 31 | assert_equal "

THIS. IS. SPARTA

\n", body 32 | end 33 | 34 | it "renders with file layouts" do 35 | haml_app { 36 | haml 'Hello World', :layout => :layout2 37 | } 38 | assert ok? 39 | assert_equal "

HAML Layout!

\n

Hello World

\n", body 40 | end 41 | 42 | it "raises error if template not found" do 43 | mock_app { 44 | get('/') { haml :no_such_template } 45 | } 46 | assert_raise(Errno::ENOENT) { get('/') } 47 | end 48 | 49 | it "passes HAML options to the Haml engine" do 50 | haml_app { 51 | haml "!!!\n%h1 Hello World", :options => {:format => :html5} 52 | } 53 | assert ok? 54 | assert_equal "\n

Hello World

\n", body 55 | end 56 | 57 | it "passes default HAML options to the Haml engine" do 58 | mock_app { 59 | set :haml, {:format => :html5} 60 | get '/' do 61 | haml "!!!\n%h1 Hello World" 62 | end 63 | } 64 | get '/' 65 | assert ok? 66 | assert_equal "\n

Hello World

\n", body 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/middleware_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "Middleware" do 4 | before do 5 | @app = mock_app(Sinatra::Default) { 6 | get '/*' do 7 | response.headers['X-Tests'] = env['test.ran']. 8 | map { |n| n.split('::').last }. 9 | join(', ') 10 | env['PATH_INFO'] 11 | end 12 | } 13 | end 14 | 15 | class MockMiddleware < Struct.new(:app) 16 | def call(env) 17 | (env['test.ran'] ||= []) << self.class.to_s 18 | app.call(env) 19 | end 20 | end 21 | 22 | class UpcaseMiddleware < MockMiddleware 23 | def call(env) 24 | env['PATH_INFO'] = env['PATH_INFO'].upcase 25 | super 26 | end 27 | end 28 | 29 | it "is added with Sinatra::Application.use" do 30 | @app.use UpcaseMiddleware 31 | get '/hello-world' 32 | assert ok? 33 | assert_equal '/HELLO-WORLD', body 34 | end 35 | 36 | class DowncaseMiddleware < MockMiddleware 37 | def call(env) 38 | env['PATH_INFO'] = env['PATH_INFO'].downcase 39 | super 40 | end 41 | end 42 | 43 | it "runs in the order defined" do 44 | @app.use UpcaseMiddleware 45 | @app.use DowncaseMiddleware 46 | get '/Foo' 47 | assert_equal "/foo", body 48 | assert_equal "UpcaseMiddleware, DowncaseMiddleware", response['X-Tests'] 49 | end 50 | 51 | it "resets the prebuilt pipeline when new middleware is added" do 52 | @app.use UpcaseMiddleware 53 | get '/Foo' 54 | assert_equal "/FOO", body 55 | @app.use DowncaseMiddleware 56 | get '/Foo' 57 | assert_equal '/foo', body 58 | assert_equal "UpcaseMiddleware, DowncaseMiddleware", response['X-Tests'] 59 | end 60 | 61 | it "works when app is used as middleware" do 62 | @app.use UpcaseMiddleware 63 | @app = @app.new 64 | get '/Foo' 65 | assert_equal "/FOO", body 66 | assert_equal "UpcaseMiddleware", response['X-Tests'] 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/erb_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "ERB Templates" do 4 | def erb_app(&block) 5 | mock_app { 6 | set :views, File.dirname(__FILE__) + '/views' 7 | get '/', &block 8 | } 9 | get '/' 10 | end 11 | 12 | it 'renders inline ERB strings' do 13 | erb_app { erb '<%= 1 + 1 %>' } 14 | assert ok? 15 | assert_equal '2', body 16 | end 17 | 18 | it 'renders .erb files in views path' do 19 | erb_app { erb :hello } 20 | assert ok? 21 | assert_equal "Hello World\n", body 22 | end 23 | 24 | it 'takes a :locals option' do 25 | erb_app { 26 | locals = {:foo => 'Bar'} 27 | erb '<%= foo %>', :locals => locals 28 | } 29 | assert ok? 30 | assert_equal 'Bar', body 31 | end 32 | 33 | it "renders with inline layouts" do 34 | mock_app { 35 | layout { 'THIS. IS. <%= yield.upcase %>!' } 36 | get('/') { erb 'Sparta' } 37 | } 38 | get '/' 39 | assert ok? 40 | assert_equal 'THIS. IS. SPARTA!', body 41 | end 42 | 43 | it "renders with file layouts" do 44 | erb_app { 45 | erb 'Hello World', :layout => :layout2 46 | } 47 | assert ok? 48 | assert_equal "ERB Layout!\nHello World\n", body 49 | end 50 | 51 | it "renders erb with blocks" do 52 | mock_app { 53 | def container 54 | @_out_buf << "THIS." 55 | yield 56 | @_out_buf << "SPARTA!" 57 | end 58 | def is; "IS." end 59 | get '/' do 60 | erb '<% container do %> <%= is %> <% end %>' 61 | end 62 | } 63 | get '/' 64 | assert ok? 65 | assert_equal 'THIS. IS. SPARTA!', body 66 | end 67 | 68 | it "can be used in a nested fashion for partials and whatnot" do 69 | mock_app { 70 | template(:inner) { "<%= 'hi' %>" } 71 | template(:outer) { "<%= erb :inner %>" } 72 | get '/' do 73 | erb :outer 74 | end 75 | } 76 | 77 | get '/' 78 | assert ok? 79 | assert_equal 'hi', body 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/reload_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | $reload_count = 0 4 | $reload_app = nil 5 | 6 | describe "Reloading" do 7 | before { 8 | @app = mock_app(Sinatra::Default) 9 | $reload_app = @app 10 | } 11 | 12 | after { 13 | $reload_app = nil 14 | } 15 | 16 | it 'is enabled by default when in development and the app_file is set' do 17 | @app.set :app_file, __FILE__ 18 | @app.set :environment, :development 19 | assert_same true, @app.reload 20 | assert_same true, @app.reload? 21 | end 22 | 23 | it 'is disabled by default when running in non-development environment' do 24 | @app.set :app_file, __FILE__ 25 | @app.set :environment, :test 26 | assert !@app.reload 27 | assert_same false, @app.reload? 28 | end 29 | 30 | it 'is disabled by default when no app_file is available' do 31 | @app.set :app_file, nil 32 | @app.set :environment, :development 33 | assert !@app.reload 34 | assert_same false, @app.reload? 35 | end 36 | 37 | it 'is disabled when app_file is a rackup (.ru) file' do 38 | @app.set :app_file, __FILE__.sub(/\.rb$/, '.ru') 39 | @app.set :environment, :development 40 | assert !@app.reload 41 | assert_same false, @app.reload? 42 | end 43 | 44 | it 'can be turned off explicitly' do 45 | @app.set :app_file, __FILE__ 46 | @app.set :environment, :development 47 | assert_same true, @app.reload 48 | @app.set :reload, false 49 | assert_same false, @app.reload 50 | assert_same false, @app.reload? 51 | end 52 | 53 | it 'reloads the app_file each time a request is made' do 54 | @app.set :app_file, File.dirname(__FILE__) + '/data/reload_app_file.rb' 55 | @app.set :reload, true 56 | @app.get('/') { 'Hello World' } 57 | 58 | get '/' 59 | assert_equal 200, status 60 | assert_equal 'Hello from reload file', body 61 | assert_equal 1, $reload_count 62 | 63 | get '/' 64 | assert_equal 200, status 65 | assert_equal 'Hello from reload file', body 66 | assert_equal 2, $reload_count 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rack' 3 | rescue LoadError 4 | require 'rubygems' 5 | require 'rack' 6 | end 7 | 8 | libdir = File.dirname(File.dirname(__FILE__)) + '/lib' 9 | $LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir) 10 | 11 | require 'test/unit' 12 | require 'sinatra/test' 13 | 14 | class Sinatra::Base 15 | # Allow assertions in request context 16 | include Test::Unit::Assertions 17 | end 18 | 19 | class Test::Unit::TestCase 20 | include Sinatra::Test 21 | 22 | # Sets up a Sinatra::Base subclass defined with the block 23 | # given. Used in setup or individual spec methods to establish 24 | # the application. 25 | def mock_app(base=Sinatra::Base, &block) 26 | @app = Sinatra.new(base, &block) 27 | end 28 | 29 | def restore_default_options 30 | Sinatra::Default.set( 31 | :environment => :development, 32 | :raise_errors => Proc.new { test? }, 33 | :dump_errors => true, 34 | :sessions => false, 35 | :logging => Proc.new { ! test? }, 36 | :methodoverride => true, 37 | :static => true, 38 | :run => Proc.new { ! test? } 39 | ) 40 | end 41 | end 42 | 43 | ## 44 | # test/spec/mini 45 | # http://pastie.caboo.se/158871 46 | # chris@ozmm.org 47 | # 48 | def describe(*args, &block) 49 | return super unless (name = args.first.capitalize) && block 50 | name = "#{name.gsub(/\W/, '')}Test" 51 | Object.send :const_set, name, Class.new(Test::Unit::TestCase) 52 | klass = Object.const_get(name) 53 | klass.class_eval do 54 | def self.it(name, &block) 55 | define_method("test_#{name.gsub(/\W/,'_').downcase}", &block) 56 | end 57 | def self.xspecify(*args) end 58 | def self.before(&block) define_method(:setup, &block) end 59 | def self.after(&block) define_method(:teardown, &block) end 60 | end 61 | klass.class_eval &block 62 | klass 63 | end 64 | 65 | def describe_option(name, &block) 66 | klass = describe("Option #{name}", &block) 67 | klass.before do 68 | restore_default_options 69 | @base = Sinatra.new 70 | @default = Class.new(Sinatra::Default) 71 | end 72 | klass 73 | end 74 | 75 | # Do not output warnings for the duration of the block. 76 | def silence_warnings 77 | $VERBOSE, v = nil, $VERBOSE 78 | yield 79 | ensure 80 | $VERBOSE = v 81 | end 82 | -------------------------------------------------------------------------------- /test/result_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Result Handling' do 4 | it "sets response.body when result is a String" do 5 | mock_app { 6 | get '/' do 7 | 'Hello World' 8 | end 9 | } 10 | 11 | get '/' 12 | assert ok? 13 | assert_equal 'Hello World', body 14 | end 15 | 16 | it "sets response.body when result is an Array of Strings" do 17 | mock_app { 18 | get '/' do 19 | ['Hello', 'World'] 20 | end 21 | } 22 | 23 | get '/' 24 | assert ok? 25 | assert_equal 'HelloWorld', body 26 | end 27 | 28 | it "sets response.body when result responds to #each" do 29 | mock_app { 30 | get '/' do 31 | res = lambda { 'Hello World' } 32 | def res.each ; yield call ; end 33 | res 34 | end 35 | } 36 | 37 | get '/' 38 | assert ok? 39 | assert_equal 'Hello World', body 40 | end 41 | 42 | it "sets response.body to [] when result is nil" do 43 | mock_app { 44 | get '/' do 45 | nil 46 | end 47 | } 48 | 49 | get '/' 50 | assert ok? 51 | assert_equal '', body 52 | end 53 | 54 | it "sets status, headers, and body when result is a Rack response tuple" do 55 | mock_app { 56 | get '/' do 57 | [205, {'Content-Type' => 'foo/bar'}, 'Hello World'] 58 | end 59 | } 60 | 61 | get '/' 62 | assert_equal 205, status 63 | assert_equal 'foo/bar', response['Content-Type'] 64 | assert_equal 'Hello World', body 65 | end 66 | 67 | it "sets status and body when result is a two-tuple" do 68 | mock_app { 69 | get '/' do 70 | [409, 'formula of'] 71 | end 72 | } 73 | 74 | get '/' 75 | assert_equal 409, status 76 | assert_equal 'formula of', body 77 | end 78 | 79 | it "raises a TypeError when result is a non two or three tuple Array" do 80 | mock_app { 81 | get '/' do 82 | [409, 'formula of', 'something else', 'even more'] 83 | end 84 | } 85 | 86 | assert_raise(TypeError) { get '/' } 87 | end 88 | 89 | it "sets status when result is a Fixnum status code" do 90 | mock_app { 91 | get('/') { 205 } 92 | } 93 | 94 | get '/' 95 | assert_equal 205, status 96 | assert_equal '', body 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /compat/builder_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Builder" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | context "without layouts" do 10 | 11 | setup do 12 | Sinatra.application = nil 13 | end 14 | 15 | specify "should render" do 16 | 17 | get '/no_layout' do 18 | builder 'xml.instruct!' 19 | end 20 | 21 | get_it '/no_layout' 22 | should.be.ok 23 | body.should == %(\n) 24 | 25 | end 26 | 27 | specify "should render inline block" do 28 | 29 | get '/no_layout_and_inlined' do 30 | @name = "Frank & Mary" 31 | builder do |xml| 32 | xml.couple @name 33 | end 34 | end 35 | 36 | get_it '/no_layout_and_inlined' 37 | should.be.ok 38 | body.should == %(Frank & Mary\n) 39 | 40 | end 41 | 42 | end 43 | 44 | 45 | 46 | context "Templates (in general)" do 47 | 48 | setup do 49 | Sinatra.application = nil 50 | end 51 | 52 | specify "are read from files if Symbols" do 53 | 54 | get '/from_file' do 55 | @name = 'Blue' 56 | builder :foo, :views_directory => File.dirname(__FILE__) + "/views" 57 | end 58 | 59 | get_it '/from_file' 60 | should.be.ok 61 | body.should.equal %(You rock Blue!\n) 62 | 63 | end 64 | 65 | specify "use layout.ext by default if available" do 66 | 67 | get '/' do 68 | builder :foo, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 69 | end 70 | 71 | get_it '/' 72 | should.be.ok 73 | body.should.equal "\nis foo!\n\n" 74 | 75 | end 76 | 77 | specify "renders without layout" do 78 | 79 | get '/' do 80 | builder :no_layout, :views_directory => File.dirname(__FILE__) + "/views/no_layout" 81 | end 82 | 83 | get_it '/' 84 | should.be.ok 85 | body.should.equal "No Layout!\n" 86 | 87 | end 88 | 89 | specify "raises error if template not found" do 90 | 91 | get '/' do 92 | builder :not_found 93 | end 94 | 95 | lambda { get_it '/' }.should.raise(Errno::ENOENT) 96 | 97 | end 98 | 99 | end 100 | 101 | end 102 | -------------------------------------------------------------------------------- /test/filter_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe "Filters" do 4 | it "executes filters in the order defined" do 5 | count = 0 6 | mock_app do 7 | get('/') { 'Hello World' } 8 | before { 9 | assert_equal 0, count 10 | count = 1 11 | } 12 | before { 13 | assert_equal 1, count 14 | count = 2 15 | } 16 | end 17 | 18 | get '/' 19 | assert ok? 20 | assert_equal 2, count 21 | assert_equal 'Hello World', body 22 | end 23 | 24 | it "allows filters to modify the request" do 25 | mock_app { 26 | get('/foo') { 'foo' } 27 | get('/bar') { 'bar' } 28 | before { request.path_info = '/bar' } 29 | } 30 | 31 | get '/foo' 32 | assert ok? 33 | assert_equal 'bar', body 34 | end 35 | 36 | it "can modify instance variables available to routes" do 37 | mock_app { 38 | before { @foo = 'bar' } 39 | get('/foo') { @foo } 40 | } 41 | 42 | get '/foo' 43 | assert ok? 44 | assert_equal 'bar', body 45 | end 46 | 47 | it "allows redirects in filters" do 48 | mock_app { 49 | before { redirect '/bar' } 50 | get('/foo') do 51 | fail 'before block should have halted processing' 52 | 'ORLY?!' 53 | end 54 | } 55 | 56 | get '/foo' 57 | assert redirect? 58 | assert_equal '/bar', response['Location'] 59 | assert_equal '', body 60 | end 61 | 62 | it "does not modify the response with its return value" do 63 | mock_app { 64 | before { 'Hello World!' } 65 | get '/foo' do 66 | assert_equal [], response.body 67 | 'cool' 68 | end 69 | } 70 | 71 | get '/foo' 72 | assert ok? 73 | assert_equal 'cool', body 74 | end 75 | 76 | it "does modify the response with halt" do 77 | mock_app { 78 | before { halt 302, 'Hi' } 79 | get '/foo' do 80 | "should not happen" 81 | end 82 | } 83 | 84 | get '/foo' 85 | assert_equal 302, response.status 86 | assert_equal 'Hi', body 87 | end 88 | 89 | it "gives you access to params" do 90 | mock_app { 91 | before { @foo = params['foo'] } 92 | get('/foo') { @foo } 93 | } 94 | 95 | get '/foo?foo=cool' 96 | assert ok? 97 | assert_equal 'cool', body 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Sinatra was designed and developed by Blake Mizerany (bmizerany) in 2 | California. Continued development would not be possible without the ongoing 3 | financial support provided by [Heroku](http://heroku.com) and the emotional 4 | support provided by Adam Wiggins (adamwiggins) of Heroku, Chris Wanstrath (defunkt), 5 | PJ Hyett (pjhyett), and the rest of the GitHub crew. 6 | 7 | Special thanks to the following extraordinary individuals, who-out which 8 | Sinatra would not be possible: 9 | 10 | * Ryan Tomayko (rtomayko) for constantly fixing whitespace errors 60d5006 11 | * Ezra Zygmuntowicz (ezmobius) for initial help and letting Blake steal 12 | some of merbs internal code. 13 | * Christopher Schneid (cschneid) for The Book, the blog (gittr.com), 14 | irclogger.com, and a bunch of useful patches. 15 | * Markus Prinz (cypher) for patches over the years, caring about 16 | the README, and hanging in there when times were rough. 17 | * Simon Rozet (sr) for a ton of doc patches, HAML options, and all that 18 | advocacy stuff he's going to do for 1.0. 19 | * Erik Kastner (kastner) for fixing `MIME_TYPES` under Rack 0.5. 20 | * Ben Bleything (bleything) for caring about HTTP status codes and doc fixes. 21 | * Igal Koshevoy (igal) for root path detection under Thin/Passenger. 22 | * Jon Crosby (jcrosby) for coffee breaks, doc fixes, and just because, man. 23 | * Karel Minarik (karmi) for screaming until the website came back up. 24 | * Jeremy Evans (jeremyevans) for unbreaking optional path params (twice!) 25 | * The GitHub guys for stealing Blake's table. 26 | * Nickolas Means (nmeans) for Sass template support. 27 | * Victor Hugo Borja (vic) for splat'n routes specs and doco. 28 | * Avdi Grimm (avdi) for basic RSpec support. 29 | * Jack Danger Canty for a more accurate root directory and for making me 30 | watch [this](http://www.youtube.com/watch?v=ueaHLHgskkw) just now. 31 | * Mathew Walker for making escaped paths work with static files. 32 | * Millions of Us for having the problem that led to Sinatra's conception. 33 | * Songbird for the problems that helped Sinatra's future become realized. 34 | * Rick Olson (technoweenie) for the killer plug at RailsConf '08. 35 | * Steven Garcia for the amazing custom artwork you see on 404's and 500's 36 | * Pat Nakajima (nakajima) for fixing non-nested params in nested params Hash's. 37 | 38 | and last but not least: 39 | 40 | * Frank Sinatra (chairman of the board) for having so much class he 41 | deserves a web-framework named after him. 42 | -------------------------------------------------------------------------------- /compat/events_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Simple Events" do 4 | def simple_request_hash(method, path) 5 | Rack::Request.new({ 6 | 'REQUEST_METHOD' => method.to_s.upcase, 7 | 'PATH_INFO' => path 8 | }) 9 | end 10 | 11 | class MockResult < Struct.new(:block, :params) 12 | end 13 | 14 | def invoke_simple(path, request_path, &b) 15 | params = nil 16 | get path do 17 | params = self.params 18 | b.call if b 19 | end 20 | get_it request_path 21 | MockResult.new(b, params) 22 | end 23 | 24 | setup { Sinatra.application = nil } 25 | 26 | specify "return last value" do 27 | block = Proc.new { 'Simple' } 28 | result = invoke_simple('/', '/', &block) 29 | result.should.not.be.nil 30 | result.block.should.be block 31 | result.params.should.equal Hash.new 32 | end 33 | 34 | specify "takes params in path" do 35 | result = invoke_simple('/:foo/:bar', '/a/b') 36 | result.should.not.be.nil 37 | result.params.should.equal "foo" => 'a', "bar" => 'b' 38 | 39 | # unscapes 40 | Sinatra.application = nil 41 | result = invoke_simple('/:foo/:bar', '/a/blake%20mizerany') 42 | result.should.not.be.nil 43 | result.params.should.equal "foo" => 'a', "bar" => 'blake mizerany' 44 | end 45 | 46 | specify "takes optional params in path" do 47 | result = invoke_simple('/?:foo?/?:bar?', '/a/b') 48 | result.should.not.be.nil 49 | result.params.should.equal "foo" => 'a', "bar" => 'b' 50 | 51 | Sinatra.application = nil 52 | result = invoke_simple('/?:foo?/?:bar?', '/a/') 53 | result.should.not.be.nil 54 | result.params.should.equal "foo" => 'a', "bar" => nil 55 | 56 | Sinatra.application = nil 57 | result = invoke_simple('/?:foo?/?:bar?', '/a') 58 | result.should.not.be.nil 59 | result.params.should.equal "foo" => 'a', "bar" => nil 60 | 61 | Sinatra.application = nil 62 | result = invoke_simple('/:foo?/?:bar?', '/') 63 | result.should.not.be.nil 64 | result.params.should.equal "foo" => nil, "bar" => nil 65 | end 66 | 67 | specify "ignores to many /'s" do 68 | result = invoke_simple('/x/y', '/x//y') 69 | result.should.not.be.nil 70 | end 71 | 72 | specify "understands splat" do 73 | invoke_simple('/foo/*', '/foo/bar').should.not.be.nil 74 | invoke_simple('/foo/*', '/foo/bar/baz').should.not.be.nil 75 | invoke_simple('/foo/*', '/foo/baz').should.not.be.nil 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /test/static_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Static' do 4 | before do 5 | mock_app { 6 | set :static, true 7 | set :public, File.dirname(__FILE__) 8 | } 9 | end 10 | 11 | it 'serves GET requests for files in the public directory' do 12 | get "/#{File.basename(__FILE__)}" 13 | assert ok? 14 | assert_equal File.read(__FILE__), body 15 | assert_equal File.size(__FILE__).to_s, response['Content-Length'] 16 | assert response.headers.include?('Last-Modified') 17 | end 18 | 19 | it 'produces a body that can be iterated over multiple times' do 20 | env = Rack::MockRequest.env_for("/#{File.basename(__FILE__)}") 21 | status, headers, body = @app.call(env) 22 | buf1, buf2 = [], [] 23 | body.each { |part| buf1 << part } 24 | body.each { |part| buf2 << part } 25 | assert_equal buf1.join, buf2.join 26 | assert_equal File.read(__FILE__), buf1.join 27 | end 28 | 29 | it 'serves HEAD requests for files in the public directory' do 30 | head "/#{File.basename(__FILE__)}" 31 | assert ok? 32 | assert_equal '', body 33 | assert_equal File.size(__FILE__).to_s, response['Content-Length'] 34 | assert response.headers.include?('Last-Modified') 35 | end 36 | 37 | it 'serves files in preference to custom routes' do 38 | @app.get("/#{File.basename(__FILE__)}") { 'Hello World' } 39 | get "/#{File.basename(__FILE__)}" 40 | assert ok? 41 | assert body != 'Hello World' 42 | end 43 | 44 | it 'does not serve directories' do 45 | get "/" 46 | assert not_found? 47 | end 48 | 49 | it 'passes to the next handler when the static option is disabled' do 50 | @app.set :static, false 51 | get "/#{File.basename(__FILE__)}" 52 | assert not_found? 53 | end 54 | 55 | it 'passes to the next handler when the public option is nil' do 56 | @app.set :public, nil 57 | get "/#{File.basename(__FILE__)}" 58 | assert not_found? 59 | end 60 | 61 | it '404s when a file is not found' do 62 | get "/foobarbaz.txt" 63 | assert not_found? 64 | end 65 | 66 | it 'serves files when .. path traverses within public directory' do 67 | get "/data/../#{File.basename(__FILE__)}" 68 | assert ok? 69 | assert_equal File.read(__FILE__), body 70 | end 71 | 72 | it '404s when .. path traverses outside of public directory' do 73 | mock_app { 74 | set :static, true 75 | set :public, File.dirname(__FILE__) + '/data' 76 | } 77 | get "/../#{File.basename(__FILE__)}" 78 | assert not_found? 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/templates_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Templating' do 4 | def render_app(&block) 5 | mock_app { 6 | def render_test(template, data, options, &block) 7 | inner = block ? block.call : '' 8 | data + inner 9 | end 10 | set :views, File.dirname(__FILE__) + '/views' 11 | get '/', &block 12 | template(:layout3) { "Layout 3!\n" } 13 | } 14 | get '/' 15 | end 16 | 17 | def with_default_layout 18 | layout = File.dirname(__FILE__) + '/views/layout.test' 19 | File.open(layout, 'wb') { |io| io.write "Layout!\n" } 20 | yield 21 | ensure 22 | File.unlink(layout) rescue nil 23 | end 24 | 25 | it 'renders String templates directly' do 26 | render_app { render :test, 'Hello World' } 27 | assert ok? 28 | assert_equal 'Hello World', body 29 | end 30 | 31 | it 'renders Proc templates using the call result' do 32 | render_app { render :test, Proc.new {'Hello World'} } 33 | assert ok? 34 | assert_equal 'Hello World', body 35 | end 36 | 37 | it 'looks up Symbol templates in views directory' do 38 | render_app { render :test, :hello } 39 | assert ok? 40 | assert_equal "Hello World!\n", body 41 | end 42 | 43 | it 'uses the default layout template if not explicitly overridden' do 44 | with_default_layout do 45 | render_app { render :test, :hello } 46 | assert ok? 47 | assert_equal "Layout!\nHello World!\n", body 48 | end 49 | end 50 | 51 | it 'uses the default layout template if not really overriden' do 52 | with_default_layout do 53 | render_app { render :test, :hello, :layout => true } 54 | assert ok? 55 | assert_equal "Layout!\nHello World!\n", body 56 | end 57 | end 58 | 59 | it 'uses the layout template specified' do 60 | render_app { render :test, :hello, :layout => :layout2 } 61 | assert ok? 62 | assert_equal "Layout 2!\nHello World!\n", body 63 | end 64 | 65 | it 'uses layout templates defined with the #template method' do 66 | render_app { render :test, :hello, :layout => :layout3 } 67 | assert ok? 68 | assert_equal "Layout 3!\nHello World!\n", body 69 | end 70 | 71 | it 'loads templates from source file with use_in_file_templates!' do 72 | mock_app { 73 | use_in_file_templates! 74 | } 75 | assert_equal "this is foo\n\n", @app.templates[:foo] 76 | assert_equal "X\n= yield\nX\n", @app.templates[:layout] 77 | end 78 | end 79 | 80 | __END__ 81 | 82 | @@ foo 83 | this is foo 84 | 85 | @@ layout 86 | X 87 | = yield 88 | X 89 | -------------------------------------------------------------------------------- /test/extensions_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Registering extensions' do 4 | module FooExtensions 5 | def foo 6 | end 7 | 8 | private 9 | def im_hiding_in_ur_foos 10 | end 11 | end 12 | 13 | module BarExtensions 14 | def bar 15 | end 16 | end 17 | 18 | module BazExtensions 19 | def baz 20 | end 21 | end 22 | 23 | module QuuxExtensions 24 | def quux 25 | end 26 | end 27 | 28 | it 'will add the methods to the DSL for the class in which you register them and its subclasses' do 29 | Sinatra::Base.register FooExtensions 30 | assert Sinatra::Base.respond_to?(:foo) 31 | 32 | Sinatra::Default.register BarExtensions 33 | assert Sinatra::Default.respond_to?(:bar) 34 | assert Sinatra::Default.respond_to?(:foo) 35 | assert !Sinatra::Base.respond_to?(:bar) 36 | end 37 | 38 | it 'allows extending by passing a block' do 39 | Sinatra::Base.register { 40 | def im_in_ur_anonymous_module; end 41 | } 42 | assert Sinatra::Base.respond_to?(:im_in_ur_anonymous_module) 43 | end 44 | 45 | it 'will make sure any public methods added via Default#register are delegated to Sinatra::Delegator' do 46 | Sinatra::Default.register FooExtensions 47 | assert Sinatra::Delegator.private_instance_methods. 48 | map { |m| m.to_sym }.include?(:foo) 49 | assert !Sinatra::Delegator.private_instance_methods. 50 | map { |m| m.to_sym }.include?(:im_hiding_in_ur_foos) 51 | end 52 | 53 | it 'will not delegate methods on Base#register' do 54 | Sinatra::Base.register QuuxExtensions 55 | assert !Sinatra::Delegator.private_instance_methods.include?("quux") 56 | end 57 | 58 | it 'will extend the Sinatra::Default application by default' do 59 | Sinatra.register BazExtensions 60 | assert !Sinatra::Base.respond_to?(:baz) 61 | assert Sinatra::Default.respond_to?(:baz) 62 | end 63 | 64 | module BizzleExtension 65 | def bizzle 66 | bizzle_option 67 | end 68 | 69 | def self.registered(base) 70 | fail "base should be BizzleApp" unless base == BizzleApp 71 | fail "base should have already extended BizzleExtension" unless base.respond_to?(:bizzle) 72 | base.set :bizzle_option, 'bizzle!' 73 | end 74 | end 75 | 76 | class BizzleApp < Sinatra::Base 77 | end 78 | 79 | it 'sends .registered to the extension module after extending the class' do 80 | BizzleApp.register BizzleExtension 81 | assert_equal 'bizzle!', BizzleApp.bizzle_option 82 | assert_equal 'bizzle!', BizzleApp.bizzle 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /compat/erb_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Erb" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | context "without layouts" do 10 | 11 | setup do 12 | Sinatra.application = nil 13 | end 14 | 15 | specify "should render" do 16 | 17 | get '/no_layout' do 18 | erb '<%= 1 + 1 %>' 19 | end 20 | 21 | get_it '/no_layout' 22 | should.be.ok 23 | body.should == '2' 24 | 25 | end 26 | 27 | specify "should take an options hash with :locals set with a string" do 28 | get '/locals' do 29 | erb '<%= foo %>', :locals => {:foo => "Bar"} 30 | end 31 | 32 | get_it '/locals' 33 | should.be.ok 34 | body.should == 'Bar' 35 | end 36 | 37 | specify "should take an options hash with :locals set with a complex object" do 38 | get '/locals-complex' do 39 | erb '<%= foo[0] %>', :locals => {:foo => ["foo", "bar", "baz"]} 40 | end 41 | 42 | get_it '/locals-complex' 43 | should.be.ok 44 | body.should == 'foo' 45 | end 46 | end 47 | 48 | context "with layouts" do 49 | 50 | setup do 51 | Sinatra.application = nil 52 | end 53 | 54 | specify "can be inline" do 55 | 56 | layout do 57 | %Q{This is <%= yield %>!} 58 | end 59 | 60 | get '/lay' do 61 | erb 'Blake' 62 | end 63 | 64 | get_it '/lay' 65 | should.be.ok 66 | body.should.equal 'This is Blake!' 67 | 68 | end 69 | 70 | specify "can use named layouts" do 71 | 72 | layout :pretty do 73 | %Q{

<%= yield %>

} 74 | end 75 | 76 | get '/pretty' do 77 | erb 'Foo', :layout => :pretty 78 | end 79 | 80 | get '/not_pretty' do 81 | erb 'Bar' 82 | end 83 | 84 | get_it '/pretty' 85 | body.should.equal '

Foo

' 86 | 87 | get_it '/not_pretty' 88 | body.should.equal 'Bar' 89 | 90 | end 91 | 92 | specify "can be read from a file if they're not inlined" do 93 | 94 | get '/foo' do 95 | @title = 'Welcome to the Hello Program' 96 | erb 'Blake', :layout => :foo_layout, 97 | :views_directory => File.dirname(__FILE__) + "/views" 98 | end 99 | 100 | get_it '/foo' 101 | body.should.equal "Welcome to the Hello Program\nHi Blake\n" 102 | 103 | end 104 | 105 | end 106 | 107 | context "Templates (in general)" do 108 | 109 | specify "are read from files if Symbols" do 110 | 111 | get '/from_file' do 112 | @name = 'Alena' 113 | erb :foo, :views_directory => File.dirname(__FILE__) + "/views" 114 | end 115 | 116 | get_it '/from_file' 117 | 118 | body.should.equal 'You rock Alena!' 119 | 120 | end 121 | 122 | specify "use layout.ext by default if available" do 123 | 124 | get '/layout_from_file' do 125 | erb :foo, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 126 | end 127 | 128 | get_it '/layout_from_file' 129 | should.be.ok 130 | body.should.equal "x This is foo! x \n" 131 | 132 | end 133 | 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /sinatra.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.specification_version = 2 if s.respond_to? :specification_version= 3 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 4 | 5 | s.name = 'sinatra' 6 | s.version = '0.9.1.1' 7 | s.date = '2009-03-09' 8 | 9 | s.description = "Classy web-development dressed in a DSL" 10 | s.summary = "Classy web-development dressed in a DSL" 11 | 12 | s.authors = ["Blake Mizerany"] 13 | s.email = "sinatrarb@googlegroups.com" 14 | 15 | # = MANIFEST = 16 | s.files = %w[ 17 | AUTHORS 18 | CHANGES 19 | LICENSE 20 | README.rdoc 21 | Rakefile 22 | compat/app_test.rb 23 | compat/application_test.rb 24 | compat/builder_test.rb 25 | compat/compat_test.rb 26 | compat/custom_error_test.rb 27 | compat/erb_test.rb 28 | compat/events_test.rb 29 | compat/filter_test.rb 30 | compat/haml_test.rb 31 | compat/helper.rb 32 | compat/mapped_error_test.rb 33 | compat/pipeline_test.rb 34 | compat/public/foo.xml 35 | compat/sass_test.rb 36 | compat/sessions_test.rb 37 | compat/streaming_test.rb 38 | compat/sym_params_test.rb 39 | compat/template_test.rb 40 | compat/use_in_file_templates_test.rb 41 | compat/views/foo.builder 42 | compat/views/foo.erb 43 | compat/views/foo.haml 44 | compat/views/foo.sass 45 | compat/views/foo_layout.erb 46 | compat/views/foo_layout.haml 47 | compat/views/layout_test/foo.builder 48 | compat/views/layout_test/foo.erb 49 | compat/views/layout_test/foo.haml 50 | compat/views/layout_test/foo.sass 51 | compat/views/layout_test/layout.builder 52 | compat/views/layout_test/layout.erb 53 | compat/views/layout_test/layout.haml 54 | compat/views/layout_test/layout.sass 55 | compat/views/no_layout/no_layout.builder 56 | compat/views/no_layout/no_layout.haml 57 | lib/sinatra.rb 58 | lib/sinatra/base.rb 59 | lib/sinatra/compat.rb 60 | lib/sinatra/images/404.png 61 | lib/sinatra/images/500.png 62 | lib/sinatra/main.rb 63 | lib/sinatra/test.rb 64 | lib/sinatra/test/bacon.rb 65 | lib/sinatra/test/rspec.rb 66 | lib/sinatra/test/spec.rb 67 | lib/sinatra/test/unit.rb 68 | sinatra.gemspec 69 | test/base_test.rb 70 | test/builder_test.rb 71 | test/data/reload_app_file.rb 72 | test/erb_test.rb 73 | test/extensions_test.rb 74 | test/filter_test.rb 75 | test/haml_test.rb 76 | test/helper.rb 77 | test/helpers_test.rb 78 | test/mapped_error_test.rb 79 | test/middleware_test.rb 80 | test/options_test.rb 81 | test/reload_test.rb 82 | test/request_test.rb 83 | test/response_test.rb 84 | test/result_test.rb 85 | test/routing_test.rb 86 | test/sass_test.rb 87 | test/server_test.rb 88 | test/sinatra_test.rb 89 | test/static_test.rb 90 | test/templates_test.rb 91 | test/test_test.rb 92 | test/views/hello.builder 93 | test/views/hello.erb 94 | test/views/hello.haml 95 | test/views/hello.sass 96 | test/views/hello.test 97 | test/views/layout2.builder 98 | test/views/layout2.erb 99 | test/views/layout2.haml 100 | test/views/layout2.test 101 | ] 102 | # = MANIFEST = 103 | 104 | s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/} 105 | 106 | s.extra_rdoc_files = %w[README.rdoc LICENSE] 107 | s.add_dependency 'rack', '>= 0.9.1', '< 1.0' 108 | 109 | s.has_rdoc = true 110 | s.homepage = "http://sinatra.rubyforge.org" 111 | s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Sinatra", "--main", "README.rdoc"] 112 | s.require_paths = %w[lib] 113 | s.rubyforge_project = 'sinatra' 114 | s.rubygems_version = '1.1.1' 115 | end 116 | -------------------------------------------------------------------------------- /test/base_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Sinatra::Base subclasses' do 4 | 5 | class TestApp < Sinatra::Base 6 | get '/' do 7 | 'Hello World' 8 | end 9 | end 10 | 11 | it 'include Rack::Utils' do 12 | assert TestApp.included_modules.include?(Rack::Utils) 13 | end 14 | 15 | it 'processes requests with #call' do 16 | assert TestApp.respond_to?(:call) 17 | 18 | request = Rack::MockRequest.new(TestApp) 19 | response = request.get('/') 20 | assert response.ok? 21 | assert_equal 'Hello World', response.body 22 | end 23 | 24 | class TestApp < Sinatra::Base 25 | get '/state' do 26 | body = "Foo: #{@foo}" 27 | @foo = 'discard' 28 | body 29 | end 30 | end 31 | 32 | it 'does not maintain state between requests' do 33 | request = Rack::MockRequest.new(TestApp) 34 | 2.times do 35 | response = request.get('/state') 36 | assert response.ok? 37 | assert_equal 'Foo: ', response.body 38 | end 39 | end 40 | end 41 | 42 | describe "Sinatra::Base as Rack middleware" do 43 | 44 | app = lambda { |env| 45 | [210, {'X-Downstream' => 'true'}, ['Hello from downstream']] } 46 | 47 | class TestMiddleware < Sinatra::Base 48 | end 49 | 50 | it 'creates a middleware that responds to #call with .new' do 51 | middleware = TestMiddleware.new(app) 52 | assert middleware.respond_to?(:call) 53 | end 54 | 55 | it 'exposes the downstream app' do 56 | middleware = TestMiddleware.new(app) 57 | assert_same app, middleware.app 58 | end 59 | 60 | class TestMiddleware < Sinatra::Base 61 | get '/' do 62 | 'Hello from middleware' 63 | end 64 | end 65 | 66 | middleware = TestMiddleware.new(app) 67 | request = Rack::MockRequest.new(middleware) 68 | 69 | it 'intercepts requests' do 70 | response = request.get('/') 71 | assert response.ok? 72 | assert_equal 'Hello from middleware', response.body 73 | end 74 | 75 | it 'automatically forwards requests downstream when no matching route found' do 76 | response = request.get('/missing') 77 | assert_equal 210, response.status 78 | assert_equal 'Hello from downstream', response.body 79 | end 80 | 81 | class TestMiddleware < Sinatra::Base 82 | get '/low-level-forward' do 83 | app.call(env) 84 | end 85 | end 86 | 87 | it 'can call the downstream app directly and return result' do 88 | response = request.get('/low-level-forward') 89 | assert_equal 210, response.status 90 | assert_equal 'true', response['X-Downstream'] 91 | assert_equal 'Hello from downstream', response.body 92 | end 93 | 94 | class TestMiddleware < Sinatra::Base 95 | get '/explicit-forward' do 96 | response['X-Middleware'] = 'true' 97 | res = forward 98 | assert_nil res 99 | assert_equal 210, response.status 100 | assert_equal 'true', response['X-Downstream'] 101 | assert_equal ['Hello from downstream'], response.body 102 | 'Hello after explicit forward' 103 | end 104 | end 105 | 106 | it 'forwards the request downstream and integrates the response into the current context' do 107 | response = request.get('/explicit-forward') 108 | assert_equal 210, response.status 109 | assert_equal 'true', response['X-Downstream'] 110 | assert_equal 'Hello after explicit forward', response.body 111 | assert_equal '28', response['Content-Length'] 112 | end 113 | 114 | app_content_length = lambda {|env| 115 | [200, {'Content-Length' => '16'}, 'From downstream!']} 116 | class TestMiddlewareContentLength < Sinatra::Base 117 | get '/forward' do 118 | res = forward 119 | 'From after explicit forward!' 120 | end 121 | end 122 | 123 | middleware_content_length = TestMiddlewareContentLength.new(app_content_length) 124 | request_content_length = Rack::MockRequest.new(middleware_content_length) 125 | 126 | it "sets content length for last response" do 127 | response = request_content_length.get('/forward') 128 | assert_equal '28', response['Content-Length'] 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/sinatra/test.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | module Sinatra 4 | module Test 5 | include Rack::Utils 6 | 7 | def self.included(base) 8 | Sinatra::Default.set(:environment, :test) 9 | end 10 | 11 | attr_reader :app, :request, :response 12 | 13 | def self.deprecate(framework) 14 | warn <<-EOF 15 | Warning: support for the #{framework} testing framework is deprecated and 16 | will be dropped in Sinatra 1.0. See 17 | for more information. 18 | EOF 19 | end 20 | 21 | def make_request(verb, path, body=nil, options={}) 22 | @app = Sinatra::Application if @app.nil? && defined?(Sinatra::Application) 23 | fail "@app not set - cannot make request" if @app.nil? 24 | 25 | @request = Rack::MockRequest.new(@app) 26 | options = { :lint => true }.merge(options || {}) 27 | 28 | case 29 | when body.respond_to?(:to_hash) 30 | options.merge! body.delete(:env) if body.key?(:env) 31 | options[:input] = param_string(body) 32 | when body.respond_to?(:to_str) 33 | options[:input] = body 34 | when body.nil? 35 | options[:input] = '' 36 | else 37 | raise ArgumentError, "body must be a Hash, String, or nil" 38 | end 39 | 40 | yield @request if block_given? 41 | @response = @request.request(verb, path, rack_options(options)) 42 | end 43 | 44 | def get(path, *args, &b) ; make_request('GET', path, *args, &b) ; end 45 | def head(path, *args, &b) ; make_request('HEAD', path, *args, &b) ; end 46 | def post(path, *args, &b) ; make_request('POST', path, *args, &b) ; end 47 | def put(path, *args, &b) ; make_request('PUT', path, *args, &b) ; end 48 | def delete(path, *args, &b) ; make_request('DELETE', path, *args, &b) ; end 49 | 50 | def follow! 51 | make_request 'GET', @response.location 52 | end 53 | 54 | def body ; @response.body ; end 55 | def status ; @response.status ; end 56 | 57 | # Delegate other missing methods to @response. 58 | def method_missing(name, *args, &block) 59 | if @response && @response.respond_to?(name) 60 | @response.send(name, *args, &block) 61 | else 62 | super 63 | end 64 | end 65 | 66 | # Also check @response since we delegate there. 67 | def respond_to?(symbol, include_private=false) 68 | super || (@response && @response.respond_to?(symbol, include_private)) 69 | end 70 | 71 | private 72 | 73 | RACK_OPTIONS = { 74 | :accept => 'HTTP_ACCEPT', 75 | :agent => 'HTTP_USER_AGENT', 76 | :host => 'HTTP_HOST', 77 | :session => 'rack.session', 78 | :cookies => 'HTTP_COOKIE', 79 | :content_type => 'CONTENT_TYPE' 80 | } 81 | 82 | def rack_options(opts) 83 | opts.merge(:lint => true).inject({}) do |hash,(key,val)| 84 | key = RACK_OPTIONS[key] || key 85 | hash[key] = val 86 | hash 87 | end 88 | end 89 | 90 | def param_string(value, prefix = nil) 91 | case value 92 | when Array 93 | value.map { |v| 94 | param_string(v, "#{prefix}[]") 95 | } * "&" 96 | when Hash 97 | value.map { |k, v| 98 | param_string(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) 99 | } * "&" 100 | else 101 | "#{prefix}=#{escape(value)}" 102 | end 103 | end 104 | 105 | if defined? Sinatra::Compat 106 | # Deprecated. Use: "get" instead of "get_it". 107 | %w(get head post put delete).each do |verb| 108 | eval <<-RUBY, binding, __FILE__, __LINE__ 109 | def #{verb}_it(*args, &block) 110 | sinatra_warn "The #{verb}_it method is deprecated; use #{verb} instead." 111 | make_request('#{verb.upcase}', *args, &block) 112 | end 113 | RUBY 114 | end 115 | end 116 | end 117 | 118 | class TestHarness 119 | include Test 120 | 121 | def initialize(app=nil) 122 | @app = app || Sinatra::Application 123 | @app.set(:environment, :test) 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/test_test.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require File.dirname(__FILE__) + '/helper' 3 | 4 | describe 'Sinatra::Test' do 5 | def request 6 | YAML.load(body) 7 | end 8 | 9 | def request_body 10 | request['test.body'] 11 | end 12 | 13 | def request_params 14 | YAML.load(request['test.params']) 15 | end 16 | 17 | before do 18 | mock_app { 19 | %w[get head post put delete].each { |verb| 20 | send(verb, '/') do 21 | redirect '/redirected' if params[:redirect] 22 | env.update('test.body' => request.body.read) 23 | env.update('test.params' => params.to_yaml) 24 | env.to_yaml 25 | end 26 | } 27 | 28 | get '/redirected' do 29 | "you've been redirected" 30 | end 31 | } 32 | end 33 | 34 | it 'allows GET/HEAD/POST/PUT/DELETE' do 35 | get '/' 36 | assert_equal('GET', request['REQUEST_METHOD']) 37 | 38 | post '/' 39 | assert_equal('POST', request['REQUEST_METHOD']) 40 | 41 | put '/' 42 | assert_equal('PUT', request['REQUEST_METHOD']) 43 | 44 | delete '/' 45 | assert_equal('DELETE', request['REQUEST_METHOD']) 46 | 47 | head '/' 48 | assert_equal('596', response.headers['Content-Length']) 49 | assert_equal('', response.body) 50 | end 51 | 52 | it 'allows to specify a body' do 53 | post '/', '42' 54 | assert_equal '42', request_body 55 | end 56 | 57 | it 'allows to specify params' do 58 | get '/', :foo => 'bar' 59 | assert_equal 'bar', request_params['foo'] 60 | end 61 | 62 | it 'supports nested params' do 63 | get '/', :foo => { :x => 'y', :chunky => 'bacon' } 64 | assert_equal "y", request_params['foo']['x'] 65 | assert_equal "bacon", request_params['foo']['chunky'] 66 | end 67 | 68 | it 'provides easy access to response status and body' do 69 | get '/' 70 | assert_equal 200, status 71 | assert body =~ /^---/ 72 | end 73 | 74 | it 'delegates methods to @response' do 75 | get '/' 76 | assert ok? 77 | end 78 | 79 | it 'follows redirect' do 80 | get '/', :redirect => true 81 | follow! 82 | assert_equal "you've been redirected", body 83 | end 84 | 85 | it 'provides sugar for common HTTP headers' do 86 | get '/', :env => { :accept => 'text/plain' } 87 | assert_equal 'text/plain', request['HTTP_ACCEPT'] 88 | 89 | get '/', :env => { :agent => 'TATFT' } 90 | assert_equal 'TATFT', request['HTTP_USER_AGENT'] 91 | 92 | get '/', :env => { :host => '1.2.3.4' } 93 | assert_equal '1.2.3.4', request['HTTP_HOST'] 94 | 95 | get '/', :env => { :session => 'foo' } 96 | assert_equal 'foo', request['rack.session'] 97 | 98 | get '/', :env => { :cookies => 'foo' } 99 | assert_equal 'foo', request['HTTP_COOKIE'] 100 | 101 | get '/', :env => { :content_type => 'text/plain' } 102 | assert_equal 'text/plain', request['CONTENT_TYPE'] 103 | end 104 | 105 | it 'allow to test session easily' do 106 | app = mock_app(Sinatra::Default) { 107 | get '/' do 108 | session['foo'] = 'bar' 109 | 200 110 | end 111 | 112 | post '/' do 113 | assert_equal 'bar', session['foo'] 114 | session['foo'] || "blah" 115 | end 116 | } 117 | 118 | browser = Sinatra::TestHarness.new(app) 119 | browser.get '/' 120 | browser.post '/', {}, :session => { 'foo' => 'bar' } 121 | assert_equal 'bar', browser.response.body 122 | end 123 | 124 | it 'yields the request object to the block before invoking the application' do 125 | called = false 126 | get '/' do |req| 127 | called = true 128 | assert req.kind_of?(Rack::MockRequest) 129 | end 130 | assert called 131 | end 132 | 133 | it 'sets the environment to :test on include' do 134 | Sinatra::Default.set(:environment, :production) 135 | Class.new { include Sinatra::Test } 136 | assert_equal :test, Sinatra::Default.environment 137 | end 138 | 139 | def test_TestHarness 140 | session = Sinatra::TestHarness.new(@app) 141 | response = session.get('/') 142 | assert_equal 200, response.status 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /compat/streaming_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Static files (by default)" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | Sinatra.application.options.public = File.dirname(__FILE__) + '/public' 8 | end 9 | 10 | specify "are served from root/public" do 11 | get_it '/foo.xml' 12 | should.be.ok 13 | headers['Content-Length'].should.equal '12' 14 | headers['Content-Type'].should.equal 'application/xml' 15 | body.should.equal "\n" 16 | end 17 | 18 | specify "are not served when verb is not GET or HEAD" do 19 | post_it '/foo.xml' 20 | # these should actually be giving back a 405 Method Not Allowed but that 21 | # complicates the routing logic quite a bit. 22 | should.be.not_found 23 | status.should.equal 404 24 | end 25 | 26 | specify "are served when verb is HEAD but missing a body" do 27 | head_it '/foo.xml' 28 | should.be.ok 29 | headers['Content-Length'].should.equal '12' 30 | headers['Content-Type'].should.equal 'application/xml' 31 | body.should.equal "" 32 | end 33 | 34 | # static files override dynamic/internal events and ... 35 | specify "are served when conflicting events exists" do 36 | get '/foo.xml' do 37 | 'this is not foo.xml!' 38 | end 39 | get_it '/foo.xml' 40 | should.be.ok 41 | body.should.equal "\n" 42 | end 43 | 44 | specify "are irrelevant when request_method is not GET/HEAD" do 45 | put '/foo.xml' do 46 | 'putted!' 47 | end 48 | put_it '/foo.xml' 49 | should.be.ok 50 | body.should.equal 'putted!' 51 | 52 | get_it '/foo.xml' 53 | should.be.ok 54 | body.should.equal "\n" 55 | end 56 | 57 | specify "include a Last-Modified header" do 58 | last_modified = File.mtime(Sinatra.application.options.public + '/foo.xml') 59 | get_it('/foo.xml') 60 | should.be.ok 61 | body.should.not.be.empty 62 | headers['Last-Modified'].should.equal last_modified.httpdate 63 | end 64 | 65 | # Deprecated. Use: ConditionalGet middleware. 66 | specify "are not served when If-Modified-Since matches" do 67 | last_modified = File.mtime(Sinatra.application.options.public + '/foo.xml') 68 | @request = Rack::MockRequest.new(Sinatra.application) 69 | @response = @request.get('/foo.xml', 'HTTP_IF_MODIFIED_SINCE' => last_modified.httpdate) 70 | status.should.equal 304 71 | body.should.be.empty 72 | end 73 | 74 | specify "should omit Content-Disposition headers" do 75 | get_it('/foo.xml') 76 | should.be.ok 77 | headers['Content-Disposition'].should.be.nil 78 | headers['Content-Transfer-Encoding'].should.be.nil 79 | end 80 | 81 | specify "should be served even if their path is url escaped" do 82 | get_it('/fo%6f.xml') 83 | should.be.ok 84 | body.should.equal "\n" 85 | end 86 | 87 | end 88 | 89 | context "SendData" do 90 | 91 | setup do 92 | Sinatra.application = nil 93 | end 94 | 95 | # Deprecated. send_data is going away. 96 | specify "should send the data with options" do 97 | get '/' do 98 | send_data 'asdf', :status => 500 99 | end 100 | 101 | get_it '/' 102 | 103 | should.be.server_error 104 | body.should.equal 'asdf' 105 | end 106 | 107 | # Deprecated. The Content-Disposition is no longer handled by sendfile. 108 | specify "should include a Content-Disposition header" do 109 | get '/' do 110 | send_file File.dirname(__FILE__) + '/public/foo.xml', 111 | :disposition => 'attachment' 112 | end 113 | 114 | get_it '/' 115 | 116 | should.be.ok 117 | headers['Content-Disposition'].should.not.be.nil 118 | headers['Content-Disposition'].should.equal 'attachment; filename="foo.xml"' 119 | end 120 | 121 | specify "should include a Content-Disposition header when :disposition set to attachment" do 122 | get '/' do 123 | send_file File.dirname(__FILE__) + '/public/foo.xml', 124 | :disposition => 'attachment' 125 | end 126 | 127 | get_it '/' 128 | 129 | should.be.ok 130 | headers['Content-Disposition'].should.not.be.nil 131 | headers['Content-Disposition'].should.equal 'attachment; filename="foo.xml"' 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | require 'rake/testtask' 3 | require 'fileutils' 4 | 5 | task :default => [:test] 6 | task :spec => :test 7 | 8 | # SPECS =============================================================== 9 | 10 | Rake::TestTask.new(:test) do |t| 11 | t.test_files = FileList['test/*_test.rb'] 12 | t.ruby_opts = ['-rubygems'] if defined? Gem 13 | end 14 | 15 | desc 'Run compatibility specs (requires test/spec)' 16 | task :compat do |t| 17 | pattern = ENV['TEST'] || '.*' 18 | sh "specrb --testcase '#{pattern}' -Ilib:test compat/*_test.rb" 19 | end 20 | 21 | # PACKAGING ============================================================ 22 | 23 | # Load the gemspec using the same limitations as github 24 | def spec 25 | @spec ||= 26 | begin 27 | require 'rubygems/specification' 28 | data = File.read('sinatra.gemspec') 29 | spec = nil 30 | Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join 31 | spec 32 | end 33 | end 34 | 35 | def package(ext='') 36 | "dist/sinatra-#{spec.version}" + ext 37 | end 38 | 39 | desc 'Build packages' 40 | task :package => %w[.gem .tar.gz].map {|e| package(e)} 41 | 42 | desc 'Build and install as local gem' 43 | task :install => package('.gem') do 44 | sh "gem install #{package('.gem')}" 45 | end 46 | 47 | directory 'dist/' 48 | CLOBBER.include('dist') 49 | 50 | file package('.gem') => %w[dist/ sinatra.gemspec] + spec.files do |f| 51 | sh "gem build sinatra.gemspec" 52 | mv File.basename(f.name), f.name 53 | end 54 | 55 | file package('.tar.gz') => %w[dist/] + spec.files do |f| 56 | sh <<-SH 57 | git archive \ 58 | --prefix=sinatra-#{source_version}/ \ 59 | --format=tar \ 60 | HEAD | gzip > #{f.name} 61 | SH 62 | end 63 | 64 | # Rubyforge Release / Publish Tasks ================================== 65 | 66 | desc 'Publish gem and tarball to rubyforge' 67 | task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t| 68 | sh <<-end 69 | rubyforge add_release sinatra sinatra #{spec.version} #{package('.gem')} && 70 | rubyforge add_file sinatra sinatra #{spec.version} #{package('.tar.gz')} 71 | end 72 | end 73 | 74 | # Website ============================================================ 75 | # Building docs requires HAML and the hanna gem: 76 | # gem install mislav-hanna --source=http://gems.github.com 77 | 78 | task 'doc' => ['doc:api'] 79 | 80 | desc 'Generate Hanna RDoc under doc/api' 81 | task 'doc:api' => ['doc/api/index.html'] 82 | 83 | file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f| 84 | rb_files = f.prerequisites 85 | sh((<<-end).gsub(/\s+/, ' ')) 86 | hanna --charset utf8 \ 87 | --fmt html \ 88 | --inline-source \ 89 | --line-numbers \ 90 | --main README.rdoc \ 91 | --op doc/api \ 92 | --title 'Sinatra API Documentation' \ 93 | #{rb_files.join(' ')} 94 | end 95 | end 96 | CLEAN.include 'doc/api' 97 | 98 | def rdoc_to_html(file_name) 99 | require 'rdoc/markup/to_html' 100 | rdoc = RDoc::Markup::ToHtml.new 101 | rdoc.convert(File.read(file_name)) 102 | end 103 | 104 | # Gemspec Helpers ==================================================== 105 | 106 | def source_version 107 | line = File.read('lib/sinatra/base.rb')[/^\s*VERSION = .*/] 108 | line.match(/.*VERSION = '(.*)'/)[1] 109 | end 110 | 111 | project_files = 112 | FileList[ 113 | '{lib,test,compat,images}/**', 114 | 'Rakefile', 'CHANGES', 'README.rdoc' 115 | ] 116 | file 'sinatra.gemspec' => project_files do |f| 117 | # read spec file and split out manifest section 118 | spec = File.read(f.name) 119 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 120 | # replace version and date 121 | head.sub!(/\.version = '.*'/, ".version = '#{source_version}'") 122 | head.sub!(/\.date = '.*'/, ".date = '#{Date.today.to_s}'") 123 | # determine file list from git ls-files 124 | files = `git ls-files`. 125 | split("\n"). 126 | sort. 127 | reject{ |file| file =~ /^\./ }. 128 | reject { |file| file =~ /^doc/ }. 129 | map{ |file| " #{file}" }. 130 | join("\n") 131 | # piece file back together and write... 132 | manifest = " s.files = %w[\n#{files}\n ]\n" 133 | spec = [head,manifest,tail].join(" # = MANIFEST =\n") 134 | File.open(f.name, 'w') { |io| io.write(spec) } 135 | puts "updated #{f.name}" 136 | end 137 | -------------------------------------------------------------------------------- /test/mapped_error_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | class FooError < RuntimeError 4 | end 5 | 6 | class FooNotFound < Sinatra::NotFound 7 | end 8 | 9 | describe 'Exception Mappings' do 10 | it 'invokes handlers registered with ::error when raised' do 11 | mock_app { 12 | set :raise_errors, false 13 | error(FooError) { 'Foo!' } 14 | get '/' do 15 | raise FooError 16 | end 17 | } 18 | get '/' 19 | assert_equal 500, status 20 | assert_equal 'Foo!', body 21 | end 22 | 23 | it 'uses the Exception handler if no matching handler found' do 24 | mock_app { 25 | set :raise_errors, false 26 | error(Exception) { 'Exception!' } 27 | get '/' do 28 | raise FooError 29 | end 30 | } 31 | get '/' 32 | assert_equal 500, status 33 | assert_equal 'Exception!', body 34 | end 35 | 36 | it "sets env['sinatra.error'] to the rescued exception" do 37 | mock_app { 38 | set :raise_errors, false 39 | error(FooError) { 40 | assert env.include?('sinatra.error') 41 | assert env['sinatra.error'].kind_of?(FooError) 42 | 'looks good' 43 | } 44 | get '/' do 45 | raise FooError 46 | end 47 | } 48 | get '/' 49 | assert_equal 'looks good', body 50 | end 51 | 52 | it 'dumps errors to rack.errors when dump_errors is enabled' do 53 | mock_app { 54 | set :raise_errors, false 55 | set :dump_errors, true 56 | get('/') { raise FooError, 'BOOM!' } 57 | } 58 | 59 | get '/' 60 | assert_equal 500, status 61 | assert @response.errors =~ /FooError - BOOM!:/ 62 | end 63 | 64 | it "raises without calling the handler when the raise_errors options is set" do 65 | mock_app { 66 | set :raise_errors, true 67 | error(FooError) { "she's not there." } 68 | get '/' do 69 | raise FooError 70 | end 71 | } 72 | assert_raise(FooError) { get '/' } 73 | end 74 | 75 | it "never raises Sinatra::NotFound beyond the application" do 76 | mock_app { 77 | set :raise_errors, true 78 | get '/' do 79 | raise Sinatra::NotFound 80 | end 81 | } 82 | assert_nothing_raised { get '/' } 83 | assert_equal 404, status 84 | end 85 | 86 | it "cascades for subclasses of Sinatra::NotFound" do 87 | mock_app { 88 | set :raise_errors, true 89 | error(FooNotFound) { "foo! not found." } 90 | get '/' do 91 | raise FooNotFound 92 | end 93 | } 94 | assert_nothing_raised { get '/' } 95 | assert_equal 404, status 96 | assert_equal 'foo! not found.', body 97 | end 98 | 99 | it 'has a not_found method for backwards compatibility' do 100 | mock_app { 101 | not_found do 102 | "Lost, are we?" 103 | end 104 | } 105 | 106 | get '/test' 107 | assert_equal 404, status 108 | assert_equal "Lost, are we?", body 109 | end 110 | end 111 | 112 | describe 'Custom Error Pages' do 113 | it 'allows numeric status code mappings to be registered with ::error' do 114 | mock_app { 115 | set :raise_errors, false 116 | error(500) { 'Foo!' } 117 | get '/' do 118 | [500, {}, 'Internal Foo Error'] 119 | end 120 | } 121 | get '/' 122 | assert_equal 500, status 123 | assert_equal 'Foo!', body 124 | end 125 | 126 | it 'allows ranges of status code mappings to be registered with :error' do 127 | mock_app { 128 | set :raise_errors, false 129 | error(500..550) { "Error: #{response.status}" } 130 | get '/' do 131 | [507, {}, 'A very special error'] 132 | end 133 | } 134 | get '/' 135 | assert_equal 507, status 136 | assert_equal 'Error: 507', body 137 | end 138 | 139 | class FooError < RuntimeError 140 | end 141 | 142 | it 'runs after exception mappings and overwrites body' do 143 | mock_app { 144 | set :raise_errors, false 145 | error FooError do 146 | response.status = 502 147 | 'from exception mapping' 148 | end 149 | error(500) { 'from 500 handler' } 150 | error(502) { 'from custom error page' } 151 | 152 | get '/' do 153 | raise FooError 154 | end 155 | } 156 | get '/' 157 | assert_equal 502, status 158 | assert_equal 'from custom error page', body 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /compat/haml_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Haml" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | context "without layouts" do 10 | 11 | setup do 12 | Sinatra.application = nil 13 | end 14 | 15 | specify "should render" do 16 | 17 | get '/no_layout' do 18 | haml '== #{1+1}' 19 | end 20 | 21 | get_it '/no_layout' 22 | should.be.ok 23 | body.should == "2\n" 24 | 25 | end 26 | end 27 | 28 | context "with layouts" do 29 | 30 | setup do 31 | Sinatra.application = nil 32 | end 33 | 34 | specify "can be inline" do 35 | 36 | layout do 37 | '== This is #{yield}!' 38 | end 39 | 40 | get '/lay' do 41 | haml 'Blake' 42 | end 43 | 44 | get_it '/lay' 45 | should.be.ok 46 | body.should.equal "This is Blake\n!\n" 47 | 48 | end 49 | 50 | specify "can use named layouts" do 51 | 52 | layout :pretty do 53 | '%h1== #{yield}' 54 | end 55 | 56 | get '/pretty' do 57 | haml 'Foo', :layout => :pretty 58 | end 59 | 60 | get '/not_pretty' do 61 | haml 'Bar' 62 | end 63 | 64 | get_it '/pretty' 65 | body.should.equal "

Foo

\n" 66 | 67 | get_it '/not_pretty' 68 | body.should.equal "Bar\n" 69 | 70 | end 71 | 72 | specify "can be read from a file if they're not inlined" do 73 | 74 | get '/foo' do 75 | @title = 'Welcome to the Hello Program' 76 | haml 'Blake', :layout => :foo_layout, 77 | :views_directory => File.dirname(__FILE__) + "/views" 78 | end 79 | 80 | get_it '/foo' 81 | body.should.equal "Welcome to the Hello Program\nHi Blake\n" 82 | 83 | end 84 | 85 | specify "can be read from file and layout from text" do 86 | get '/foo' do 87 | haml 'Test', :layout => '== Foo #{yield}' 88 | end 89 | 90 | get_it '/foo' 91 | 92 | body.should.equal "Foo Test\n" 93 | end 94 | 95 | end 96 | 97 | context "Templates (in general)" do 98 | 99 | setup do 100 | Sinatra.application = nil 101 | end 102 | 103 | specify "are read from files if Symbols" do 104 | 105 | get '/from_file' do 106 | @name = 'Alena' 107 | haml :foo, :views_directory => File.dirname(__FILE__) + "/views" 108 | end 109 | 110 | get_it '/from_file' 111 | 112 | body.should.equal "You rock Alena!\n" 113 | 114 | end 115 | 116 | specify "use layout.ext by default if available" do 117 | 118 | get '/' do 119 | haml :foo, :views_directory => File.dirname(__FILE__) + "/views/layout_test" 120 | end 121 | 122 | get_it '/' 123 | should.be.ok 124 | body.should.equal "x This is foo!\n x\n" 125 | 126 | end 127 | 128 | specify "renders without layout" do 129 | 130 | get '/' do 131 | haml :no_layout, :views_directory => File.dirname(__FILE__) + "/views/no_layout" 132 | end 133 | 134 | get_it '/' 135 | should.be.ok 136 | body.should.equal "

No Layout!

\n" 137 | 138 | end 139 | 140 | specify "can render with no layout" do 141 | layout do 142 | "X\n= yield\nX" 143 | end 144 | 145 | get '/' do 146 | haml 'blake', :layout => false 147 | end 148 | 149 | get_it '/' 150 | 151 | body.should.equal "blake\n" 152 | end 153 | 154 | specify "raises error if template not found" do 155 | get '/' do 156 | haml :not_found 157 | end 158 | 159 | lambda { get_it '/' }.should.raise(Errno::ENOENT) 160 | end 161 | 162 | specify "use layout.ext by default if available" do 163 | 164 | template :foo do 165 | 'asdf' 166 | end 167 | 168 | get '/' do 169 | haml :foo, :layout => false, 170 | :views_directory => File.dirname(__FILE__) + "/views/layout_test" 171 | end 172 | 173 | get_it '/' 174 | should.be.ok 175 | body.should.equal "asdf\n" 176 | 177 | end 178 | 179 | end 180 | 181 | describe 'Options passed to the HAML interpreter' do 182 | setup do 183 | Sinatra.application = nil 184 | end 185 | 186 | specify 'are empty be default' do 187 | 188 | get '/' do 189 | haml 'foo' 190 | end 191 | 192 | Haml::Engine.expects(:new).with('foo', {}).returns(stub(:render => 'foo')) 193 | 194 | get_it '/' 195 | should.be.ok 196 | 197 | end 198 | 199 | specify 'can be configured by passing :options to haml' do 200 | 201 | get '/' do 202 | haml 'foo', :options => {:format => :html4} 203 | end 204 | 205 | Haml::Engine.expects(:new).with('foo', {:format => :html4}).returns(stub(:render => 'foo')) 206 | 207 | get_it '/' 208 | should.be.ok 209 | 210 | end 211 | 212 | specify 'can be configured using set_option :haml' do 213 | 214 | configure do 215 | set_option :haml, :format => :html4, 216 | :escape_html => true 217 | end 218 | 219 | get '/' do 220 | haml 'foo' 221 | end 222 | 223 | Haml::Engine.expects(:new).with('foo', {:format => :html4, 224 | :escape_html => true}).returns(stub(:render => 'foo')) 225 | 226 | get_it '/' 227 | should.be.ok 228 | 229 | end 230 | 231 | end 232 | 233 | end 234 | -------------------------------------------------------------------------------- /compat/application_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | require 'uri' 4 | 5 | class TesterWithEach 6 | def each 7 | yield 'foo' 8 | yield 'bar' 9 | yield 'baz' 10 | end 11 | end 12 | 13 | context "An app returns" do 14 | 15 | setup do 16 | Sinatra.application = nil 17 | end 18 | 19 | specify "404 if no events found" do 20 | request = Rack::MockRequest.new(@app) 21 | get_it '/' 22 | should.be.not_found 23 | body.should.equal '

Not Found

' 24 | end 25 | 26 | specify "200 if success" do 27 | get '/' do 28 | 'Hello World' 29 | end 30 | get_it '/' 31 | should.be.ok 32 | body.should.equal 'Hello World' 33 | end 34 | 35 | specify "an objects result from each if it has it" do 36 | 37 | get '/' do 38 | TesterWithEach.new 39 | end 40 | 41 | get_it '/' 42 | should.be.ok 43 | body.should.equal 'foobarbaz' 44 | 45 | end 46 | 47 | specify "404 if NotFound is raised" do 48 | 49 | get '/' do 50 | raise Sinatra::NotFound 51 | end 52 | 53 | get_it '/' 54 | should.be.not_found 55 | 56 | end 57 | 58 | end 59 | 60 | context "Application#configure blocks" do 61 | 62 | setup do 63 | Sinatra.application = nil 64 | end 65 | 66 | specify "run when no environment specified" do 67 | ref = false 68 | configure { ref = true } 69 | ref.should.equal true 70 | end 71 | 72 | specify "run when matching environment specified" do 73 | ref = false 74 | configure(:test) { ref = true } 75 | ref.should.equal true 76 | end 77 | 78 | specify "do not run when no matching environment specified" do 79 | configure(:foo) { flunk "block should not have been executed" } 80 | configure(:development, :production, :foo) { flunk "block should not have been executed" } 81 | end 82 | 83 | specify "accept multiple environments" do 84 | ref = false 85 | configure(:foo, :test, :bar) { ref = true } 86 | ref.should.equal true 87 | end 88 | 89 | end 90 | 91 | context "Events in an app" do 92 | 93 | setup do 94 | Sinatra.application = nil 95 | end 96 | 97 | specify "evaluate in a clean context" do 98 | helpers do 99 | def foo 100 | 'foo' 101 | end 102 | end 103 | 104 | get '/foo' do 105 | foo 106 | end 107 | 108 | get_it '/foo' 109 | should.be.ok 110 | body.should.equal 'foo' 111 | end 112 | 113 | specify "get access to request, response, and params" do 114 | get '/:foo' do 115 | params["foo"] + params["bar"] 116 | end 117 | 118 | get_it '/foo?bar=baz' 119 | should.be.ok 120 | body.should.equal 'foobaz' 121 | end 122 | 123 | specify "can filters by agent" do 124 | 125 | get '/', :agent => /Windows/ do 126 | request.env['HTTP_USER_AGENT'] 127 | end 128 | 129 | get_it '/', :env => { :agent => 'Windows' } 130 | should.be.ok 131 | body.should.equal 'Windows' 132 | 133 | get_it '/', :env => { :agent => 'Mac' } 134 | should.not.be.ok 135 | 136 | end 137 | 138 | specify "can use regex to get parts of user-agent" do 139 | 140 | get '/', :agent => /Windows (NT)/ do 141 | params[:agent].first 142 | end 143 | 144 | get_it '/', :env => { :agent => 'Windows NT' } 145 | 146 | body.should.equal 'NT' 147 | 148 | end 149 | 150 | specify "can deal with spaces in paths" do 151 | 152 | path = '/path with spaces' 153 | 154 | get path do 155 | "Look ma, a path with spaces!" 156 | end 157 | 158 | get_it URI.encode(path) 159 | 160 | body.should.equal "Look ma, a path with spaces!" 161 | end 162 | 163 | specify "route based on host" do 164 | 165 | get '/' do 166 | 'asdf' 167 | end 168 | 169 | get_it '/' 170 | assert ok? 171 | assert_equal('asdf', body) 172 | 173 | get '/foo', :host => 'foo.sinatrarb.com' do 174 | 'in foo!' 175 | end 176 | 177 | get '/foo', :host => 'bar.sinatrarb.com' do 178 | 'in bar!' 179 | end 180 | 181 | get_it '/foo', {}, 'HTTP_HOST' => 'foo.sinatrarb.com' 182 | assert ok? 183 | assert_equal 'in foo!', body 184 | 185 | get_it '/foo', {}, 'HTTP_HOST' => 'bar.sinatrarb.com' 186 | assert ok? 187 | assert_equal 'in bar!', body 188 | 189 | get_it '/foo' 190 | assert not_found? 191 | 192 | end 193 | 194 | end 195 | 196 | 197 | context "Options in an app" do 198 | 199 | setup do 200 | Sinatra.application = nil 201 | @app = Sinatra::application 202 | end 203 | 204 | specify "can be set singly on app" do 205 | @app.set :foo, 1234 206 | @app.options.foo.should.equal 1234 207 | end 208 | 209 | specify "can be set singly from top-level" do 210 | set_option :foo, 1234 211 | @app.options.foo.should.equal 1234 212 | end 213 | 214 | specify "can be set multiply on app" do 215 | @app.options.foo.should.be.nil 216 | @app.set :foo => 1234, 217 | :bar => 'hello, world' 218 | @app.options.foo.should.equal 1234 219 | @app.options.bar.should.equal 'hello, world' 220 | end 221 | 222 | specify "can be set multiply from top-level" do 223 | @app.options.foo.should.be.nil 224 | set_options :foo => 1234, 225 | :bar => 'hello, world' 226 | @app.options.foo.should.equal 1234 227 | @app.options.bar.should.equal 'hello, world' 228 | end 229 | 230 | specify "can be enabled on app" do 231 | @app.options.foo.should.be.nil 232 | @app.enable :sessions, :foo, :bar 233 | @app.options.sessions.should.equal true 234 | @app.options.foo.should.equal true 235 | @app.options.bar.should.equal true 236 | end 237 | 238 | specify "can be enabled from top-level" do 239 | @app.options.foo.should.be.nil 240 | enable :sessions, :foo, :bar 241 | @app.options.sessions.should.equal true 242 | @app.options.foo.should.equal true 243 | @app.options.bar.should.equal true 244 | end 245 | 246 | specify "can be disabled on app" do 247 | @app.options.foo.should.be.nil 248 | @app.disable :sessions, :foo, :bar 249 | @app.options.sessions.should.equal false 250 | @app.options.foo.should.equal false 251 | @app.options.bar.should.equal false 252 | end 253 | 254 | specify "can be enabled from top-level" do 255 | @app.options.foo.should.be.nil 256 | disable :sessions, :foo, :bar 257 | @app.options.sessions.should.equal false 258 | @app.options.foo.should.equal false 259 | @app.options.bar.should.equal false 260 | end 261 | 262 | end 263 | -------------------------------------------------------------------------------- /compat/app_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Sinatra" do 4 | 5 | setup do 6 | Sinatra.application = nil 7 | end 8 | 9 | specify "should put all DSL methods on (main)" do 10 | object = Object.new 11 | methods = %w[get put post head delete configure template helpers set] 12 | methods.each do |method| 13 | object.private_methods.map { |m| m.to_sym }.should.include(method.to_sym) 14 | end 15 | end 16 | 17 | specify "should handle result of nil" do 18 | get '/' do 19 | nil 20 | end 21 | 22 | get_it '/' 23 | should.be.ok 24 | body.should == '' 25 | end 26 | 27 | specify "handles events" do 28 | get '/:name' do 29 | 'Hello ' + params["name"] 30 | end 31 | 32 | get_it '/Blake' 33 | 34 | should.be.ok 35 | body.should.equal 'Hello Blake' 36 | end 37 | 38 | 39 | specify "handles splats" do 40 | get '/hi/*' do 41 | params["splat"].kind_of?(Array).should.equal true 42 | params["splat"].first 43 | end 44 | 45 | get_it '/hi/Blake' 46 | 47 | should.be.ok 48 | body.should.equal 'Blake' 49 | end 50 | 51 | specify "handles multiple splats" do 52 | get '/say/*/to/*' do 53 | params["splat"].join(' ') 54 | end 55 | 56 | get_it '/say/hello/to/world' 57 | 58 | should.be.ok 59 | body.should.equal 'hello world' 60 | end 61 | 62 | specify "allow empty splats" do 63 | get '/say/*/to*/*' do 64 | params["splat"].join(' ') 65 | end 66 | 67 | get_it '/say/hello/to/world' 68 | 69 | should.be.ok 70 | body.should.equal 'hello world' # second splat is empty 71 | 72 | get_it '/say/hello/tomy/world' 73 | 74 | should.be.ok 75 | body.should.equal 'hello my world' 76 | end 77 | 78 | specify "gives access to underlying response header Hash" do 79 | get '/' do 80 | header['X-Test'] = 'Is this thing on?' 81 | headers 'X-Test2' => 'Foo', 'X-Test3' => 'Bar' 82 | '' 83 | end 84 | 85 | get_it '/' 86 | should.be.ok 87 | headers.should.include 'X-Test' 88 | headers['X-Test'].should.equal 'Is this thing on?' 89 | headers.should.include 'X-Test3' 90 | headers['X-Test3'].should.equal 'Bar' 91 | end 92 | 93 | specify "follows redirects" do 94 | get '/' do 95 | redirect '/blake' 96 | end 97 | 98 | get '/blake' do 99 | 'Mizerany' 100 | end 101 | 102 | get_it '/' 103 | should.be.redirection 104 | body.should.equal '' 105 | 106 | follow! 107 | should.be.ok 108 | body.should.equal 'Mizerany' 109 | end 110 | 111 | specify "renders a body with a redirect" do 112 | helpers do 113 | def foo ; 'blah' ; end 114 | end 115 | get "/" do 116 | redirect 'foo', :foo 117 | end 118 | get_it '/' 119 | should.be.redirection 120 | headers['Location'].should.equal 'foo' 121 | body.should.equal 'blah' 122 | end 123 | 124 | specify "redirects permanently with 301 status code" do 125 | get "/" do 126 | redirect 'foo', 301 127 | end 128 | get_it '/' 129 | should.be.redirection 130 | headers['Location'].should.equal 'foo' 131 | status.should.equal 301 132 | body.should.be.empty 133 | end 134 | 135 | specify "stop sets content and ends event" do 136 | get '/set_body' do 137 | stop 'Hello!' 138 | stop 'World!' 139 | fail 'stop should have halted' 140 | end 141 | 142 | get_it '/set_body' 143 | 144 | should.be.ok 145 | body.should.equal 'Hello!' 146 | 147 | end 148 | 149 | specify "should easily set response Content-Type" do 150 | get '/foo.html' do 151 | content_type 'text/html', :charset => 'utf-8' 152 | "

Hello, World

" 153 | end 154 | 155 | get_it '/foo.html' 156 | should.be.ok 157 | headers['Content-Type'].should.equal 'text/html;charset=utf-8' 158 | body.should.equal '

Hello, World

' 159 | 160 | get '/foo_test.xml' do 161 | content_type :xml 162 | "" 163 | end 164 | 165 | get_it '/foo_test.xml' 166 | should.be.ok 167 | headers['Content-Type'].should.equal 'application/xml' 168 | body.should.equal '' 169 | end 170 | 171 | specify "supports conditional GETs with last_modified" do 172 | modified_at = Time.now 173 | get '/maybe' do 174 | last_modified modified_at 175 | 'response body, maybe' 176 | end 177 | 178 | get_it '/maybe' 179 | should.be.ok 180 | body.should.equal 'response body, maybe' 181 | 182 | get_it '/maybe', :env => { 'HTTP_IF_MODIFIED_SINCE' => modified_at.httpdate } 183 | status.should.equal 304 184 | body.should.equal '' 185 | end 186 | 187 | specify "supports conditional GETs with entity_tag" do 188 | get '/strong' do 189 | entity_tag 'FOO' 190 | 'foo response' 191 | end 192 | 193 | get_it '/strong' 194 | should.be.ok 195 | body.should.equal 'foo response' 196 | 197 | get_it '/strong', {}, 198 | 'HTTP_IF_NONE_MATCH' => '"BAR"' 199 | should.be.ok 200 | body.should.equal 'foo response' 201 | 202 | get_it '/strong', {}, 203 | 'HTTP_IF_NONE_MATCH' => '"FOO"' 204 | status.should.equal 304 205 | body.should.equal '' 206 | 207 | get_it '/strong', {}, 208 | 'HTTP_IF_NONE_MATCH' => '"BAR", *' 209 | status.should.equal 304 210 | body.should.equal '' 211 | end 212 | 213 | specify "delegates HEAD requests to GET handlers" do 214 | get '/invisible' do 215 | "I am invisible to the world" 216 | end 217 | 218 | head_it '/invisible' 219 | should.be.ok 220 | body.should.not.equal "I am invisible to the world" 221 | body.should.equal '' 222 | end 223 | 224 | 225 | specify "supports PUT" do 226 | put '/' do 227 | 'puted' 228 | end 229 | put_it '/' 230 | assert_equal 'puted', body 231 | end 232 | 233 | specify "rewrites POSTs with _method param to PUT" do 234 | put '/' do 235 | 'puted' 236 | end 237 | post_it '/', :_method => 'PUT' 238 | assert_equal 'puted', body 239 | end 240 | 241 | specify "rewrites POSTs with lowercase _method param to PUT" do 242 | put '/' do 243 | 'puted' 244 | end 245 | post_it '/', :_method => 'put' 246 | body.should.equal 'puted' 247 | end 248 | 249 | specify "does not rewrite GETs with _method param to PUT" do 250 | get '/' do 251 | 'getted' 252 | end 253 | get_it '/', :_method => 'put' 254 | should.be.ok 255 | body.should.equal 'getted' 256 | end 257 | 258 | specify "ignores _method query string parameter on non-POST requests" do 259 | post '/' do 260 | 'posted' 261 | end 262 | put '/' do 263 | 'booo' 264 | end 265 | post_it "/?_method=PUT" 266 | should.be.ok 267 | body.should.equal 'posted' 268 | end 269 | 270 | specify "does not read body if content type is not url encoded" do 271 | post '/foo.xml' do 272 | request.env['CONTENT_TYPE'].should.be == 'application/xml' 273 | request.content_type.should.be == 'application/xml' 274 | request.body.read 275 | end 276 | 277 | post_it '/foo.xml', '', :content_type => 'application/xml' 278 | @response.should.be.ok 279 | @response.body.should.be == '' 280 | end 281 | 282 | end 283 | -------------------------------------------------------------------------------- /lib/sinatra/compat.rb: -------------------------------------------------------------------------------- 1 | # Sinatra 0.3.x compatibility module. 2 | # 3 | # The following code makes Sinatra 0.9.x compatible with Sinatra 0.3.x to 4 | # ease the transition to the final 1.0 release. Everything defined in this 5 | # file will be removed for the 1.0 release. 6 | 7 | require 'ostruct' 8 | require 'sinatra/base' 9 | require 'sinatra/main' 10 | 11 | # Like Kernel#warn but outputs the location that triggered the warning. 12 | def sinatra_warn(*message) #:nodoc: 13 | line = caller. 14 | detect { |line| line !~ /(?:lib\/sinatra\/|__DELEGATE__)/ }. 15 | sub(/:in .*/, '') 16 | warn "#{line}: warning: #{message.join(' ')}" 17 | end 18 | 19 | # Rack now supports evented and swiftiplied mongrels through separate 20 | # handler. 21 | if ENV['SWIFT'] 22 | sinatra_warn 'the SWIFT environment variable is deprecated;', 23 | 'use Rack::Handler::SwiftipliedMongrel instead.' 24 | require 'swiftcore/swiftiplied_mongrel' 25 | puts "Using Swiftiplied Mongrel" 26 | elsif ENV['EVENT'] 27 | sinatra_warn 'the EVENT environment variable is deprecated;', 28 | 'use Rack::Handler::EventedMongrel instead.' 29 | require 'swiftcore/evented_mongrel' 30 | puts "Using Evented Mongrel" 31 | end 32 | 33 | # Make Rack 0.9.0 backward compatibile with 0.4.0 mime types. This isn't 34 | # technically a Sinatra issue but many Sinatra apps access the old 35 | # MIME_TYPES constants due to Sinatra example code. 36 | require 'rack/file' 37 | module Rack #:nodoc: 38 | class File #:nodoc: 39 | def self.const_missing(const_name) 40 | if const_name == :MIME_TYPES 41 | hash = Hash.new { |hash,key| Rack::Mime::MIME_TYPES[".#{key}"] } 42 | const_set :MIME_TYPES, hash 43 | sinatra_warn 'Rack::File::MIME_TYPES is deprecated; use Rack::Mime instead.' 44 | hash 45 | else 46 | super 47 | end 48 | end 49 | end 50 | end 51 | 52 | module Sinatra 53 | module Compat #:nodoc: 54 | end 55 | 56 | # Make Sinatra::EventContext an alias for Sinatra::Default to unbreak plugins. 57 | def self.const_missing(const_name) #:nodoc: 58 | if const_name == :EventContext 59 | const_set :EventContext, Sinatra::Default 60 | sinatra_warn 'Sinatra::EventContext is deprecated; use Sinatra::Default instead.' 61 | Sinatra::Default 62 | else 63 | super 64 | end 65 | end 66 | 67 | # The ServerError exception is deprecated. Any exception is considered an 68 | # internal server error. 69 | class ServerError < RuntimeError 70 | def initialize(*args, &block) 71 | sinatra_warn 'Sinatra::ServerError is deprecated;', 72 | 'use another exception, error, or Kernel#fail instead.' 73 | end 74 | def code ; 500 ; end 75 | end 76 | 77 | class Default < Base 78 | def self.const_missing(const_name) #:nodoc: 79 | if const_name == :FORWARD_METHODS 80 | sinatra_warn 'Sinatra::Application::FORWARD_METHODS is deprecated;', 81 | 'use Sinatra::Delegator::METHODS instead.' 82 | const_set :FORWARD_METHODS, Sinatra::Delegator::METHODS 83 | Sinatra::Delegator::METHODS 84 | else 85 | super 86 | end 87 | end 88 | 89 | # Deprecated. Use: response['Header-Name'] 90 | def header(header=nil) 91 | sinatra_warn "The 'header' method is deprecated; use 'headers' instead." 92 | headers(header) 93 | end 94 | 95 | # Deprecated. Use: halt 96 | def stop(*args, &block) 97 | sinatra_warn "The 'stop' method is deprecated; use 'halt' instead." 98 | halt(*args, &block) 99 | end 100 | 101 | # Deprecated. Use: etag 102 | def entity_tag(*args, &block) 103 | sinatra_warn "The 'entity_tag' method is deprecated; use 'etag' instead." 104 | etag(*args, &block) 105 | end 106 | 107 | # Deprecated. Use the #attachment helper and return the data as a String or 108 | # Array. 109 | def send_data(data, options={}) 110 | sinatra_warn "The 'send_data' method is deprecated. use attachment, status, content_type, etc. helpers instead." 111 | 112 | status options[:status] if options[:status] 113 | attachment options[:filename] if options[:disposition] == 'attachment' 114 | content_type options[:type] if options[:type] 115 | halt data 116 | end 117 | 118 | # Throwing halt with a Symbol and the to_result convention are 119 | # deprecated. Override the invoke method to detect those types of return 120 | # values. 121 | def invoke(&block) #:nodoc: 122 | res = super 123 | case 124 | when res.kind_of?(Symbol) 125 | sinatra_warn "Invoking the :#{res} helper by returning a Symbol is deprecated;", 126 | "call the helper directly instead." 127 | @response.body = __send__(res) 128 | when res.respond_to?(:to_result) 129 | sinatra_warn "The to_result convention is deprecated." 130 | @response.body = res.to_result(self) 131 | end 132 | res 133 | end 134 | 135 | def options #:nodoc: 136 | Options.new(self.class) 137 | end 138 | 139 | class Options < Struct.new(:target) #:nodoc: 140 | def method_missing(name, *args, &block) 141 | if target.respond_to?(name) 142 | target.__send__(name, *args, &block) 143 | elsif args.empty? && name.to_s !~ /=$/ 144 | sinatra_warn 'accessing undefined options will raise a NameError in Sinatra 1.0' 145 | nil 146 | else 147 | super 148 | end 149 | end 150 | end 151 | 152 | class << self 153 | # Deprecated. Options are stored directly on the class object. 154 | def options 155 | sinatra_warn "The 'options' class method is deprecated; use 'self' instead." 156 | Options.new(self) 157 | end 158 | 159 | # Deprecated. Use: configure 160 | def configures(*args, &block) 161 | sinatra_warn "The 'configures' method is deprecated; use 'configure' instead." 162 | configure(*args, &block) 163 | end 164 | 165 | # Deprecated. Use: set 166 | def default_options 167 | sinatra_warn "Sinatra::Application.default_options is deprecated; use 'set' instead." 168 | fake = lambda { |options| set(options) } 169 | def fake.merge!(options) ; call(options) ; end 170 | fake 171 | end 172 | 173 | # Deprecated. Use: set 174 | def set_option(*args, &block) 175 | sinatra_warn "The 'set_option' method is deprecated; use 'set' instead." 176 | set(*args, &block) 177 | end 178 | 179 | def set_options(*args, &block) 180 | sinatra_warn "The 'set_options' method is deprecated; use 'set' instead." 181 | set(*args, &block) 182 | end 183 | 184 | # Deprecated. Use: set :environment, ENV 185 | def env=(value) 186 | sinatra_warn "The :env option is deprecated; use :environment instead." 187 | set :environment, value 188 | end 189 | 190 | # Deprecated. Use: options.environment 191 | def env 192 | sinatra_warn "The :env option is deprecated; use :environment instead." 193 | environment 194 | end 195 | end 196 | 197 | # Deprecated. Missing messages are no longer delegated to @response. 198 | def method_missing(name, *args, &b) #:nodoc: 199 | if @response.respond_to?(name) 200 | sinatra_warn "The '#{name}' method is deprecated; use 'response.#{name}' instead." 201 | @response.send(name, *args, &b) 202 | else 203 | super 204 | end 205 | end 206 | end 207 | 208 | class << self 209 | # Deprecated. Use: Sinatra::Application 210 | def application 211 | sinatra_warn "Sinatra.application is deprecated; use Sinatra::Application instead." 212 | Sinatra::Application 213 | end 214 | 215 | # Deprecated. Use: Sinatra::Application.reset! 216 | def application=(value) 217 | raise ArgumentError unless value.nil? 218 | sinatra_warn "Setting Sinatra.application to nil is deprecated; create a new instance instead." 219 | Sinatra.class_eval do 220 | remove_const :Application 221 | const_set :Application, Class.new(Sinatra::Default) 222 | end 223 | end 224 | 225 | def build_application 226 | sinatra_warn "Sinatra.build_application is deprecated; use Sinatra::Application instead." 227 | Sinatra::Application 228 | end 229 | 230 | def options 231 | sinatra_warn "Sinatra.options is deprecated; use Sinatra::Application.option_name instead." 232 | Sinatra::Application.options 233 | end 234 | 235 | def port 236 | sinatra_warn "Sinatra.port is deprecated; use Sinatra::Application.port instead." 237 | options.port 238 | end 239 | 240 | def host 241 | sinatra_warn "Sinatra.host is deprecated; use Sinatra::Application.host instead." 242 | options.host 243 | end 244 | 245 | def env 246 | sinatra_warn "Sinatra.env is deprecated; use Sinatra::Application.environment instead." 247 | options.environment 248 | end 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /test/options_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Options' do 4 | before do 5 | restore_default_options 6 | @app = Sinatra.new 7 | end 8 | 9 | it 'sets options to literal values' do 10 | @app.set(:foo, 'bar') 11 | assert @app.respond_to?(:foo) 12 | assert_equal 'bar', @app.foo 13 | end 14 | 15 | it 'sets options to Procs' do 16 | @app.set(:foo, Proc.new { 'baz' }) 17 | assert @app.respond_to?(:foo) 18 | assert_equal 'baz', @app.foo 19 | end 20 | 21 | it "sets multiple options with a Hash" do 22 | @app.set :foo => 1234, 23 | :bar => 'Hello World', 24 | :baz => Proc.new { 'bizzle' } 25 | assert_equal 1234, @app.foo 26 | assert_equal 'Hello World', @app.bar 27 | assert_equal 'bizzle', @app.baz 28 | end 29 | 30 | it 'inherits option methods when subclassed' do 31 | @app.set :foo, 'bar' 32 | @app.set :biz, Proc.new { 'baz' } 33 | 34 | sub = Class.new(@app) 35 | assert sub.respond_to?(:foo) 36 | assert_equal 'bar', sub.foo 37 | assert sub.respond_to?(:biz) 38 | assert_equal 'baz', sub.biz 39 | end 40 | 41 | it 'overrides options in subclass' do 42 | @app.set :foo, 'bar' 43 | @app.set :biz, Proc.new { 'baz' } 44 | sub = Class.new(@app) 45 | sub.set :foo, 'bling' 46 | assert_equal 'bling', sub.foo 47 | assert_equal 'bar', @app.foo 48 | end 49 | 50 | it 'creates setter methods when first defined' do 51 | @app.set :foo, 'bar' 52 | assert @app.respond_to?('foo=') 53 | @app.foo = 'biz' 54 | assert_equal 'biz', @app.foo 55 | end 56 | 57 | it 'creates predicate methods when first defined' do 58 | @app.set :foo, 'hello world' 59 | assert @app.respond_to?(:foo?) 60 | assert @app.foo? 61 | @app.set :foo, nil 62 | assert !@app.foo? 63 | end 64 | 65 | it 'uses existing setter methods if detected' do 66 | class << @app 67 | def foo 68 | @foo 69 | end 70 | def foo=(value) 71 | @foo = 'oops' 72 | end 73 | end 74 | 75 | @app.set :foo, 'bam' 76 | assert_equal 'oops', @app.foo 77 | end 78 | 79 | it "sets multiple options to true with #enable" do 80 | @app.enable :sessions, :foo, :bar 81 | assert @app.sessions 82 | assert @app.foo 83 | assert @app.bar 84 | end 85 | 86 | it "sets multiple options to false with #disable" do 87 | @app.disable :sessions, :foo, :bar 88 | assert !@app.sessions 89 | assert !@app.foo 90 | assert !@app.bar 91 | end 92 | 93 | it 'enables MethodOverride middleware when :methodoverride is enabled' do 94 | @app.set :methodoverride, true 95 | @app.put('/') { 'okay' } 96 | post '/', {'_method'=>'PUT'}, {} 97 | assert_equal 200, status 98 | assert_equal 'okay', body 99 | end 100 | end 101 | 102 | describe_option 'clean_trace' do 103 | def clean_backtrace(trace) 104 | @base.new.send(:clean_backtrace, trace) 105 | end 106 | 107 | it 'is enabled on Base' do 108 | assert @base.clean_trace? 109 | end 110 | 111 | it 'is enabled on Default' do 112 | assert @default.clean_trace? 113 | end 114 | 115 | it 'does nothing when disabled' do 116 | backtrace = [ 117 | "./lib/sinatra/base.rb", 118 | "./myapp:42", 119 | ("#{Gem.dir}/some/lib.rb" if defined?(Gem)) 120 | ].compact 121 | @base.set :clean_trace, false 122 | assert_equal backtrace, clean_backtrace(backtrace) 123 | end 124 | 125 | it 'removes sinatra lib paths from backtrace when enabled' do 126 | backtrace = [ 127 | "./lib/sinatra/base.rb", 128 | "./lib/sinatra/compat.rb:42", 129 | "./lib/sinatra/main.rb:55 in `foo'" 130 | ] 131 | assert clean_backtrace(backtrace).empty? 132 | end 133 | 134 | it 'removes ./ prefix from backtrace paths when enabled' do 135 | assert_equal ['myapp.rb:42'], clean_backtrace(['./myapp.rb:42']) 136 | end 137 | 138 | if defined?(Gem) 139 | it 'removes gem lib paths from backtrace when enabled' do 140 | assert clean_backtrace(["#{Gem.dir}/some/lib"]).empty? 141 | end 142 | end 143 | end 144 | 145 | describe_option 'run' do 146 | it 'is disabled on Base' do 147 | assert ! @base.run? 148 | end 149 | 150 | it 'is enabled on Default when not in test environment' do 151 | assert @default.development? 152 | assert @default.run? 153 | 154 | @default.set :environment, :development 155 | assert @default.run? 156 | end 157 | 158 | # TODO: it 'is enabled when $0 == app_file' 159 | end 160 | 161 | describe_option 'raise_errors' do 162 | it 'is enabled on Base' do 163 | assert @base.raise_errors? 164 | end 165 | 166 | it 'is enabled on Default only in test' do 167 | @default.set(:environment, :development) 168 | assert @default.development? 169 | assert ! @default.raise_errors?, "disabled development" 170 | 171 | @default.set(:environment, :production) 172 | assert ! @default.raise_errors? 173 | 174 | @default.set(:environment, :test) 175 | assert @default.raise_errors? 176 | end 177 | end 178 | 179 | describe_option 'dump_errors' do 180 | it 'is disabled on Base' do 181 | assert ! @base.dump_errors? 182 | end 183 | 184 | it 'is enabled on Default' do 185 | assert @default.dump_errors? 186 | end 187 | 188 | it 'dumps exception with backtrace to rack.errors' do 189 | Sinatra::Default.disable(:raise_errors) 190 | 191 | mock_app(Sinatra::Default) { 192 | error do 193 | error = @env['rack.errors'].instance_variable_get(:@error) 194 | error.rewind 195 | 196 | error.read 197 | end 198 | 199 | get '/' do 200 | raise 201 | end 202 | } 203 | 204 | get '/' 205 | assert body.include?("RuntimeError") && body.include?("options_test.rb") 206 | end 207 | end 208 | 209 | describe_option 'sessions' do 210 | it 'is disabled on Base' do 211 | assert ! @base.sessions? 212 | end 213 | 214 | it 'is disabled on Default' do 215 | assert ! @default.sessions? 216 | end 217 | 218 | # TODO: it 'uses Rack::Session::Cookie when enabled' do 219 | end 220 | 221 | describe_option 'logging' do 222 | it 'is disabled on Base' do 223 | assert ! @base.logging? 224 | end 225 | 226 | it 'is enabled on Default when not in test environment' do 227 | assert @default.logging? 228 | 229 | @default.set :environment, :test 230 | assert ! @default.logging 231 | end 232 | 233 | # TODO: it 'uses Rack::CommonLogger when enabled' do 234 | end 235 | 236 | describe_option 'static' do 237 | it 'is disabled on Base' do 238 | assert ! @base.static? 239 | end 240 | 241 | it 'is enabled on Default' do 242 | assert @default.static? 243 | end 244 | 245 | # TODO: it setup static routes if public is enabled 246 | # TODO: however, that's already tested in static_test so... 247 | end 248 | 249 | describe_option 'host' do 250 | it 'defaults to 0.0.0.0' do 251 | assert_equal '0.0.0.0', @base.host 252 | assert_equal '0.0.0.0', @default.host 253 | end 254 | end 255 | 256 | describe_option 'port' do 257 | it 'defaults to 4567' do 258 | assert_equal 4567, @base.port 259 | assert_equal 4567, @default.port 260 | end 261 | end 262 | 263 | describe_option 'server' do 264 | it 'is one of thin, mongrel, webrick' do 265 | assert_equal %w[thin mongrel webrick], @base.server 266 | assert_equal %w[thin mongrel webrick], @default.server 267 | end 268 | end 269 | 270 | describe_option 'app_file' do 271 | it 'is nil' do 272 | assert @base.app_file.nil? 273 | assert @default.app_file.nil? 274 | end 275 | end 276 | 277 | describe_option 'root' do 278 | it 'is nil if app_file is not set' do 279 | assert @base.root.nil? 280 | assert @default.root.nil? 281 | end 282 | 283 | it 'is equal to the expanded basename of app_file' do 284 | @base.app_file = __FILE__ 285 | assert_equal File.expand_path(File.dirname(__FILE__)), @base.root 286 | 287 | @default.app_file = __FILE__ 288 | assert_equal File.expand_path(File.dirname(__FILE__)), @default.root 289 | end 290 | end 291 | 292 | describe_option 'views' do 293 | it 'is nil if root is not set' do 294 | assert @base.views.nil? 295 | assert @default.views.nil? 296 | end 297 | 298 | it 'is set to root joined with views/' do 299 | @base.root = File.dirname(__FILE__) 300 | assert_equal File.dirname(__FILE__) + "/views", @base.views 301 | 302 | @default.root = File.dirname(__FILE__) 303 | assert_equal File.dirname(__FILE__) + "/views", @default.views 304 | end 305 | end 306 | 307 | describe_option 'public' do 308 | it 'is nil if root is not set' do 309 | assert @base.public.nil? 310 | assert @default.public.nil? 311 | end 312 | 313 | it 'is set to root joined with public/' do 314 | @base.root = File.dirname(__FILE__) 315 | assert_equal File.dirname(__FILE__) + "/public", @base.public 316 | 317 | @default.root = File.dirname(__FILE__) 318 | assert_equal File.dirname(__FILE__) + "/public", @default.public 319 | end 320 | end 321 | 322 | describe_option 'reload' do 323 | it 'is enabled when 324 | app_file is set, 325 | is not a rackup file, 326 | and we are in development' do 327 | @base.app_file = __FILE__ 328 | @base.set(:environment, :development) 329 | assert @base.reload? 330 | 331 | @default.app_file = __FILE__ 332 | @default.set(:environment, :development) 333 | assert @default.reload? 334 | end 335 | 336 | it 'is disabled if app_file is not set' do 337 | assert ! @base.reload? 338 | assert ! @default.reload? 339 | end 340 | 341 | it 'is disabled if app_file is a rackup file' do 342 | @base.app_file = 'config.ru' 343 | assert ! @base.reload? 344 | 345 | @default.app_file = 'config.ru' 346 | assert ! @base.reload? 347 | end 348 | 349 | it 'is disabled if we are not in development' do 350 | @base.set(:environment, :foo) 351 | assert ! @base.reload 352 | 353 | @default.set(:environment, :bar) 354 | assert ! @default.reload 355 | end 356 | end 357 | 358 | describe_option 'lock' do 359 | it 'is enabled when reload is enabled' do 360 | @base.enable(:reload) 361 | assert @base.lock? 362 | 363 | @default.enable(:reload) 364 | assert @default.lock? 365 | end 366 | 367 | it 'is disabled when reload is disabled' do 368 | @base.disable(:reload) 369 | assert ! @base.lock? 370 | 371 | @default.disable(:reload) 372 | assert ! @default.lock? 373 | end 374 | end 375 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | = 0.9.1.1 / 2009-03-09 2 | 3 | * Fix directory traversal vulnerability in default static files 4 | route. See [#177] for more info. 5 | 6 | = 0.9.1 / 2009-03-01 7 | 8 | * Sinatra now runs under Ruby 1.9.1 [#61] 9 | 10 | * Route patterns (splats, :named, or Regexp captures) are now 11 | passed as arguments to the block. [#140] 12 | 13 | * The "helpers" method now takes a variable number of modules 14 | along with the normal block syntax. [#133] 15 | 16 | * New request-level #forward method for middleware components: passes 17 | the env to the downstream app and merges the response status, headers, 18 | and body into the current context. [#126] 19 | 20 | * Requests are now automatically forwarded to the downstream app when 21 | running as middleware and no matching route is found or all routes 22 | pass. 23 | 24 | * New simple API for extensions/plugins to add DSL-level and 25 | request-level methods. Use Sinatra.register(mixin) to extend 26 | the DSL with all public methods defined in the mixin module; 27 | use Sinatra.helpers(mixin) to make all public methods defined 28 | in the mixin module available at the request level. [#138] 29 | See http://www.sinatrarb.com/extensions.html for details. 30 | 31 | * Named parameters in routes now capture the "." character. This makes 32 | routes like "/:path/:filename" match against requests like 33 | "/foo/bar.txt"; in this case, "params[:filename]" is "bar.txt". 34 | Previously, the route would not match at all. 35 | 36 | * Added request-level "redirect back" to redirect to the referring 37 | URL. 38 | 39 | * Added a new "clean_trace" option that causes backtraces dumped 40 | to rack.errors and displayed on the development error page to 41 | omit framework and core library backtrace lines. The option is 42 | enabled by default. [#77] 43 | 44 | * The ERB output buffer is now available to helpers via the @_out_buf 45 | instance variable. 46 | 47 | * It's now much easier to test sessions in unit tests by passing a 48 | ":session" option to any of the mock request methods. e.g., 49 | get '/', {}, :session => { 'foo' => 'bar' } 50 | 51 | * The testing framework specific files ('sinatra/test/spec', 52 | 'sinatra/test/bacon', 'sinatra/test/rspec', etc.) have been deprecated. 53 | See http://sinatrarb.com/testing.html for instructions on setting up 54 | a testing environment with these frameworks. 55 | 56 | * The request-level #send_data method from Sinatra 0.3.3 has been added 57 | for compatibility but is deprecated. 58 | 59 | * Fix :provides causing crash on any request when request has no 60 | Accept header [#139] 61 | 62 | * Fix that ERB templates were evaluated twice per "erb" call. 63 | 64 | * Fix app-level middleware not being run when the Sinatra application is 65 | run as middleware. 66 | 67 | * Fixed some issues with running under Rack's CGI handler caused by 68 | writing informational stuff to stdout. 69 | 70 | * Fixed that reloading was sometimes enabled when starting from a 71 | rackup file [#110] 72 | 73 | * Fixed that "." in route patterns erroneously matched any character 74 | instead of a literal ".". [#124] 75 | 76 | = 0.9.0.4 / 2009-01-25 77 | 78 | * Using halt with more than 1 args causes ArgumentError [#131] 79 | * using halt in a before filter doesn't modify response [#127] 80 | * Add deprecated Sinatra::EventContext to unbreak plugins [#130] 81 | * Give access to GET/POST params in filters [#129] 82 | * Preserve non-nested params in nested params hash [#117] 83 | * Fix backtrace dump with Rack::Lint [#116] 84 | 85 | = 0.9.0.3 / 2009-01-21 86 | 87 | * Fall back on mongrel then webrick when thin not found. [#75] 88 | * Use :environment instead of :env in test helpers to 89 | fix deprecation warnings coming from framework. 90 | * Make sinatra/test/rspec work again [#113] 91 | * Fix app_file detection on windows [#118] 92 | * Fix static files with Rack::Lint in pipeline [#121] 93 | 94 | = 0.9.0.2 / 2009-01-18 95 | 96 | * Halting a before block should stop processing of routes [#85] 97 | * Fix redirect/halt in before filters [#85] 98 | 99 | = 0.9.0 / 2009-01-18 100 | 101 | * Works with and requires Rack >= 0.9.1 102 | 103 | * Multiple Sinatra applications can now co-exist peacefully within a 104 | single process. The new "Sinatra::Base" class can be subclassed to 105 | establish a blank-slate Rack application or middleware component. 106 | Documentation on using these features is forth-coming; the following 107 | provides the basic gist: http://gist.github.com/38605 108 | 109 | * Parameters with subscripts are now parsed into a nested/recursive 110 | Hash structure. e.g., "post[title]=Hello&post[body]=World" yields 111 | params: {'post' => {'title' => 'Hello', 'body' => 'World'}}. 112 | 113 | * Regular expressions may now be used in route pattens; captures are 114 | available at "params[:captures]". 115 | 116 | * New ":provides" route condition takes an array of mime types and 117 | matches only when an Accept request header is present with a 118 | corresponding type. [cypher] 119 | 120 | * New request-level "pass" method; immediately exits the current block 121 | and passes control to the next matching route. 122 | 123 | * The request-level "body" method now takes a block; evaluation is 124 | deferred until an attempt is made to read the body. The block must 125 | return a String or Array. 126 | 127 | * New "route conditions" system for attaching rules for when a route 128 | matches. The :agent and :host route options now use this system. 129 | 130 | * New "dump_errors" option controls whether the backtrace is dumped to 131 | rack.errors when an exception is raised from a route. The option is 132 | enabled by default for top-level apps. 133 | 134 | * Better default "app_file", "root", "public", and "views" location 135 | detection; changes to "root" and "app_file" automatically cascade to 136 | other options that depend on them. 137 | 138 | * Error mappings are now split into two distinct layers: exception 139 | mappings and custom error pages. Exception mappings are registered 140 | with "error(Exception)" and are run only when the app raises an 141 | exception. Custom error pages are registered with "error(status_code)", 142 | where "status_code" is an integer, and are run any time the response 143 | has the status code specified. It's also possible to register an error 144 | page for a range of status codes: "error(500..599)". 145 | 146 | * In-file templates are now automatically imported from the file that 147 | requires 'sinatra'. The use_in_file_templates! method is still available 148 | for loading templates from other files. 149 | 150 | * Sinatra's testing support is no longer dependent on Test::Unit. Requiring 151 | 'sinatra/test' adds the Sinatra::Test module and Sinatra::TestHarness 152 | class, which can be used with any test framework. The 'sinatra/test/unit', 153 | 'sinatra/test/spec', 'sinatra/test/rspec', or 'sinatra/test/bacon' files 154 | can be required to setup a framework-specific testing environment. See the 155 | README for more information. 156 | 157 | * Added support for Bacon (test framework). The 'sinatra/test/bacon' file 158 | can be required to setup Sinatra test helpers on Bacon::Context. 159 | 160 | * Deprecated "set_option" and "set_options"; use "set" instead. 161 | 162 | * Deprecated the "env" option ("options.env"); use "environment" instead. 163 | 164 | * Deprecated the request level "stop" method; use "halt" instead. 165 | 166 | * Deprecated the request level "entity_tag" method; use "etag" instead. 167 | Both "entity_tag" and "etag" were previously supported. 168 | 169 | * Deprecated the request level "headers" method (HTTP response headers); 170 | use "response['Header-Name']" instead. 171 | 172 | * Deprecated "Sinatra.application"; use "Sinatra::Application" instead. 173 | 174 | * Deprecated setting Sinatra.application = nil to reset an application. 175 | This should no longer be necessary. 176 | 177 | * Deprecated "Sinatra.default_options"; use 178 | "Sinatra::Default.set(key, value)" instead. 179 | 180 | * Deprecated the "ServerError" exception. All Exceptions are now 181 | treated as internal server errors and result in a 500 response 182 | status. 183 | 184 | * Deprecated the "get_it", "post_it", "put_it", "delete_it", and "head_it" 185 | test helper methods. Use "get", "post", "put", "delete", and "head", 186 | respectively, instead. 187 | 188 | * Removed Event and EventContext classes. Applications are defined in a 189 | subclass of Sinatra::Base; each request is processed within an 190 | instance. 191 | 192 | = 0.3.3 / 2009-01-06 193 | 194 | * Pin to Rack 0.4.0 (this is the last release on Rack 0.4) 195 | 196 | * Log unhandled exception backtraces to rack.errors. 197 | 198 | * Use RACK_ENV environment variable to establish Sinatra 199 | environment when given. Thin sets this when started with 200 | the -e argument. 201 | 202 | * BUG: raising Sinatra::NotFound resulted in a 500 response 203 | code instead of 404. 204 | 205 | * BUG: use_in_file_templates! fails with CR/LF (#45) 206 | 207 | * BUG: Sinatra detects the app file and root path when run under 208 | thin/passenger. 209 | 210 | = 0.3.2 211 | 212 | * BUG: Static and send_file read entire file into String before 213 | sending. Updated to stream with 8K chunks instead. 214 | 215 | * Rake tasks and assets for building basic documentation website. 216 | See http://sinatra.rubyforge.org 217 | 218 | * Various minor doc fixes. 219 | 220 | = 0.3.1 221 | 222 | * Unbreak optional path parameters [jeremyevans] 223 | 224 | = 0.3.0 225 | 226 | * Add sinatra.gemspec w/ support for github gem builds. Forks can now 227 | enable the build gem option in github to get free username-sinatra.gem 228 | builds: gem install username-sinatra.gem --source=http://gems.github.com/ 229 | 230 | * Require rack-0.4 gem; removes frozen rack dir. 231 | 232 | * Basic RSpec support; require 'sinatra/test/rspec' instead of 233 | 'sinatra/test/spec' to use. [avdi] 234 | 235 | * before filters can modify request environment vars used for 236 | routing (e.g., PATH_INFO, REQUEST_METHOD, etc.) for URL rewriting 237 | type functionality. 238 | 239 | * In-file templates now uses @@ instead of ## as template separator. 240 | 241 | * Top-level environment test predicates: development?, test?, production? 242 | 243 | * Top-level "set", "enable", and "disable" methods for tweaking 244 | app options. [rtomayko] 245 | 246 | * Top-level "use" method for building Rack middleware pipelines 247 | leading to app. See README for usage. [rtomayko] 248 | 249 | * New "reload" option - set false to disable reloading in development. 250 | 251 | * New "host" option - host/ip to bind to [cschneid] 252 | 253 | * New "app_file" option - override the file to reload in development 254 | mode [cschneid] 255 | 256 | * Development error/not_found page cleanup [sr, adamwiggins] 257 | 258 | * Remove a bunch of core extensions (String#to_param, String#from_param, 259 | Hash#from_params, Hash#to_params, Hash#symbolize_keys, Hash#pass) 260 | 261 | * Various grammar and formatting fixes to README; additions on 262 | community and contributing [cypher] 263 | 264 | * Build RDoc using Hanna template: http://sinatrarb.rubyforge.org/api 265 | 266 | * Specs, documentation and fixes for splat'n routes [vic] 267 | 268 | * Fix whitespace errors across all source files. [rtomayko] 269 | 270 | * Fix streaming issues with Mongrel (body not closed). [bmizerany] 271 | 272 | * Fix various issues with environment not being set properly (configure 273 | blocks not running, error pages not registering, etc.) [cypher] 274 | 275 | * Fix to allow locals to be passed to ERB templates [cschneid] 276 | 277 | * Fix locking issues causing random errors during reload in development. 278 | 279 | * Fix for escaped paths not resolving static files [Matthew Walker] 280 | 281 | = 0.2.1 282 | 283 | * File upload fix and minor tweaks. 284 | 285 | = 0.2.0 286 | 287 | * Initial gem release of 0.2 codebase. 288 | -------------------------------------------------------------------------------- /test/helpers_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'Helpers#status' do 4 | before do 5 | mock_app { 6 | get '/' do 7 | status 207 8 | nil 9 | end 10 | } 11 | end 12 | 13 | it 'sets the response status code' do 14 | get '/' 15 | assert_equal 207, response.status 16 | end 17 | end 18 | 19 | describe 'Helpers#body' do 20 | it 'takes a block for defered body generation' do 21 | mock_app { 22 | get '/' do 23 | body { 'Hello World' } 24 | end 25 | } 26 | 27 | get '/' 28 | assert_equal 'Hello World', body 29 | end 30 | 31 | it 'takes a String, Array, or other object responding to #each' do 32 | mock_app { 33 | get '/' do 34 | body 'Hello World' 35 | end 36 | } 37 | 38 | get '/' 39 | assert_equal 'Hello World', body 40 | end 41 | end 42 | 43 | describe 'Helpers#redirect' do 44 | it 'uses a 302 when only a path is given' do 45 | mock_app { 46 | get '/' do 47 | redirect '/foo' 48 | fail 'redirect should halt' 49 | end 50 | } 51 | 52 | get '/' 53 | assert_equal 302, status 54 | assert_equal '', body 55 | assert_equal '/foo', response['Location'] 56 | end 57 | 58 | it 'uses the code given when specified' do 59 | mock_app { 60 | get '/' do 61 | redirect '/foo', 301 62 | fail 'redirect should halt' 63 | end 64 | } 65 | 66 | get '/' 67 | assert_equal 301, status 68 | assert_equal '', body 69 | assert_equal '/foo', response['Location'] 70 | end 71 | 72 | it 'redirects back to request.referer when passed back' do 73 | mock_app { 74 | get '/try_redirect' do 75 | redirect back 76 | end 77 | } 78 | 79 | request = Rack::MockRequest.new(@app) 80 | response = request.get('/try_redirect', 'HTTP_REFERER' => '/foo') 81 | assert_equal 302, response.status 82 | assert_equal '/foo', response['Location'] 83 | end 84 | 85 | end 86 | 87 | describe 'Helpers#error' do 88 | it 'sets a status code and halts' do 89 | mock_app { 90 | get '/' do 91 | error 501 92 | fail 'error should halt' 93 | end 94 | } 95 | 96 | get '/' 97 | assert_equal 501, status 98 | assert_equal '', body 99 | end 100 | 101 | it 'takes an optional body' do 102 | mock_app { 103 | get '/' do 104 | error 501, 'FAIL' 105 | fail 'error should halt' 106 | end 107 | } 108 | 109 | get '/' 110 | assert_equal 501, status 111 | assert_equal 'FAIL', body 112 | end 113 | 114 | it 'uses a 500 status code when first argument is a body' do 115 | mock_app { 116 | get '/' do 117 | error 'FAIL' 118 | fail 'error should halt' 119 | end 120 | } 121 | 122 | get '/' 123 | assert_equal 500, status 124 | assert_equal 'FAIL', body 125 | end 126 | end 127 | 128 | describe 'Helpers#not_found' do 129 | it 'halts with a 404 status' do 130 | mock_app { 131 | get '/' do 132 | not_found 133 | fail 'not_found should halt' 134 | end 135 | } 136 | 137 | get '/' 138 | assert_equal 404, status 139 | assert_equal '', body 140 | end 141 | end 142 | 143 | describe 'Helpers#headers' do 144 | it 'sets headers on the response object when given a Hash' do 145 | mock_app { 146 | get '/' do 147 | headers 'X-Foo' => 'bar', 'X-Baz' => 'bling' 148 | 'kthx' 149 | end 150 | } 151 | 152 | get '/' 153 | assert ok? 154 | assert_equal 'bar', response['X-Foo'] 155 | assert_equal 'bling', response['X-Baz'] 156 | assert_equal 'kthx', body 157 | end 158 | 159 | it 'returns the response headers hash when no hash provided' do 160 | mock_app { 161 | get '/' do 162 | headers['X-Foo'] = 'bar' 163 | 'kthx' 164 | end 165 | } 166 | 167 | get '/' 168 | assert ok? 169 | assert_equal 'bar', response['X-Foo'] 170 | end 171 | end 172 | 173 | describe 'Helpers#session' do 174 | it 'uses the existing rack.session' do 175 | mock_app { 176 | get '/' do 177 | session[:foo] 178 | end 179 | } 180 | 181 | get '/', :env => { 'rack.session' => { :foo => 'bar' } } 182 | assert_equal 'bar', body 183 | end 184 | 185 | it 'creates a new session when none provided' do 186 | mock_app { 187 | get '/' do 188 | assert session.empty? 189 | session[:foo] = 'bar' 190 | 'Hi' 191 | end 192 | } 193 | 194 | get '/' 195 | assert_equal 'Hi', body 196 | end 197 | end 198 | 199 | describe 'Helpers#media_type' do 200 | include Sinatra::Helpers 201 | 202 | it "looks up media types in Rack's MIME registry" do 203 | Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' 204 | assert_equal 'application/foo', media_type('foo') 205 | assert_equal 'application/foo', media_type('.foo') 206 | assert_equal 'application/foo', media_type(:foo) 207 | end 208 | 209 | it 'returns nil when given nil' do 210 | assert media_type(nil).nil? 211 | end 212 | 213 | it 'returns nil when media type not registered' do 214 | assert media_type(:bizzle).nil? 215 | end 216 | 217 | it 'returns the argument when given a media type string' do 218 | assert_equal 'text/plain', media_type('text/plain') 219 | end 220 | end 221 | 222 | describe 'Helpers#content_type' do 223 | it 'sets the Content-Type header' do 224 | mock_app { 225 | get '/' do 226 | content_type 'text/plain' 227 | 'Hello World' 228 | end 229 | } 230 | 231 | get '/' 232 | assert_equal 'text/plain', response['Content-Type'] 233 | assert_equal 'Hello World', body 234 | end 235 | 236 | it 'takes media type parameters (like charset=)' do 237 | mock_app { 238 | get '/' do 239 | content_type 'text/html', :charset => 'utf-8' 240 | "

Hello, World

" 241 | end 242 | } 243 | 244 | get '/' 245 | assert ok? 246 | assert_equal 'text/html;charset=utf-8', response['Content-Type'] 247 | assert_equal "

Hello, World

", body 248 | end 249 | 250 | it "looks up symbols in Rack's mime types dictionary" do 251 | Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' 252 | mock_app { 253 | get '/foo.xml' do 254 | content_type :foo 255 | "I AM FOO" 256 | end 257 | } 258 | 259 | get '/foo.xml' 260 | assert ok? 261 | assert_equal 'application/foo', response['Content-Type'] 262 | assert_equal 'I AM FOO', body 263 | end 264 | 265 | it 'fails when no mime type is registered for the argument provided' do 266 | mock_app { 267 | get '/foo.xml' do 268 | content_type :bizzle 269 | "I AM FOO" 270 | end 271 | } 272 | assert_raise(RuntimeError) { get '/foo.xml' } 273 | end 274 | end 275 | 276 | describe 'Helpers#send_file' do 277 | before do 278 | @file = File.dirname(__FILE__) + '/file.txt' 279 | File.open(@file, 'wb') { |io| io.write('Hello World') } 280 | end 281 | 282 | after do 283 | File.unlink @file 284 | @file = nil 285 | end 286 | 287 | def send_file_app(opts={}) 288 | path = @file 289 | mock_app { 290 | get '/file.txt' do 291 | send_file path, opts 292 | end 293 | } 294 | end 295 | 296 | it "sends the contents of the file" do 297 | send_file_app 298 | get '/file.txt' 299 | assert ok? 300 | assert_equal 'Hello World', body 301 | end 302 | 303 | it 'sets the Content-Type response header if a mime-type can be located' do 304 | send_file_app 305 | get '/file.txt' 306 | assert_equal 'text/plain', response['Content-Type'] 307 | end 308 | 309 | it 'sets the Content-Length response header' do 310 | send_file_app 311 | get '/file.txt' 312 | assert_equal 'Hello World'.length.to_s, response['Content-Length'] 313 | end 314 | 315 | it 'sets the Last-Modified response header' do 316 | send_file_app 317 | get '/file.txt' 318 | assert_equal File.mtime(@file).httpdate, response['Last-Modified'] 319 | end 320 | 321 | it "returns a 404 when not found" do 322 | mock_app { 323 | get '/' do 324 | send_file 'this-file-does-not-exist.txt' 325 | end 326 | } 327 | get '/' 328 | assert not_found? 329 | end 330 | 331 | it "does not set the Content-Disposition header by default" do 332 | send_file_app 333 | get '/file.txt' 334 | assert_nil response['Content-Disposition'] 335 | end 336 | 337 | it "sets the Content-Disposition header when :disposition set to 'attachment'" do 338 | send_file_app :disposition => 'attachment' 339 | get '/file.txt' 340 | assert_equal 'attachment; filename="file.txt"', response['Content-Disposition'] 341 | end 342 | 343 | it "sets the Content-Disposition header when :filename provided" do 344 | send_file_app :filename => 'foo.txt' 345 | get '/file.txt' 346 | assert_equal 'attachment; filename="foo.txt"', response['Content-Disposition'] 347 | end 348 | end 349 | 350 | describe 'Helpers#last_modified' do 351 | before do 352 | now = Time.now 353 | mock_app { 354 | get '/' do 355 | body { 'Hello World' } 356 | last_modified now 357 | 'Boo!' 358 | end 359 | } 360 | @now = now 361 | end 362 | 363 | it 'sets the Last-Modified header to a valid RFC 2616 date value' do 364 | get '/' 365 | assert_equal @now.httpdate, response['Last-Modified'] 366 | end 367 | 368 | it 'returns a body when conditional get misses' do 369 | get '/' 370 | assert_equal 200, status 371 | assert_equal 'Boo!', body 372 | end 373 | 374 | it 'halts when a conditional GET matches' do 375 | get '/', :env => { 'HTTP_IF_MODIFIED_SINCE' => @now.httpdate } 376 | assert_equal 304, status 377 | assert_equal '', body 378 | end 379 | end 380 | 381 | describe 'Helpers#etag' do 382 | before do 383 | mock_app { 384 | get '/' do 385 | body { 'Hello World' } 386 | etag 'FOO' 387 | 'Boo!' 388 | end 389 | } 390 | end 391 | 392 | it 'sets the ETag header' do 393 | get '/' 394 | assert_equal '"FOO"', response['ETag'] 395 | end 396 | 397 | it 'returns a body when conditional get misses' do 398 | get '/' 399 | assert_equal 200, status 400 | assert_equal 'Boo!', body 401 | end 402 | 403 | it 'halts when a conditional GET matches' do 404 | get '/', :env => { 'HTTP_IF_NONE_MATCH' => '"FOO"' } 405 | assert_equal 304, status 406 | assert_equal '', body 407 | end 408 | 409 | it 'should handle multiple ETag values in If-None-Match header' do 410 | get '/', :env => { 'HTTP_IF_NONE_MATCH' => '"BAR", *' } 411 | assert_equal 304, status 412 | assert_equal '', body 413 | end 414 | 415 | it 'uses a weak etag with the :weak option' do 416 | mock_app { 417 | get '/' do 418 | etag 'FOO', :weak 419 | "that's weak, dude." 420 | end 421 | } 422 | get '/' 423 | assert_equal 'W/"FOO"', response['ETag'] 424 | end 425 | end 426 | 427 | describe 'Helpers#back' do 428 | it "makes redirecting back pretty" do 429 | mock_app { 430 | get '/foo' do 431 | redirect back 432 | end 433 | } 434 | 435 | get '/foo', {}, 'HTTP_REFERER' => 'http://github.com' 436 | assert redirect? 437 | assert_equal "http://github.com", response.location 438 | end 439 | end 440 | 441 | module HelperOne; def one; '1'; end; end 442 | module HelperTwo; def two; '2'; end; end 443 | 444 | describe 'Adding new helpers' do 445 | it 'takes a list of modules to mix into the app' do 446 | mock_app { 447 | helpers HelperOne, HelperTwo 448 | 449 | get '/one' do 450 | one 451 | end 452 | 453 | get '/two' do 454 | two 455 | end 456 | } 457 | 458 | get '/one' 459 | assert_equal '1', body 460 | 461 | get '/two' 462 | assert_equal '2', body 463 | end 464 | 465 | it 'takes a block to mix into the app' do 466 | mock_app { 467 | helpers do 468 | def foo 469 | 'foo' 470 | end 471 | end 472 | 473 | get '/' do 474 | foo 475 | end 476 | } 477 | 478 | get '/' 479 | assert_equal 'foo', body 480 | end 481 | 482 | it 'evaluates the block in class context so that methods can be aliased' do 483 | mock_app { 484 | helpers do 485 | alias_method :h, :escape_html 486 | end 487 | 488 | get '/' do 489 | h('42 < 43') 490 | end 491 | } 492 | 493 | get '/' 494 | assert ok? 495 | assert_equal '42 < 43', body 496 | end 497 | end 498 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Sinatra 2 | 3 | Sinatra is a DSL for quickly creating web-applications in Ruby with minimal 4 | effort: 5 | 6 | # myapp.rb 7 | require 'rubygems' 8 | require 'sinatra' 9 | get '/' do 10 | 'Hello world!' 11 | end 12 | 13 | Install the gem and run with: 14 | 15 | sudo gem install sinatra 16 | ruby myapp.rb 17 | 18 | View at: http://localhost:4567 19 | 20 | == Routes 21 | 22 | In Sinatra, a route is an HTTP method paired with an URL matching pattern. 23 | Each route is associated with a block: 24 | 25 | get '/' do 26 | .. show something .. 27 | end 28 | 29 | post '/' do 30 | .. create something .. 31 | end 32 | 33 | put '/' do 34 | .. update something .. 35 | end 36 | 37 | delete '/' do 38 | .. annihilate something .. 39 | end 40 | 41 | Routes are matched in the order they are defined. The first route that 42 | matches the request is invoked. 43 | 44 | Route patterns may include named parameters, accessible via the 45 | params hash: 46 | 47 | get '/hello/:name' do 48 | # matches "GET /foo" and "GET /bar" 49 | # params[:name] is 'foo' or 'bar' 50 | "Hello #{params[:name]}!" 51 | end 52 | 53 | You can also access named parameters via block parameters: 54 | 55 | get '/hello/:name' do |n| 56 | "Hello #{n}!" 57 | end 58 | 59 | Route patterns may also include splat (or wildcard) parameters, accessible 60 | via the params[:splat] array. 61 | 62 | get '/say/*/to/*' do 63 | # matches /say/hello/to/world 64 | params[:splat] # => ["hello", "world"] 65 | end 66 | 67 | get '/download/*.*' do 68 | # matches /download/path/to/file.xml 69 | params[:splat] # => ["path/to/file", "xml"] 70 | end 71 | 72 | Route matching with Regular Expressions: 73 | 74 | get %r{/hello/([\w]+)} do 75 | "Hello, #{params[:captures].first}!" 76 | end 77 | 78 | Or with a block parameter: 79 | 80 | get %r{/hello/([\w]+)} do |c| 81 | "Hello, #{c}!" 82 | end 83 | 84 | Routes may include a variety of matching conditions, such as the user agent: 85 | 86 | get '/foo', :agent => /Songbird (\d\.\d)[\d\/]*?/ do 87 | "You're using Songbird version #{params[:agent][0]}" 88 | end 89 | 90 | get '/foo' do 91 | # Matches non-songbird browsers 92 | end 93 | 94 | == Static Files 95 | 96 | Static files are served from the ./public directory. You can specify 97 | a different location by setting the :public option: 98 | 99 | set :public, File.dirname(__FILE__) + '/static' 100 | 101 | Note that the public directory name is not included in the URL. A file 102 | ./public/css/style.css is made available as 103 | http://example.com/css/style.css. 104 | 105 | == Views / Templates 106 | 107 | Templates are assumed to be located directly under the ./views 108 | directory. To use a different views directory: 109 | 110 | set :views, File.dirname(__FILE__) + '/templates' 111 | 112 | === Haml Templates 113 | 114 | The haml gem/library is required to render HAML templates: 115 | 116 | get '/' do 117 | haml :index 118 | end 119 | 120 | Renders ./views/index.haml. 121 | 122 | === Erb Templates 123 | 124 | get '/' do 125 | erb :index 126 | end 127 | 128 | Renders ./views/index.erb 129 | 130 | === Builder Templates 131 | 132 | The builder gem/library is required to render builder templates: 133 | 134 | get '/' do 135 | content_type 'application/xml', :charset => 'utf-8' 136 | builder :index 137 | end 138 | 139 | Renders ./views/index.builder. 140 | 141 | === Sass Templates 142 | 143 | The sass gem/library is required to render Sass templates: 144 | 145 | get '/stylesheet.css' do 146 | content_type 'text/css', :charset => 'utf-8' 147 | sass :stylesheet 148 | end 149 | 150 | Renders ./views/stylesheet.sass. 151 | 152 | === Inline Templates 153 | 154 | get '/' do 155 | haml '%div.title Hello World' 156 | end 157 | 158 | Renders the inlined template string. 159 | 160 | === Accessing Variables in Templates 161 | 162 | Templates are evaluated within the same context as route handlers. Instance 163 | variables set in route handlers are direcly accessible by templates: 164 | 165 | get '/:id' do 166 | @foo = Foo.find(params[:id]) 167 | haml '%h1= @foo.name' 168 | end 169 | 170 | Or, specify an explicit Hash of local variables: 171 | 172 | get '/:id' do 173 | foo = Foo.find(params[:id]) 174 | haml '%h1= foo.name', :locals => { :foo => foo } 175 | end 176 | 177 | This is typically used when rendering templates as partials from within 178 | other templates. 179 | 180 | === In-file Templates 181 | 182 | Templates may be defined at the end of the source file: 183 | 184 | require 'rubygems' 185 | require 'sinatra' 186 | 187 | get '/' do 188 | haml :index 189 | end 190 | 191 | __END__ 192 | 193 | @@ layout 194 | %html 195 | = yield 196 | 197 | @@ index 198 | %div.title Hello world!!!!! 199 | 200 | NOTE: In-file templates defined in the source file that requires sinatra 201 | are automatically loaded. Call the use_in_file_templates! 202 | method explicitly if you have in-file templates in other source files. 203 | 204 | === Named Templates 205 | 206 | Templates may also be defined using the top-level template method: 207 | 208 | template :layout do 209 | "%html\n =yield\n" 210 | end 211 | 212 | template :index do 213 | '%div.title Hello World!' 214 | end 215 | 216 | get '/' do 217 | haml :index 218 | end 219 | 220 | If a template named "layout" exists, it will be used each time a template 221 | is rendered. You can disable layouts by passing :layout => false. 222 | 223 | get '/' do 224 | haml :index, :layout => !request.xhr? 225 | end 226 | 227 | == Helpers 228 | 229 | Use the top-level helpers method to define helper methods for use in 230 | route handlers and templates: 231 | 232 | helpers do 233 | def bar(name) 234 | "#{name}bar" 235 | end 236 | end 237 | 238 | get '/:name' do 239 | bar(params[:name]) 240 | end 241 | 242 | == Filters 243 | 244 | Before filters are evaluated before each request within the context of the 245 | request and can modify the request and response. Instance variables set in 246 | filters are accessible by routes and templates: 247 | 248 | before do 249 | @note = 'Hi!' 250 | request.path_info = '/foo/bar/baz' 251 | end 252 | 253 | get '/foo/*' do 254 | @note #=> 'Hi!' 255 | params[:splat] #=> 'bar/baz' 256 | end 257 | 258 | == Halting 259 | 260 | To immediately stop a request during a before filter or route use: 261 | 262 | halt 263 | 264 | You can also specify a body when halting ... 265 | 266 | halt 'this will be the body' 267 | 268 | Or set the status and body ... 269 | 270 | halt 401, 'go away!' 271 | 272 | == Passing 273 | 274 | A route can punt processing to the next matching route using pass: 275 | 276 | get '/guess/:who' do 277 | pass unless params[:who] == 'Frank' 278 | "You got me!" 279 | end 280 | 281 | get '/guess/*' do 282 | "You missed!" 283 | end 284 | 285 | The route block is immediately exited and control continues with the next 286 | matching route. If no matching route is found, a 404 is returned. 287 | 288 | == Configuration and Reloading 289 | 290 | Sinatra supports multiple environments and reloading. Reloading happens 291 | before each request when running under the :development 292 | environment. Wrap your configurations (e.g., database connections, constants, 293 | etc.) in configure blocks to protect them from reloading or to 294 | target specific environments. 295 | 296 | Run once, at startup, in any environment: 297 | 298 | configure do 299 | ... 300 | end 301 | 302 | Run only when the environment (RACK_ENV environment variable) is set to 303 | :production. 304 | 305 | configure :production do 306 | ... 307 | end 308 | 309 | Run when the environment (RACK_ENV environment variable) is set to 310 | either :production or :test. 311 | 312 | configure :production, :test do 313 | ... 314 | end 315 | 316 | == Error handling 317 | 318 | Error handlers run within the same context as routes and before filters, which 319 | means you get all the goodies it has to offer, like haml, erb, 320 | halt, etc. 321 | 322 | === Not Found 323 | 324 | When a Sinatra::NotFound exception is raised, or the response's status 325 | code is 404, the not_found handler is invoked: 326 | 327 | not_found do 328 | 'This is nowhere to be found' 329 | end 330 | 331 | === Error 332 | 333 | The +error+ handler is invoked any time an exception is raised from a route 334 | block or before filter. The exception object can be obtained from the 335 | sinatra.error Rack variable: 336 | 337 | error do 338 | 'Sorry there was a nasty error - ' + env['sinatra.error'].name 339 | end 340 | 341 | Custom errors: 342 | 343 | error MyCustomError do 344 | 'So what happened was...' + request.env['sinatra.error'].message 345 | end 346 | 347 | Then, if this happens: 348 | 349 | get '/' do 350 | raise MyCustomError, 'something bad' 351 | end 352 | 353 | You get this: 354 | 355 | So what happened was... something bad 356 | 357 | Sinatra installs special not_found and error handlers when 358 | running under the development environment. 359 | 360 | == Mime types 361 | 362 | When using send_file or static files you may have mime types Sinatra 363 | doesn't understand. Use +mime+ to register them by file extension: 364 | 365 | mime :foo, 'text/foo' 366 | 367 | == Rack Middleware 368 | 369 | Sinatra rides on Rack[http://rack.rubyforge.org/], a minimal standard 370 | interface for Ruby web frameworks. One of Rack's most interesting capabilities 371 | for application developers is support for "middleware" -- components that sit 372 | between the server and your application monitoring and/or manipulating the 373 | HTTP request/response to provide various types of common functionality. 374 | 375 | Sinatra makes building Rack middleware pipelines a cinch via a top-level 376 | +use+ method: 377 | 378 | require 'sinatra' 379 | require 'my_custom_middleware' 380 | 381 | use Rack::Lint 382 | use MyCustomMiddleware 383 | 384 | get '/hello' do 385 | 'Hello World' 386 | end 387 | 388 | The semantics of +use+ are identical to those defined for the 389 | Rack::Builder[http://rack.rubyforge.org/doc/classes/Rack/Builder.html] DSL 390 | (most frequently used from rackup files). For example, the +use+ method 391 | accepts multiple/variable args as well as blocks: 392 | 393 | use Rack::Auth::Basic do |username, password| 394 | username == 'admin' && password == 'secret' 395 | end 396 | 397 | Rack is distributed with a variety of standard middleware for logging, 398 | debugging, URL routing, authentication, and session handling. Sinatra uses 399 | many of of these components automatically based on configuration so you 400 | typically don't have to +use+ them explicitly. 401 | 402 | == Testing 403 | 404 | The Sinatra::Test mixin and Sinatra::TestHarness class include a variety of 405 | helper methods for testing your Sinatra app: 406 | 407 | require 'my_sinatra_app' 408 | require 'test/unit' 409 | require 'sinatra/test' 410 | 411 | class MyAppTest < Test::Unit::TestCase 412 | include Sinatra::Test 413 | 414 | def test_my_default 415 | get '/' 416 | assert_equal 'Hello World!', @response.body 417 | end 418 | 419 | def test_with_params 420 | get '/meet', {:name => 'Frank'} 421 | assert_equal 'Hello Frank!', @response.body 422 | end 423 | 424 | def test_with_rack_env 425 | get '/', {}, :agent => 'Songbird' 426 | assert_equal "You're using Songbird!", @response.body 427 | end 428 | end 429 | 430 | See http://www.sinatrarb.com/testing.html for more on Sinatra::Test and using it 431 | with other test frameworks such as RSpec, Bacon, and test/spec. 432 | 433 | == Command line 434 | 435 | Sinatra applications can be run directly: 436 | 437 | ruby myapp.rb [-h] [-x] [-e ENVIRONMENT] [-p PORT] [-s HANDLER] 438 | 439 | Options are: 440 | 441 | -h # help 442 | -p # set the port (default is 4567) 443 | -e # set the environment (default is development) 444 | -s # specify rack server/handler (default is thin) 445 | -x # turn on the mutex lock (default is off) 446 | 447 | == The Bleeding Edge 448 | 449 | If you would like to use Sinatra's latest bleeding code, create a local 450 | clone and run your app with the sinatra/lib directory on the 451 | LOAD_PATH: 452 | 453 | cd myapp 454 | git clone git://github.com/sinatra/sinatra.git 455 | ruby -Isinatra/lib myapp.rb 456 | 457 | Alternatively, you can add the sinatra/lib directory to the 458 | LOAD_PATH in your application: 459 | 460 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/sinatra/lib' 461 | require 'rubygems' 462 | require 'sinatra' 463 | 464 | get '/about' do 465 | "I'm running version " + Sinatra::VERSION 466 | end 467 | 468 | To update the Sinatra sources in the future: 469 | 470 | cd myproject/sinatra 471 | git pull 472 | 473 | == More 474 | 475 | * {Project Website}[http://sinatra.github.com/] - Additional documentation, 476 | news, and links to other resources. 477 | * {Contributing}[http://sinatra.github.com/contributing.html] - Find a bug? Need 478 | help? Have a patch? 479 | * {Lighthouse}[http://sinatra.lighthouseapp.com] - Issue tracking and release 480 | planning. 481 | * {Twitter}[http://twitter.com/sinatra] 482 | * {Mailing List}[http://groups.google.com/group/sinatrarb] 483 | * {IRC: #sinatra}[irc://chat.freenode.net/#sinatra] on http://freenode.net 484 | -------------------------------------------------------------------------------- /test/routing_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | # Helper method for easy route pattern matching testing 4 | def route_def(pattern) 5 | mock_app { get(pattern) { } } 6 | end 7 | 8 | describe "Routing" do 9 | %w[get put post delete].each do |verb| 10 | it "defines #{verb.upcase} request handlers with #{verb}" do 11 | mock_app { 12 | send verb, '/hello' do 13 | 'Hello World' 14 | end 15 | } 16 | 17 | request = Rack::MockRequest.new(@app) 18 | response = request.request(verb.upcase, '/hello', {}) 19 | assert response.ok? 20 | assert_equal 'Hello World', response.body 21 | end 22 | end 23 | 24 | it "defines HEAD request handlers with HEAD" do 25 | mock_app { 26 | head '/hello' do 27 | response['X-Hello'] = 'World!' 28 | 'remove me' 29 | end 30 | } 31 | 32 | request = Rack::MockRequest.new(@app) 33 | response = request.request('HEAD', '/hello', {}) 34 | assert response.ok? 35 | assert_equal 'World!', response['X-Hello'] 36 | assert_equal '', response.body 37 | end 38 | 39 | it "404s when no route satisfies the request" do 40 | mock_app { 41 | get('/foo') { } 42 | } 43 | get '/bar' 44 | assert_equal 404, status 45 | end 46 | 47 | it 'takes multiple definitions of a route' do 48 | mock_app { 49 | user_agent(/Foo/) 50 | get '/foo' do 51 | 'foo' 52 | end 53 | 54 | get '/foo' do 55 | 'not foo' 56 | end 57 | } 58 | 59 | get '/foo', {}, 'HTTP_USER_AGENT' => 'Foo' 60 | assert ok? 61 | assert_equal 'foo', body 62 | 63 | get '/foo' 64 | assert ok? 65 | assert_equal 'not foo', body 66 | end 67 | 68 | it "exposes params with indifferent hash" do 69 | mock_app { 70 | get '/:foo' do 71 | assert_equal 'bar', params['foo'] 72 | assert_equal 'bar', params[:foo] 73 | 'well, alright' 74 | end 75 | } 76 | get '/bar' 77 | assert_equal 'well, alright', body 78 | end 79 | 80 | it "merges named params and query string params in params" do 81 | mock_app { 82 | get '/:foo' do 83 | assert_equal 'bar', params['foo'] 84 | assert_equal 'biz', params['baz'] 85 | end 86 | } 87 | get '/bar?baz=biz' 88 | assert ok? 89 | end 90 | 91 | it "supports named params like /hello/:person" do 92 | mock_app { 93 | get '/hello/:person' do 94 | "Hello #{params['person']}" 95 | end 96 | } 97 | get '/hello/Frank' 98 | assert_equal 'Hello Frank', body 99 | end 100 | 101 | it "supports optional named params like /?:foo?/?:bar?" do 102 | mock_app { 103 | get '/?:foo?/?:bar?' do 104 | "foo=#{params[:foo]};bar=#{params[:bar]}" 105 | end 106 | } 107 | 108 | get '/hello/world' 109 | assert ok? 110 | assert_equal "foo=hello;bar=world", body 111 | 112 | get '/hello' 113 | assert ok? 114 | assert_equal "foo=hello;bar=", body 115 | 116 | get '/' 117 | assert ok? 118 | assert_equal "foo=;bar=", body 119 | end 120 | 121 | it "supports single splat params like /*" do 122 | mock_app { 123 | get '/*' do 124 | assert params['splat'].kind_of?(Array) 125 | params['splat'].join "\n" 126 | end 127 | } 128 | 129 | get '/foo' 130 | assert_equal "foo", body 131 | 132 | get '/foo/bar/baz' 133 | assert_equal "foo/bar/baz", body 134 | end 135 | 136 | it "supports mixing multiple splat params like /*/foo/*/*" do 137 | mock_app { 138 | get '/*/foo/*/*' do 139 | assert params['splat'].kind_of?(Array) 140 | params['splat'].join "\n" 141 | end 142 | } 143 | 144 | get '/bar/foo/bling/baz/boom' 145 | assert_equal "bar\nbling\nbaz/boom", body 146 | 147 | get '/bar/foo/baz' 148 | assert not_found? 149 | end 150 | 151 | it "supports mixing named and splat params like /:foo/*" do 152 | mock_app { 153 | get '/:foo/*' do 154 | assert_equal 'foo', params['foo'] 155 | assert_equal ['bar/baz'], params['splat'] 156 | end 157 | } 158 | 159 | get '/foo/bar/baz' 160 | assert ok? 161 | end 162 | 163 | it "matches a dot ('.') as part of a named param" do 164 | mock_app { 165 | get '/:foo/:bar' do 166 | params[:foo] 167 | end 168 | } 169 | 170 | get '/user@example.com/name' 171 | assert_equal 200, response.status 172 | assert_equal 'user@example.com', body 173 | end 174 | 175 | it "matches a literal dot ('.') outside of named params" do 176 | mock_app { 177 | get '/:file.:ext' do 178 | assert_equal 'pony', params[:file] 179 | assert_equal 'jpg', params[:ext] 180 | 'right on' 181 | end 182 | } 183 | 184 | get '/pony.jpg' 185 | assert_equal 200, response.status 186 | assert_equal 'right on', body 187 | end 188 | 189 | it "literally matches . in paths" do 190 | route_def '/test.bar' 191 | 192 | get '/test.bar' 193 | assert ok? 194 | get 'test0bar' 195 | assert not_found? 196 | end 197 | 198 | it "literally matches $ in paths" do 199 | route_def '/test$/' 200 | 201 | get '/test$/' 202 | assert ok? 203 | end 204 | 205 | it "literally matches + in paths" do 206 | route_def '/te+st/' 207 | 208 | get '/te%2Bst/' 209 | assert ok? 210 | get '/teeeeeeest/' 211 | assert not_found? 212 | end 213 | 214 | it "literally matches () in paths" do 215 | route_def '/test(bar)/' 216 | 217 | get '/test(bar)/' 218 | assert ok? 219 | end 220 | 221 | it "supports basic nested params" do 222 | mock_app { 223 | get '/hi' do 224 | params["person"]["name"] 225 | end 226 | } 227 | 228 | get "/hi?person[name]=John+Doe" 229 | assert ok? 230 | assert_equal "John Doe", body 231 | end 232 | 233 | it "exposes nested params with indifferent hash" do 234 | mock_app { 235 | get '/testme' do 236 | assert_equal 'baz', params['bar']['foo'] 237 | assert_equal 'baz', params['bar'][:foo] 238 | 'well, alright' 239 | end 240 | } 241 | get '/testme?bar[foo]=baz' 242 | assert_equal 'well, alright', body 243 | end 244 | 245 | it "supports deeply nested params" do 246 | input = { 247 | 'browser[chrome][engine][name]' => 'V8', 248 | 'browser[chrome][engine][version]' => '1.0', 249 | 'browser[firefox][engine][name]' => 'spidermonkey', 250 | 'browser[firefox][engine][version]' => '1.7.0', 251 | 'emacs[map][goto-line]' => 'M-g g', 252 | 'emacs[version]' => '22.3.1', 253 | 'paste[name]' => 'hello world', 254 | 'paste[syntax]' => 'ruby' 255 | } 256 | expected = { 257 | "emacs" => { 258 | "map" => { "goto-line" => "M-g g" }, 259 | "version" => "22.3.1" 260 | }, 261 | "browser" => { 262 | "firefox" => {"engine" => {"name"=>"spidermonkey", "version"=>"1.7.0"}}, 263 | "chrome" => {"engine" => {"name"=>"V8", "version"=>"1.0"}} 264 | }, 265 | "paste" => {"name"=>"hello world", "syntax"=>"ruby"} 266 | } 267 | mock_app { 268 | get '/foo' do 269 | assert_equal expected, params 270 | 'looks good' 271 | end 272 | } 273 | get "/foo?#{build_query(input)}" 274 | assert ok? 275 | assert_equal 'looks good', body 276 | end 277 | 278 | it "preserves non-nested params" do 279 | mock_app { 280 | get '/foo' do 281 | assert_equal "2", params["article_id"] 282 | assert_equal "awesome", params['comment']['body'] 283 | assert_nil params['comment[body]'] 284 | 'looks good' 285 | end 286 | } 287 | 288 | get '/foo?article_id=2&comment[body]=awesome' 289 | assert ok? 290 | assert_equal 'looks good', body 291 | end 292 | 293 | it "matches paths that include spaces encoded with %20" do 294 | mock_app { 295 | get '/path with spaces' do 296 | 'looks good' 297 | end 298 | } 299 | 300 | get '/path%20with%20spaces' 301 | assert ok? 302 | assert_equal 'looks good', body 303 | end 304 | 305 | it "matches paths that include spaces encoded with +" do 306 | mock_app { 307 | get '/path with spaces' do 308 | 'looks good' 309 | end 310 | } 311 | 312 | get '/path+with+spaces' 313 | assert ok? 314 | assert_equal 'looks good', body 315 | end 316 | 317 | it "URL decodes named parameters and splats" do 318 | mock_app { 319 | get '/:foo/*' do 320 | assert_equal 'hello world', params['foo'] 321 | assert_equal ['how are you'], params['splat'] 322 | nil 323 | end 324 | } 325 | 326 | get '/hello%20world/how%20are%20you' 327 | assert ok? 328 | end 329 | 330 | it 'supports regular expressions' do 331 | mock_app { 332 | get(/^\/foo...\/bar$/) do 333 | 'Hello World' 334 | end 335 | } 336 | 337 | get '/foooom/bar' 338 | assert ok? 339 | assert_equal 'Hello World', body 340 | end 341 | 342 | it 'makes regular expression captures available in params[:captures]' do 343 | mock_app { 344 | get(/^\/fo(.*)\/ba(.*)/) do 345 | assert_equal ['orooomma', 'f'], params[:captures] 346 | 'right on' 347 | end 348 | } 349 | 350 | get '/foorooomma/baf' 351 | assert ok? 352 | assert_equal 'right on', body 353 | end 354 | 355 | it 'raises a TypeError when pattern is not a String or Regexp' do 356 | @app = mock_app 357 | assert_raise(TypeError) { @app.get(42){} } 358 | end 359 | 360 | it "returns response immediately on halt" do 361 | mock_app { 362 | get '/' do 363 | halt 'Hello World' 364 | 'Boo-hoo World' 365 | end 366 | } 367 | 368 | get '/' 369 | assert ok? 370 | assert_equal 'Hello World', body 371 | end 372 | 373 | it "halts with a response tuple" do 374 | mock_app { 375 | get '/' do 376 | halt 295, {'Content-Type' => 'text/plain'}, 'Hello World' 377 | end 378 | } 379 | 380 | get '/' 381 | assert_equal 295, status 382 | assert_equal 'text/plain', response['Content-Type'] 383 | assert_equal 'Hello World', body 384 | end 385 | 386 | it "halts with an array of strings" do 387 | mock_app { 388 | get '/' do 389 | halt %w[Hello World How Are You] 390 | end 391 | } 392 | 393 | get '/' 394 | assert_equal 'HelloWorldHowAreYou', body 395 | end 396 | 397 | it "transitions to the next matching route on pass" do 398 | mock_app { 399 | get '/:foo' do 400 | pass 401 | 'Hello Foo' 402 | end 403 | 404 | get '/*' do 405 | assert !params.include?('foo') 406 | 'Hello World' 407 | end 408 | } 409 | 410 | get '/bar' 411 | assert ok? 412 | assert_equal 'Hello World', body 413 | end 414 | 415 | it "transitions to 404 when passed and no subsequent route matches" do 416 | mock_app { 417 | get '/:foo' do 418 | pass 419 | 'Hello Foo' 420 | end 421 | } 422 | 423 | get '/bar' 424 | assert not_found? 425 | end 426 | 427 | it "passes when matching condition returns false" do 428 | mock_app { 429 | condition { params[:foo] == 'bar' } 430 | get '/:foo' do 431 | 'Hello World' 432 | end 433 | } 434 | 435 | get '/bar' 436 | assert ok? 437 | assert_equal 'Hello World', body 438 | 439 | get '/foo' 440 | assert not_found? 441 | end 442 | 443 | it "does not pass when matching condition returns nil" do 444 | mock_app { 445 | condition { nil } 446 | get '/:foo' do 447 | 'Hello World' 448 | end 449 | } 450 | 451 | get '/bar' 452 | assert ok? 453 | assert_equal 'Hello World', body 454 | end 455 | 456 | it "passes to next route when condition calls pass explicitly" do 457 | mock_app { 458 | condition { pass unless params[:foo] == 'bar' } 459 | get '/:foo' do 460 | 'Hello World' 461 | end 462 | } 463 | 464 | get '/bar' 465 | assert ok? 466 | assert_equal 'Hello World', body 467 | 468 | get '/foo' 469 | assert not_found? 470 | end 471 | 472 | it "passes to the next route when host_name does not match" do 473 | mock_app { 474 | host_name 'example.com' 475 | get '/foo' do 476 | 'Hello World' 477 | end 478 | } 479 | get '/foo' 480 | assert not_found? 481 | 482 | get '/foo', :env => { 'HTTP_HOST' => 'example.com' } 483 | assert_equal 200, status 484 | assert_equal 'Hello World', body 485 | end 486 | 487 | it "passes to the next route when user_agent does not match" do 488 | mock_app { 489 | user_agent(/Foo/) 490 | get '/foo' do 491 | 'Hello World' 492 | end 493 | } 494 | get '/foo' 495 | assert not_found? 496 | 497 | get '/foo', :env => { 'HTTP_USER_AGENT' => 'Foo Bar' } 498 | assert_equal 200, status 499 | assert_equal 'Hello World', body 500 | end 501 | 502 | it "makes captures in user agent pattern available in params[:agent]" do 503 | mock_app { 504 | user_agent(/Foo (.*)/) 505 | get '/foo' do 506 | 'Hello ' + params[:agent].first 507 | end 508 | } 509 | get '/foo', :env => { 'HTTP_USER_AGENT' => 'Foo Bar' } 510 | assert_equal 200, status 511 | assert_equal 'Hello Bar', body 512 | end 513 | 514 | it "filters by accept header" do 515 | mock_app { 516 | get '/', :provides => :xml do 517 | request.env['HTTP_ACCEPT'] 518 | end 519 | } 520 | 521 | get '/', :env => { :accept => 'application/xml' } 522 | assert ok? 523 | assert_equal 'application/xml', body 524 | assert_equal 'application/xml', response.headers['Content-Type'] 525 | 526 | get '/', :env => { :accept => 'text/html' } 527 | assert !ok? 528 | end 529 | 530 | it "allows multiple mime types for accept header" do 531 | types = ['image/jpeg', 'image/pjpeg'] 532 | 533 | mock_app { 534 | get '/', :provides => types do 535 | request.env['HTTP_ACCEPT'] 536 | end 537 | } 538 | 539 | types.each do |type| 540 | get '/', :env => { :accept => type } 541 | assert ok? 542 | assert_equal type, body 543 | assert_equal type, response.headers['Content-Type'] 544 | end 545 | end 546 | 547 | it 'degrades gracefully when optional accept header is not provided' do 548 | mock_app { 549 | get '/', :provides => :xml do 550 | request.env['HTTP_ACCEPT'] 551 | end 552 | get '/' do 553 | 'default' 554 | end 555 | } 556 | get '/' 557 | assert ok? 558 | assert_equal 'default', body 559 | end 560 | 561 | it 'passes a single url param as block parameters when one param is specified' do 562 | mock_app { 563 | get '/:foo' do |foo| 564 | assert_equal 'bar', foo 565 | end 566 | } 567 | 568 | get '/bar' 569 | assert ok? 570 | end 571 | 572 | it 'passes multiple params as block parameters when many are specified' do 573 | mock_app { 574 | get '/:foo/:bar/:baz' do |foo, bar, baz| 575 | assert_equal 'abc', foo 576 | assert_equal 'def', bar 577 | assert_equal 'ghi', baz 578 | end 579 | } 580 | 581 | get '/abc/def/ghi' 582 | assert ok? 583 | end 584 | 585 | it 'passes regular expression captures as block parameters' do 586 | mock_app { 587 | get(/^\/fo(.*)\/ba(.*)/) do |foo, bar| 588 | assert_equal 'orooomma', foo 589 | assert_equal 'f', bar 590 | 'looks good' 591 | end 592 | } 593 | 594 | get '/foorooomma/baf' 595 | assert ok? 596 | assert_equal 'looks good', body 597 | end 598 | 599 | it "supports mixing multiple splat params like /*/foo/*/* as block parameters" do 600 | mock_app { 601 | get '/*/foo/*/*' do |foo, bar, baz| 602 | assert_equal 'bar', foo 603 | assert_equal 'bling', bar 604 | assert_equal 'baz/boom', baz 605 | 'looks good' 606 | end 607 | } 608 | 609 | get '/bar/foo/bling/baz/boom' 610 | assert ok? 611 | assert_equal 'looks good', body 612 | end 613 | 614 | it 'raises an ArgumentError with block arity > 1 and too many values' do 615 | mock_app { 616 | get '/:foo/:bar/:baz' do |foo, bar| 617 | 'quux' 618 | end 619 | } 620 | 621 | assert_raise(ArgumentError) { get '/a/b/c' } 622 | end 623 | 624 | it 'raises an ArgumentError with block param arity > 1 and too few values' do 625 | mock_app { 626 | get '/:foo/:bar' do |foo, bar, baz| 627 | 'quux' 628 | end 629 | } 630 | 631 | assert_raise(ArgumentError) { get '/a/b' } 632 | end 633 | 634 | it 'succeeds if no block parameters are specified' do 635 | mock_app { 636 | get '/:foo/:bar' do 637 | 'quux' 638 | end 639 | } 640 | 641 | get '/a/b' 642 | assert ok? 643 | assert_equal 'quux', body 644 | end 645 | 646 | it 'passes all params with block param arity -1 (splat args)' do 647 | mock_app { 648 | get '/:foo/:bar' do |*args| 649 | args.join 650 | end 651 | } 652 | 653 | get '/a/b' 654 | assert ok? 655 | assert_equal 'ab', body 656 | end 657 | 658 | # NOTE Block params behaves differently under 1.8 and 1.9. Under 1.8, block 659 | # param arity is lax: declaring a mismatched number of block params results 660 | # in a warning. Under 1.9, block param arity is strict: mismatched block 661 | # arity raises an ArgumentError. 662 | 663 | if RUBY_VERSION >= '1.9' 664 | 665 | it 'raises an ArgumentError with block param arity 1 and no values' do 666 | mock_app { 667 | get '/foo' do |foo| 668 | 'quux' 669 | end 670 | } 671 | 672 | assert_raise(ArgumentError) { get '/foo' } 673 | end 674 | 675 | it 'raises an ArgumentError with block param arity 1 and too many values' do 676 | mock_app { 677 | get '/:foo/:bar/:baz' do |foo| 678 | 'quux' 679 | end 680 | } 681 | 682 | assert_raise(ArgumentError) { get '/a/b/c' } 683 | end 684 | 685 | else 686 | 687 | it 'does not raise an ArgumentError with block param arity 1 and no values' do 688 | mock_app { 689 | get '/foo' do |foo| 690 | 'quux' 691 | end 692 | } 693 | 694 | silence_warnings { get '/foo' } 695 | assert ok? 696 | assert_equal 'quux', body 697 | end 698 | 699 | it 'does not raise an ArgumentError with block param arity 1 and too many values' do 700 | mock_app { 701 | get '/:foo/:bar/:baz' do |foo| 702 | 'quux' 703 | end 704 | } 705 | 706 | silence_warnings { get '/a/b/c' } 707 | assert ok? 708 | assert_equal 'quux', body 709 | end 710 | 711 | end 712 | end 713 | -------------------------------------------------------------------------------- /lib/sinatra/base.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'time' 3 | require 'uri' 4 | require 'rack' 5 | require 'rack/builder' 6 | 7 | module Sinatra 8 | VERSION = '0.9.1.1' 9 | 10 | # The request object. See Rack::Request for more info: 11 | # http://rack.rubyforge.org/doc/classes/Rack/Request.html 12 | class Request < Rack::Request 13 | def user_agent 14 | @env['HTTP_USER_AGENT'] 15 | end 16 | 17 | def accept 18 | @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip } 19 | end 20 | 21 | # Override Rack 0.9.x's #params implementation (see #72 in lighthouse) 22 | def params 23 | self.GET.update(self.POST) 24 | rescue EOFError => boom 25 | self.GET 26 | end 27 | end 28 | 29 | # The response object. See Rack::Response and Rack::ResponseHelpers for 30 | # more info: 31 | # http://rack.rubyforge.org/doc/classes/Rack/Response.html 32 | # http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html 33 | class Response < Rack::Response 34 | def initialize 35 | @status, @body = 200, [] 36 | @header = Rack::Utils::HeaderHash.new({'Content-Type' => 'text/html'}) 37 | end 38 | 39 | def write(str) 40 | @body << str.to_s 41 | str 42 | end 43 | 44 | def finish 45 | @body = block if block_given? 46 | if [204, 304].include?(status.to_i) 47 | header.delete "Content-Type" 48 | [status.to_i, header.to_hash, []] 49 | else 50 | body = @body || [] 51 | body = [body] if body.respond_to? :to_str 52 | if body.respond_to?(:to_ary) 53 | header["Content-Length"] = body.to_ary. 54 | inject(0) { |len, part| len + part.bytesize }.to_s 55 | end 56 | [status.to_i, header.to_hash, body] 57 | end 58 | end 59 | end 60 | 61 | class NotFound < NameError #:nodoc: 62 | def code ; 404 ; end 63 | end 64 | 65 | # Methods available to routes, before filters, and views. 66 | module Helpers 67 | # Set or retrieve the response status code. 68 | def status(value=nil) 69 | response.status = value if value 70 | response.status 71 | end 72 | 73 | # Set or retrieve the response body. When a block is given, 74 | # evaluation is deferred until the body is read with #each. 75 | def body(value=nil, &block) 76 | if block_given? 77 | def block.each ; yield call ; end 78 | response.body = block 79 | else 80 | response.body = value 81 | end 82 | end 83 | 84 | # Halt processing and redirect to the URI provided. 85 | def redirect(uri, *args) 86 | status 302 87 | response['Location'] = uri 88 | halt(*args) 89 | end 90 | 91 | # Halt processing and return the error status provided. 92 | def error(code, body=nil) 93 | code, body = 500, code.to_str if code.respond_to? :to_str 94 | response.body = body unless body.nil? 95 | halt code 96 | end 97 | 98 | # Halt processing and return a 404 Not Found. 99 | def not_found(body=nil) 100 | error 404, body 101 | end 102 | 103 | # Set multiple response headers with Hash. 104 | def headers(hash=nil) 105 | response.headers.merge! hash if hash 106 | response.headers 107 | end 108 | 109 | # Access the underlying Rack session. 110 | def session 111 | env['rack.session'] ||= {} 112 | end 113 | 114 | # Look up a media type by file extension in Rack's mime registry. 115 | def media_type(type) 116 | Base.media_type(type) 117 | end 118 | 119 | # Set the Content-Type of the response body given a media type or file 120 | # extension. 121 | def content_type(type, params={}) 122 | media_type = self.media_type(type) 123 | fail "Unknown media type: %p" % type if media_type.nil? 124 | if params.any? 125 | params = params.collect { |kv| "%s=%s" % kv }.join(', ') 126 | response['Content-Type'] = [media_type, params].join(";") 127 | else 128 | response['Content-Type'] = media_type 129 | end 130 | end 131 | 132 | # Set the Content-Disposition to "attachment" with the specified filename, 133 | # instructing the user agents to prompt to save. 134 | def attachment(filename=nil) 135 | response['Content-Disposition'] = 'attachment' 136 | if filename 137 | params = '; filename="%s"' % File.basename(filename) 138 | response['Content-Disposition'] << params 139 | end 140 | end 141 | 142 | # Use the contents of the file at +path+ as the response body. 143 | def send_file(path, opts={}) 144 | stat = File.stat(path) 145 | last_modified stat.mtime 146 | 147 | content_type media_type(opts[:type]) || 148 | media_type(File.extname(path)) || 149 | response['Content-Type'] || 150 | 'application/octet-stream' 151 | 152 | response['Content-Length'] ||= (opts[:length] || stat.size).to_s 153 | 154 | if opts[:disposition] == 'attachment' || opts[:filename] 155 | attachment opts[:filename] || path 156 | elsif opts[:disposition] == 'inline' 157 | response['Content-Disposition'] = 'inline' 158 | end 159 | 160 | halt StaticFile.open(path, 'rb') 161 | rescue Errno::ENOENT 162 | not_found 163 | end 164 | 165 | class StaticFile < ::File #:nodoc: 166 | alias_method :to_path, :path 167 | def each 168 | rewind 169 | while buf = read(8192) 170 | yield buf 171 | end 172 | end 173 | end 174 | 175 | # Set the last modified time of the resource (HTTP 'Last-Modified' header) 176 | # and halt if conditional GET matches. The +time+ argument is a Time, 177 | # DateTime, or other object that responds to +to_time+. 178 | # 179 | # When the current request includes an 'If-Modified-Since' header that 180 | # matches the time specified, execution is immediately halted with a 181 | # '304 Not Modified' response. 182 | def last_modified(time) 183 | time = time.to_time if time.respond_to?(:to_time) 184 | time = time.httpdate if time.respond_to?(:httpdate) 185 | response['Last-Modified'] = time 186 | halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE'] 187 | time 188 | end 189 | 190 | # Set the response entity tag (HTTP 'ETag' header) and halt if conditional 191 | # GET matches. The +value+ argument is an identifier that uniquely 192 | # identifies the current version of the resource. The +strength+ argument 193 | # indicates whether the etag should be used as a :strong (default) or :weak 194 | # cache validator. 195 | # 196 | # When the current request includes an 'If-None-Match' header with a 197 | # matching etag, execution is immediately halted. If the request method is 198 | # GET or HEAD, a '304 Not Modified' response is sent. 199 | def etag(value, kind=:strong) 200 | raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind) 201 | value = '"%s"' % value 202 | value = 'W/' + value if kind == :weak 203 | response['ETag'] = value 204 | 205 | # Conditional GET check 206 | if etags = env['HTTP_IF_NONE_MATCH'] 207 | etags = etags.split(/\s*,\s*/) 208 | halt 304 if etags.include?(value) || etags.include?('*') 209 | end 210 | end 211 | 212 | ## Sugar for redirect (example: redirect back) 213 | def back ; request.referer ; end 214 | 215 | end 216 | 217 | # Template rendering methods. Each method takes a the name of a template 218 | # to render as a Symbol and returns a String with the rendered output. 219 | module Templates 220 | def erb(template, options={}) 221 | require 'erb' unless defined? ::ERB 222 | render :erb, template, options 223 | end 224 | 225 | def haml(template, options={}) 226 | require 'haml' unless defined? ::Haml 227 | options[:options] ||= self.class.haml if self.class.respond_to? :haml 228 | render :haml, template, options 229 | end 230 | 231 | def sass(template, options={}, &block) 232 | require 'sass' unless defined? ::Sass 233 | options[:layout] = false 234 | render :sass, template, options 235 | end 236 | 237 | def builder(template=nil, options={}, &block) 238 | require 'builder' unless defined? ::Builder 239 | options, template = template, nil if template.is_a?(Hash) 240 | template = lambda { block } if template.nil? 241 | render :builder, template, options 242 | end 243 | 244 | private 245 | def render(engine, template, options={}) #:nodoc: 246 | data = lookup_template(engine, template, options) 247 | output = __send__("render_#{engine}", template, data, options) 248 | layout, data = lookup_layout(engine, options) 249 | if layout 250 | __send__("render_#{engine}", layout, data, options) { output } 251 | else 252 | output 253 | end 254 | end 255 | 256 | def lookup_template(engine, template, options={}) 257 | case template 258 | when Symbol 259 | if cached = self.class.templates[template] 260 | lookup_template(engine, cached, options) 261 | else 262 | ::File.read(template_path(engine, template, options)) 263 | end 264 | when Proc 265 | template.call 266 | when String 267 | template 268 | else 269 | raise ArgumentError 270 | end 271 | end 272 | 273 | def lookup_layout(engine, options) 274 | return if options[:layout] == false 275 | options.delete(:layout) if options[:layout] == true 276 | template = options[:layout] || :layout 277 | data = lookup_template(engine, template, options) 278 | [template, data] 279 | rescue Errno::ENOENT 280 | nil 281 | end 282 | 283 | def template_path(engine, template, options={}) 284 | views_dir = 285 | options[:views_directory] || self.options.views || "./views" 286 | "#{views_dir}/#{template}.#{engine}" 287 | end 288 | 289 | def render_erb(template, data, options, &block) 290 | original_out_buf = @_out_buf 291 | data = data.call if data.kind_of? Proc 292 | 293 | instance = ::ERB.new(data, nil, nil, '@_out_buf') 294 | locals = options[:locals] || {} 295 | locals_assigns = locals.to_a.collect { |k,v| "#{k} = locals[:#{k}]" } 296 | 297 | src = "#{locals_assigns.join("\n")}\n#{instance.src}" 298 | eval src, binding, '(__ERB__)', locals_assigns.length + 1 299 | @_out_buf, result = original_out_buf, @_out_buf 300 | result 301 | end 302 | 303 | def render_haml(template, data, options, &block) 304 | engine = ::Haml::Engine.new(data, options[:options] || {}) 305 | engine.render(self, options[:locals] || {}, &block) 306 | end 307 | 308 | def render_sass(template, data, options, &block) 309 | engine = ::Sass::Engine.new(data, options[:sass] || {}) 310 | engine.render 311 | end 312 | 313 | def render_builder(template, data, options, &block) 314 | xml = ::Builder::XmlMarkup.new(:indent => 2) 315 | if data.respond_to?(:to_str) 316 | eval data.to_str, binding, '', 1 317 | elsif data.kind_of?(Proc) 318 | data.call(xml) 319 | end 320 | xml.target! 321 | end 322 | end 323 | 324 | # Base class for all Sinatra applications and middleware. 325 | class Base 326 | include Rack::Utils 327 | include Helpers 328 | include Templates 329 | 330 | attr_accessor :app 331 | 332 | def initialize(app=nil) 333 | @app = app 334 | yield self if block_given? 335 | end 336 | 337 | # Rack call interface. 338 | def call(env) 339 | dup.call!(env) 340 | end 341 | 342 | attr_accessor :env, :request, :response, :params 343 | 344 | def call!(env) 345 | @env = env 346 | @request = Request.new(env) 347 | @response = Response.new 348 | @params = nil 349 | 350 | invoke { dispatch! } 351 | invoke { error_block!(response.status) } 352 | 353 | status, header, body = @response.finish 354 | 355 | # Never produce a body on HEAD requests. Do retain the Content-Length 356 | # unless it's "0", in which case we assume it was calculated erroneously 357 | # for a manual HEAD response and remove it entirely. 358 | if @env['REQUEST_METHOD'] == 'HEAD' 359 | body = [] 360 | header.delete('Content-Length') if header['Content-Length'] == '0' 361 | end 362 | 363 | [status, header, body] 364 | end 365 | 366 | # Access options defined with Base.set. 367 | def options 368 | self.class 369 | end 370 | 371 | # Exit the current block and halt the response. 372 | def halt(*response) 373 | response = response.first if response.length == 1 374 | throw :halt, response 375 | end 376 | 377 | # Pass control to the next matching route. 378 | def pass 379 | throw :pass 380 | end 381 | 382 | # Forward the request to the downstream app -- middleware only. 383 | def forward 384 | fail "downstream app not set" unless @app.respond_to? :call 385 | status, headers, body = @app.call(@request.env) 386 | @response.status = status 387 | @response.body = body 388 | @response.headers.merge! headers 389 | nil 390 | end 391 | 392 | private 393 | # Run before filters and then locate and run a matching route. 394 | def route! 395 | @params = nested_params(@request.params) 396 | 397 | # before filters 398 | self.class.filters.each { |block| instance_eval(&block) } 399 | 400 | # routes 401 | if routes = self.class.routes[@request.request_method] 402 | original_params = @params 403 | path = unescape(@request.path_info) 404 | 405 | routes.each do |pattern, keys, conditions, block| 406 | if match = pattern.match(path) 407 | values = match.captures.to_a 408 | params = 409 | if keys.any? 410 | keys.zip(values).inject({}) do |hash,(k,v)| 411 | if k == 'splat' 412 | (hash[k] ||= []) << v 413 | else 414 | hash[k] = v 415 | end 416 | hash 417 | end 418 | elsif values.any? 419 | {'captures' => values} 420 | else 421 | {} 422 | end 423 | @params = original_params.merge(params) 424 | @block_params = values 425 | 426 | catch(:pass) do 427 | conditions.each { |cond| 428 | throw :pass if instance_eval(&cond) == false } 429 | throw :halt, instance_eval(&block) 430 | end 431 | end 432 | end 433 | end 434 | 435 | # No matching route found or all routes passed -- forward downstream 436 | # when running as middleware; 404 when running as normal app. 437 | if @app 438 | forward 439 | else 440 | raise NotFound 441 | end 442 | end 443 | 444 | def nested_params(params) 445 | return indifferent_hash.merge(params) if !params.keys.join.include?('[') 446 | params.inject indifferent_hash do |res, (key,val)| 447 | if key.include?('[') 448 | head = key.split(/[\]\[]+/) 449 | last = head.pop 450 | head.inject(res){ |hash,k| hash[k] ||= indifferent_hash }[last] = val 451 | else 452 | res[key] = val 453 | end 454 | res 455 | end 456 | end 457 | 458 | def indifferent_hash 459 | Hash.new {|hash,key| hash[key.to_s] if Symbol === key } 460 | end 461 | 462 | # Run the block with 'throw :halt' support and apply result to the response. 463 | def invoke(&block) 464 | res = catch(:halt) { instance_eval(&block) } 465 | return if res.nil? 466 | 467 | case 468 | when res.respond_to?(:to_str) 469 | @response.body = [res] 470 | when res.respond_to?(:to_ary) 471 | res = res.to_ary 472 | if Fixnum === res.first 473 | if res.length == 3 474 | @response.status, headers, body = res 475 | @response.body = body if body 476 | headers.each { |k, v| @response.headers[k] = v } if headers 477 | elsif res.length == 2 478 | @response.status = res.first 479 | @response.body = res.last 480 | else 481 | raise TypeError, "#{res.inspect} not supported" 482 | end 483 | else 484 | @response.body = res 485 | end 486 | when res.respond_to?(:each) 487 | @response.body = res 488 | when (100...599) === res 489 | @response.status = res 490 | end 491 | 492 | res 493 | end 494 | 495 | # Dispatch a request with error handling. 496 | def dispatch! 497 | route! 498 | rescue NotFound => boom 499 | handle_not_found!(boom) 500 | rescue ::Exception => boom 501 | handle_exception!(boom) 502 | end 503 | 504 | def handle_not_found!(boom) 505 | @env['sinatra.error'] = boom 506 | @response.status = 404 507 | @response.body = ['

Not Found

'] 508 | error_block! boom.class, NotFound 509 | end 510 | 511 | def handle_exception!(boom) 512 | @env['sinatra.error'] = boom 513 | 514 | dump_errors!(boom) if options.dump_errors? 515 | raise boom if options.raise_errors? 516 | 517 | @response.status = 500 518 | error_block! boom.class, Exception 519 | end 520 | 521 | # Find an custom error block for the key(s) specified. 522 | def error_block!(*keys) 523 | errmap = self.class.errors 524 | keys.each do |key| 525 | if block = errmap[key] 526 | res = instance_eval(&block) 527 | return res 528 | end 529 | end 530 | nil 531 | end 532 | 533 | def dump_errors!(boom) 534 | backtrace = clean_backtrace(boom.backtrace) 535 | msg = ["#{boom.class} - #{boom.message}:", 536 | *backtrace].join("\n ") 537 | @env['rack.errors'].write(msg) 538 | end 539 | 540 | def clean_backtrace(trace) 541 | return trace unless options.clean_trace? 542 | 543 | trace.reject { |line| 544 | line =~ /lib\/sinatra.*\.rb/ || 545 | (defined?(Gem) && line.include?(Gem.dir)) 546 | }.map! { |line| line.gsub(/^\.\//, '') } 547 | end 548 | 549 | @routes = {} 550 | @filters = [] 551 | @conditions = [] 552 | @templates = {} 553 | @middleware = [] 554 | @errors = {} 555 | @prototype = nil 556 | @extensions = [] 557 | 558 | class << self 559 | attr_accessor :routes, :filters, :conditions, :templates, 560 | :middleware, :errors 561 | 562 | def set(option, value=self) 563 | if value.kind_of?(Proc) 564 | metadef(option, &value) 565 | metadef("#{option}?") { !!__send__(option) } 566 | metadef("#{option}=") { |val| set(option, Proc.new{val}) } 567 | elsif value == self && option.respond_to?(:to_hash) 568 | option.to_hash.each { |k,v| set(k, v) } 569 | elsif respond_to?("#{option}=") 570 | __send__ "#{option}=", value 571 | else 572 | set option, Proc.new{value} 573 | end 574 | self 575 | end 576 | 577 | def enable(*opts) 578 | opts.each { |key| set(key, true) } 579 | end 580 | 581 | def disable(*opts) 582 | opts.each { |key| set(key, false) } 583 | end 584 | 585 | def error(codes=Exception, &block) 586 | if codes.respond_to? :each 587 | codes.each { |err| error(err, &block) } 588 | else 589 | @errors[codes] = block 590 | end 591 | end 592 | 593 | def not_found(&block) 594 | error 404, &block 595 | end 596 | 597 | def template(name, &block) 598 | templates[name] = block 599 | end 600 | 601 | def layout(name=:layout, &block) 602 | template name, &block 603 | end 604 | 605 | def use_in_file_templates!(file=nil) 606 | file ||= caller_files.first 607 | if data = ::IO.read(file).split('__END__')[1] 608 | data.gsub!(/\r\n/, "\n") 609 | template = nil 610 | data.each_line do |line| 611 | if line =~ /^@@\s*(.*)/ 612 | template = templates[$1.to_sym] = '' 613 | elsif template 614 | template << line 615 | end 616 | end 617 | end 618 | end 619 | 620 | # Look up a media type by file extension in Rack's mime registry. 621 | def media_type(type) 622 | return type if type.nil? || type.to_s.include?('/') 623 | type = ".#{type}" unless type.to_s[0] == ?. 624 | Rack::Mime.mime_type(type, nil) 625 | end 626 | 627 | def before(&block) 628 | @filters << block 629 | end 630 | 631 | def condition(&block) 632 | @conditions << block 633 | end 634 | 635 | private 636 | def host_name(pattern) 637 | condition { pattern === request.host } 638 | end 639 | 640 | def user_agent(pattern) 641 | condition { 642 | if request.user_agent =~ pattern 643 | @params[:agent] = $~[1..-1] 644 | true 645 | else 646 | false 647 | end 648 | } 649 | end 650 | 651 | def accept_mime_types(types) 652 | types = [types] unless types.kind_of? Array 653 | types.map!{|t| media_type(t)} 654 | 655 | condition { 656 | matching_types = (request.accept & types) 657 | unless matching_types.empty? 658 | response.headers['Content-Type'] = matching_types.first 659 | true 660 | else 661 | false 662 | end 663 | } 664 | end 665 | 666 | public 667 | def get(path, opts={}, &block) 668 | conditions = @conditions.dup 669 | route('GET', path, opts, &block) 670 | 671 | @conditions = conditions 672 | route('HEAD', path, opts, &block) 673 | end 674 | 675 | def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end 676 | def post(path, opts={}, &bk); route 'POST', path, opts, &bk; end 677 | def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk; end 678 | def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk; end 679 | 680 | private 681 | def route(verb, path, opts={}, &block) 682 | host_name opts[:host] if opts.key?(:host) 683 | user_agent opts[:agent] if opts.key?(:agent) 684 | accept_mime_types opts[:provides] if opts.key?(:provides) 685 | 686 | pattern, keys = compile(path) 687 | conditions, @conditions = @conditions, [] 688 | 689 | define_method "#{verb} #{path}", &block 690 | unbound_method = instance_method("#{verb} #{path}") 691 | block = 692 | if block.arity != 0 693 | lambda { unbound_method.bind(self).call(*@block_params) } 694 | else 695 | lambda { unbound_method.bind(self).call } 696 | end 697 | 698 | invoke_hook(:route_added, verb, path) 699 | 700 | (routes[verb] ||= []). 701 | push([pattern, keys, conditions, block]).last 702 | end 703 | 704 | def invoke_hook(name, *args) 705 | extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } 706 | end 707 | 708 | def compile(path) 709 | keys = [] 710 | if path.respond_to? :to_str 711 | special_chars = %w{. + ( )} 712 | pattern = 713 | path.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match| 714 | case match 715 | when "*" 716 | keys << 'splat' 717 | "(.*?)" 718 | when *special_chars 719 | Regexp.escape(match) 720 | else 721 | keys << $2[1..-1] 722 | "([^/?&#]+)" 723 | end 724 | end 725 | [/^#{pattern}$/, keys] 726 | elsif path.respond_to? :match 727 | [path, keys] 728 | else 729 | raise TypeError, path 730 | end 731 | end 732 | 733 | public 734 | def helpers(*extensions, &block) 735 | class_eval(&block) if block_given? 736 | include *extensions if extensions.any? 737 | end 738 | 739 | def extensions 740 | (@extensions + (superclass.extensions rescue [])).uniq 741 | end 742 | 743 | def register(*extensions, &block) 744 | extensions << Module.new(&block) if block_given? 745 | @extensions += extensions 746 | extensions.each do |extension| 747 | extend extension 748 | extension.registered(self) if extension.respond_to?(:registered) 749 | end 750 | end 751 | 752 | def development? ; environment == :development ; end 753 | def test? ; environment == :test ; end 754 | def production? ; environment == :production ; end 755 | 756 | def configure(*envs, &block) 757 | return if reloading? 758 | yield if envs.empty? || envs.include?(environment.to_sym) 759 | end 760 | 761 | def use(middleware, *args, &block) 762 | @prototype = nil 763 | @middleware << [middleware, args, block] 764 | end 765 | 766 | def run!(options={}) 767 | set options 768 | handler = detect_rack_handler 769 | handler_name = handler.name.gsub(/.*::/, '') 770 | puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " + 771 | "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i 772 | handler.run self, :Host => host, :Port => port do |server| 773 | trap(:INT) do 774 | ## Use thins' hard #stop! if available, otherwise just #stop 775 | server.respond_to?(:stop!) ? server.stop! : server.stop 776 | puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i 777 | end 778 | end 779 | rescue Errno::EADDRINUSE => e 780 | puts "== Someone is already performing on port #{port}!" 781 | end 782 | 783 | # The prototype instance used to process requests. 784 | def prototype 785 | @prototype ||= new 786 | end 787 | 788 | # Create a new instance of the class fronted by its middleware 789 | # pipeline. The object is guaranteed to respond to #call but may not be 790 | # an instance of the class new was called on. 791 | def new(*args, &bk) 792 | builder = Rack::Builder.new 793 | builder.use Rack::Session::Cookie if sessions? && !test? 794 | builder.use Rack::CommonLogger if logging? 795 | builder.use Rack::MethodOverride if methodoverride? 796 | @middleware.each { |c, args, bk| builder.use(c, *args, &bk) } 797 | builder.run super 798 | builder.to_app 799 | end 800 | 801 | def call(env) 802 | synchronize do 803 | reload! if reload? 804 | prototype.call(env) 805 | end 806 | end 807 | 808 | def reloading? 809 | @reloading 810 | end 811 | 812 | def reload! 813 | @reloading = true 814 | reset! 815 | $LOADED_FEATURES.delete("sinatra.rb") 816 | ::Kernel.load app_file 817 | @reloading = false 818 | end 819 | 820 | def reset!(base=superclass) 821 | @routes = base.dupe_routes 822 | @templates = base.templates.dup 823 | @conditions = [] 824 | @filters = base.filters.dup 825 | @errors = base.errors.dup 826 | @middleware = base.middleware.dup 827 | @prototype = nil 828 | @extensions = [] 829 | end 830 | 831 | protected 832 | def dupe_routes 833 | routes.inject({}) do |hash,(request_method,routes)| 834 | hash[request_method] = routes.dup 835 | hash 836 | end 837 | end 838 | 839 | private 840 | def detect_rack_handler 841 | servers = Array(self.server) 842 | servers.each do |server_name| 843 | begin 844 | return Rack::Handler.get(server_name) 845 | rescue LoadError 846 | rescue NameError 847 | end 848 | end 849 | fail "Server handler (#{servers.join(',')}) not found." 850 | end 851 | 852 | def inherited(subclass) 853 | subclass.reset! self 854 | super 855 | end 856 | 857 | @@mutex = Mutex.new 858 | def synchronize(&block) 859 | if lock? 860 | @@mutex.synchronize(&block) 861 | else 862 | yield 863 | end 864 | end 865 | 866 | def metadef(message, &block) 867 | (class << self; self; end). 868 | send :define_method, message, &block 869 | end 870 | 871 | # Like Kernel#caller but excluding certain magic entries and without 872 | # line / method information; the resulting array contains filenames only. 873 | def caller_files 874 | ignore = [ 875 | /lib\/sinatra.*\.rb$/, # all sinatra code 876 | /\(.*\)/, # generated code 877 | /custom_require\.rb$/, # rubygems require hacks 878 | /active_support/, # active_support require hacks 879 | ] 880 | caller(1). 881 | map { |line| line.split(/:\d/, 2).first }. 882 | reject { |file| ignore.any? { |pattern| file =~ pattern } } 883 | end 884 | end 885 | 886 | set :raise_errors, true 887 | set :dump_errors, false 888 | set :clean_trace, true 889 | set :sessions, false 890 | set :logging, false 891 | set :methodoverride, false 892 | set :static, false 893 | set :environment, (ENV['RACK_ENV'] || :development).to_sym 894 | 895 | set :run, false 896 | set :server, %w[thin mongrel webrick] 897 | set :host, '0.0.0.0' 898 | set :port, 4567 899 | 900 | set :app_file, nil 901 | set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) } 902 | set :views, Proc.new { root && File.join(root, 'views') } 903 | set :public, Proc.new { root && File.join(root, 'public') } 904 | set :reload, Proc.new { app_file? && app_file !~ /\.ru$/i && development? } 905 | set :lock, Proc.new { reload? } 906 | 907 | # static files route 908 | get(/.*[^\/]$/) do 909 | pass unless options.static? && options.public? 910 | public_dir = File.expand_path(options.public) 911 | path = File.expand_path(public_dir + unescape(request.path_info)) 912 | pass if path[0, public_dir.length] != public_dir 913 | pass unless File.file?(path) 914 | send_file path, :disposition => nil 915 | end 916 | 917 | error ::Exception do 918 | response.status = 500 919 | content_type 'text/html' 920 | '

Internal Server Error

' 921 | end 922 | 923 | configure :development do 924 | get '/__sinatra__/:image.png' do 925 | filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png" 926 | content_type :png 927 | send_file filename 928 | end 929 | 930 | error NotFound do 931 | (<<-HTML).gsub(/^ {8}/, '') 932 | 933 | 934 | 935 | 940 | 941 | 942 |

Sinatra doesn't know this ditty.

943 | 944 |
945 | Try this: 946 |
#{request.request_method.downcase} '#{request.path_info}' do\n  "Hello World"\nend
947 |
948 | 949 | 950 | HTML 951 | end 952 | 953 | error do 954 | next unless err = request.env['sinatra.error'] 955 | heading = err.class.name + ' - ' + err.message.to_s 956 | (<<-HTML).gsub(/^ {8}/, '') 957 | 958 | 959 | 960 | 968 | 969 | 970 |
971 | 972 |

#{escape_html(heading)}

973 |
#{escape_html(clean_backtrace(err.backtrace) * "\n")}
974 |

Params

975 |
#{escape_html(params.inspect)}
976 |
977 | 978 | 979 | HTML 980 | end 981 | end 982 | end 983 | 984 | # Base class for classic style (top-level) applications. 985 | class Default < Base 986 | set :raise_errors, Proc.new { test? } 987 | set :dump_errors, true 988 | set :sessions, false 989 | set :logging, Proc.new { ! test? } 990 | set :methodoverride, true 991 | set :static, true 992 | set :run, Proc.new { ! test? } 993 | 994 | def self.register(*extensions, &block) #:nodoc: 995 | added_methods = extensions.map {|m| m.public_instance_methods }.flatten 996 | Delegator.delegate *added_methods 997 | super(*extensions, &block) 998 | end 999 | end 1000 | 1001 | # The top-level Application. All DSL methods executed on main are delegated 1002 | # to this class. 1003 | class Application < Default 1004 | end 1005 | 1006 | module Delegator #:nodoc: 1007 | def self.delegate(*methods) 1008 | methods.each do |method_name| 1009 | eval <<-RUBY, binding, '(__DELEGATE__)', 1 1010 | def #{method_name}(*args, &b) 1011 | ::Sinatra::Application.#{method_name}(*args, &b) 1012 | end 1013 | private :#{method_name} 1014 | RUBY 1015 | end 1016 | end 1017 | 1018 | delegate :get, :put, :post, :delete, :head, :template, :layout, :before, 1019 | :error, :not_found, :configures, :configure, :set, :set_option, 1020 | :set_options, :enable, :disable, :use, :development?, :test?, 1021 | :production?, :use_in_file_templates!, :helpers 1022 | end 1023 | 1024 | def self.new(base=Base, options={}, &block) 1025 | base = Class.new(base) 1026 | base.send :class_eval, &block if block_given? 1027 | base 1028 | end 1029 | 1030 | # Extend the top-level DSL with the modules provided. 1031 | def self.register(*extensions, &block) 1032 | Default.register(*extensions, &block) 1033 | end 1034 | 1035 | # Include the helper modules provided in Sinatra's request context. 1036 | def self.helpers(*extensions, &block) 1037 | Default.helpers(*extensions, &block) 1038 | end 1039 | end 1040 | 1041 | class String #:nodoc: 1042 | # Define String#each under 1.9 for Rack compatibility. This should be 1043 | # removed once Rack is fully 1.9 compatible. 1044 | alias_method :each, :each_line unless ''.respond_to? :each 1045 | 1046 | # Define String#bytesize as an alias to String#length for Ruby 1.8.6 and 1047 | # earlier. 1048 | alias_method :bytesize, :length unless ''.respond_to? :bytesize 1049 | end 1050 | --------------------------------------------------------------------------------