Much needed refactoring in dispatching code [#131]/[#127]

This moves the :halt catch out so that all routing code runs
within one giant catch block instead of running each type
of handler in its own catch block. This required some cleanup
in the error handling code, which cleaned things up quite a bit.

This corrects two issues:

1. halt with > 1 args causes ArgumentError
   http://sinatra.lighthouseapp.com/projects/9779/tickets/131

2. halting in a before filter doesn't modify response
   http://sinatra.lighthouseapp.com/projects/9779/tickets/127

We still need to split up the more epic methods (#route!, #invoke)
but the logic is pretty sound at this point.
This commit is contained in:
Ryan Tomayko 2009-01-24 23:41:09 -08:00
parent 877bfb379c
commit 0ade0beec8
4 changed files with 93 additions and 25 deletions

View File

@ -320,7 +320,11 @@ module Sinatra
@request = Request.new(env) @request = Request.new(env)
@response = Response.new @response = Response.new
@params = nil @params = nil
error_detection { dispatch! }
invoke { dispatch! }
invoke { error_block!(response.status) }
@response.body = [] if @env['REQUEST_METHOD'] == 'HEAD'
@response.finish @response.finish
end end
@ -329,7 +333,8 @@ module Sinatra
end end
def halt(*response) def halt(*response)
throw :halt, *response response = response.first if response.length == 1
throw :halt, response
end end
def pass def pass
@ -337,18 +342,19 @@ module Sinatra
end end
private private
def dispatch! # Run before filters and then locate and run a matching route.
@params = original_params = nested_params(@request.params) def route!
@params = nested_params(@request.params)
self.class.filters.each do |block| # before filters
res = catch(:halt) { instance_eval(&block) ; :continue } self.class.filters.each { |block| instance_eval(&block) }
return unless res == :continue
end
# routes
if routes = self.class.routes[@request.request_method] if routes = self.class.routes[@request.request_method]
original_params = @params
path = @request.path_info path = @request.path_info
routes.each do |pattern, keys, conditions, method_name| routes.each do |pattern, keys, conditions, block|
if match = pattern.match(path) if match = pattern.match(path)
values = match.captures.map{|val| val && unescape(val) } values = match.captures.map{|val| val && unescape(val) }
params = params =
@ -368,14 +374,15 @@ module Sinatra
end end
@params = original_params.merge(params) @params = original_params.merge(params)
catch(:pass) { catch(:pass) do
conditions.each { |cond| conditions.each { |cond|
throw :pass if instance_eval(&cond) == false } throw :pass if instance_eval(&cond) == false }
return invoke(method_name) throw :halt, instance_eval(&block)
} end
end end
end end
end end
raise NotFound raise NotFound
end end
@ -397,7 +404,8 @@ module Sinatra
Hash.new {|hash,key| hash[key.to_s] if Symbol === key } Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
end end
def invoke(block) # Run the block with 'throw :halt' support and apply result to the response.
def invoke(&block)
res = catch(:halt) { instance_eval(&block) } res = catch(:halt) { instance_eval(&block) }
return if res.nil? return if res.nil?
@ -429,15 +437,15 @@ module Sinatra
res res
end end
def error_detection # Dispatch a request with error handling.
errmap = self.class.errors def dispatch!
yield route!
rescue NotFound => boom rescue NotFound => boom
@env['sinatra.error'] = boom @env['sinatra.error'] = boom
@response.status = 404 @response.status = 404
@response.body = ['<h1>Not Found</h1>'] @response.body = ['<h1>Not Found</h1>']
handler = errmap[boom.class] || errmap[NotFound] error_block! boom.class, NotFound
invoke handler unless handler.nil?
rescue ::Exception => boom rescue ::Exception => boom
@env['sinatra.error'] = boom @env['sinatra.error'] = boom
@ -449,11 +457,19 @@ module Sinatra
raise boom if options.raise_errors? raise boom if options.raise_errors?
@response.status = 500 @response.status = 500
invoke errmap[boom.class] || errmap[Exception] error_block! boom.class, Exception
ensure end
if @response.status >= 400 && errmap.key?(response.status)
invoke errmap[response.status] # Find an custom error block for the key(s) specified.
def error_block!(*keys)
errmap = self.class.errors
keys.each do |key|
if block = errmap[key]
res = instance_eval(&block)
return res
end
end end
nil
end end
def clean_backtrace(trace) def clean_backtrace(trace)
@ -592,7 +608,7 @@ module Sinatra
route('GET', path, opts, &block) route('GET', path, opts, &block)
@conditions = conditions @conditions = conditions
head(path, opts) { invoke(block) ; [] } route('HEAD', path, opts, &block)
end end
def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end

View File

@ -107,7 +107,7 @@ module Sinatra
# Throwing halt with a Symbol and the to_result convention are # Throwing halt with a Symbol and the to_result convention are
# deprecated. Override the invoke method to detect those types of return # deprecated. Override the invoke method to detect those types of return
# values. # values.
def invoke(handler) def invoke(&block)
res = super res = super
case case
when res.kind_of?(Symbol) when res.kind_of?(Symbol)

View File

@ -73,6 +73,19 @@ describe "Filters" do
assert_equal 'cool', body assert_equal 'cool', body
end end
it "does modify the response with halt" do
mock_app {
before { halt 302, 'Hi' }
get '/foo' do
"should not happen"
end
}
get '/foo'
assert_equal 302, response.status
assert_equal 'Hi', body
end
it "gives you access to params" do it "gives you access to params" do
mock_app { mock_app {
before { @foo = params['foo'] } before { @foo = params['foo'] }

View File

@ -6,7 +6,7 @@ def route_def(pattern)
end end
describe "Routing" do describe "Routing" do
%w[get put post delete head].each do |verb| %w[get put post delete].each do |verb|
it "defines #{verb.upcase} request handlers with #{verb}" do it "defines #{verb.upcase} request handlers with #{verb}" do
mock_app { mock_app {
send verb, '/hello' do send verb, '/hello' do
@ -21,6 +21,21 @@ describe "Routing" do
end end
end end
it "defines HEAD request handlers with HEAD" do
mock_app {
head '/hello' do
response['X-Hello'] = 'World!'
'remove me'
end
}
request = Rack::MockRequest.new(@app)
response = request.request('HEAD', '/hello', {})
assert response.ok?
assert_equal 'World!', response['X-Hello']
assert_equal '', response.body
end
it "404s when no route satisfies the request" do it "404s when no route satisfies the request" do
mock_app { mock_app {
get('/foo') { } get('/foo') { }
@ -296,6 +311,30 @@ describe "Routing" do
assert_equal 'Hello World', body assert_equal 'Hello World', body
end end
it "halts with a response tuple" do
mock_app {
get '/' do
halt 295, {'Content-Type' => 'text/plain'}, 'Hello World'
end
}
get '/'
assert_equal 295, status
assert_equal 'text/plain', response['Content-Type']
assert_equal 'Hello World', body
end
it "halts with an array of strings" do
mock_app {
get '/' do
halt %w[Hello World How Are You]
end
}
get '/'
assert_equal 'HelloWorldHowAreYou', body
end
it "transitions to the next matching route on pass" do it "transitions to the next matching route on pass" do
mock_app { mock_app {
get '/:foo' do get '/:foo' do