├── 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!
\nHello 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 "\nHello 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 "\nHello 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 |
--------------------------------------------------------------------------------