Skip to content

Commit

Permalink
middleware and correct tree searching
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbuddy committed Jun 2, 2010
1 parent e75852b commit 83bd9dd
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 56 deletions.
6 changes: 4 additions & 2 deletions lib/http_router.rb
Expand Up @@ -32,13 +32,14 @@ def initialize(*args, &block)
options = args.first
else
default_app = args.first
options = args.last
options = args[1]
end

@options = options
@default_app = default_app || options && options[:default_app] || proc{|env| ::Rack::Response.new("Not Found", 404).finish }
@ignore_trailing_slash = options && options.key?(:ignore_trailing_slash) ? options[:ignore_trailing_slash] : true
@redirect_trailing_slash = options && options.key?(:redirect_trailing_slash) ? options[:redirect_trailing_slash] : false
@middleware = options && options.key?(:middleware) ? options[:middleware] : false
@routes = []
@named_routes = {}
@init_block = block
Expand Down Expand Up @@ -122,7 +123,7 @@ def call(env)
response.finish
else
env['router'] = self
if response = recognize(request)
if response = recognize(request) and !@middleware
if response.matched? && response.route.dest && response.route.dest.respond_to?(:call)
process_params(env, response)
consume_path!(request, response) if response.partial_match?
Expand All @@ -131,6 +132,7 @@ def call(env)
return [response.status, response.headers, []]
end
end
env['router.response'] = response if @middleware
@default_app.call(env)
end
end
Expand Down
14 changes: 8 additions & 6 deletions lib/http_router/glob.rb
@@ -1,12 +1,14 @@
class HttpRouter
class Glob < Variable
def matches(env, parts, whole_path)
if @matches_with && match = @matches_with.match(parts.first)

def matches?(env, parts, whole_path)
@matches_with.nil? or (!parts.empty? and match = @matches_with.match(parts.first) and match.begin(0))
end

def consume(env, parts, whole_path)
if @matches_with
params = [parts.shift]
while !parts.empty? and match = @matches_with.match(parts.first)
params << parts.shift
end
whole_path.replace(parts.join('/'))
params << parts.shift while matches?(env, parts, whole_path)
params
else
params = parts.dup
Expand Down
29 changes: 16 additions & 13 deletions lib/http_router/node.rb
Expand Up @@ -141,30 +141,33 @@ def generate_request_method_tree(request_options)

def find_on_parts(request, parts, params)
unless parts.empty?
whole_path = parts.join('/')
if @linear && !@linear.empty?
whole_path = parts.join('/')
response = nil
dupped_parts = nil
next_node = @linear.find do |(tester, node)|
if tester.is_a?(Regexp) and match = tester.match(whole_path) #and match.index == 0 TODO
whole_path.slice!(0,match[0].size)
parts.replace(router.split(whole_path))
node
elsif tester.respond_to?(:matches) and new_params = tester.matches(request.env, parts, whole_path)
params << new_params
node
if tester.respond_to?(:matches?) and tester.matches?(request.env, parts, whole_path)
dupped_parts = parts.dup
params << tester.consume(request.env, dupped_parts, whole_path)
if response = node.find_on_parts(request, dupped_parts, params)
parts.replace(dupped_parts)
end
elsif tester.respond_to?(:match) and match = tester.match(whole_path) and match.begin(0) == 0
dupped_parts = router.split(whole_path[match[0].size, whole_path.size])
if response = node.find_on_parts(request, dupped_parts, params)
parts.replace(dupped_parts)
end
else
nil
end
end
if next_node and match = next_node.last.find_on_parts(request, parts, params)
return match
end
return response if response
end
if match = @lookup && @lookup[parts.first]
parts.shift
return match.find_on_parts(request, parts, params)
elsif @catchall
params << @catchall.variable.matches(request.env, parts, whole_path)
parts.shift
params << @catchall.variable.consume(request.env, parts, whole_path)
return @catchall.find_on_parts(request, parts, params)
elsif parts.size == 1 && parts.first == '' && (value && value.route.trailing_slash_ignore? || router.ignore_trailing_slash?)
parts.shift
Expand Down
16 changes: 10 additions & 6 deletions lib/http_router/variable.rb
Expand Up @@ -8,13 +8,17 @@ def initialize(base, name, matches_with = nil)
@matches_with = matches_with
end

def matches(env, parts, whole_path)
if @matches_with.nil?
parts.first
elsif @matches_with and match = @matches_with.match(whole_path)
whole_path.slice!(0, match[0].size)
parts.replace(router.split(whole_path))
def matches?(env, parts, whole_path)
@matches_with.nil? or (@matches_with and match = @matches_with.match(whole_path) and match.begin(0) == 0)
end

def consume(env, parts, whole_path)
if @matches_with
match = @matches_with.match(whole_path)
parts.replace(router.split(whole_path[match.end(0), whole_path.size]))
match[0]
else
parts.shift
end
end

Expand Down
20 changes: 20 additions & 0 deletions spec/rack/middleware_spec.rb
@@ -0,0 +1,20 @@
describe "HttpRouter as middleware" do
before(:each) do
@builder = Rack::Builder.new do
use(HttpRouter, :middleware => true) {
add('/test').name(:test).to(:test)
}
end
end

it "should always have the router" do
@builder.run proc{|env| [200, {}, [env['router'].url(:test)]]}
@builder.call(Rack::MockRequest.env_for('/some-path')).last.join.should == '/test'
end

it "should stash the match if it exists" do
@builder.run proc{|env| [200, {}, [env['router.response'].dest.to_s]]}
@builder.call(Rack::MockRequest.env_for('/test')).last.join.should == 'test'
end
end

69 changes: 40 additions & 29 deletions spec/recognize_spec.rb
Expand Up @@ -3,7 +3,7 @@
@router = HttpRouter.new
end

context("static paths") do
context("with static paths") do
['/', '/test', '/test/time', '/one/more/what', '/test.html'].each do |path|
it "should recognize #{path.inspect}" do
route = @router.add(path).to(path)
Expand All @@ -28,7 +28,7 @@
end
end

context("partial matching") do
context("with partial matching") do
it "should match partially or completely" do
route = @router.add("/test*").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test')).route.should == route
Expand All @@ -38,8 +38,8 @@
end
end

context("proc acceptance") do
it "should match optionally with a proc" do
context("with proc acceptance") do
it "should match" do
@router.add("/test").arbitrary(Proc.new{|req| req.host == 'hellodooly' }).to(:test1)
@router.add("/test").arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 80}.to(:test2)
@router.add("/test").arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 8080}.to(:test3)
Expand All @@ -56,7 +56,7 @@
response.dest.should == :test4
end

it "should match optionally with a proc and request conditions" do
it "should match with request conditions" do
@router.add("/test").get.arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 80}.to(:test1)
@router.add("/test").get.arbitrary(Proc.new{|req| req.host == 'lovelove' }).arbitrary{|req| req.port == 8080}.to(:test2)
response = @router.recognize(Rack::MockRequest.env_for('http://lovelove:8080/test'))
Expand All @@ -73,57 +73,57 @@

end

context("trailing slashes") do
it "should ignore a trailing slash" do
context("with trailing slashes") do
it "should ignore" do
route = @router.add("/test").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
end

it "should not recognize a trailing slash when used with the /? syntax and ignore_trailing_slash disabled" do
it "should not recognize when used with the /? syntax and ignore_trailing_slash disabled" do
@router = HttpRouter.new(:ignore_trailing_slash => false)
route = @router.add("/test/?").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test/')).route.should == route
end

it "should recognize a trailing slash when used with the /? syntax and ignore_trailing_slash enabled" do
it "should recognize when used with the /? syntax and ignore_trailing_slash enabled" do
@router = HttpRouter.new(:ignore_trailing_slash => false)
route = @router.add("/test").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test/')).should be_nil
end
it "should not capture the trailing slash in a variable normally" do
it "should not capture normally" do
route = @router.add("/:test").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test/')).params.first.should == 'test'
end
end
end

context "variables" do
it "should recognize a simple variable" do
context "with variables" do
it "should recognize" do
@router.add("/foo").to(:test1)
@router.add("/foo/:id").to(:test2)
@router.recognize(Rack::MockRequest.env_for('/foo')).dest.should == :test1
@router.recognize(Rack::MockRequest.env_for('/foo/id')).dest.should == :test2
end
end

context "missing leading /" do
it "should recognize a simple path" do
context "with missing leading /" do
it "should recognize" do
@router.add("foo").to(:test1)
@router.add("foo.html").to(:test2)
@router.recognize(Rack::MockRequest.env_for('/foo')).dest.should == :test1
@router.recognize(Rack::MockRequest.env_for('/foo.html')).dest.should == :test2
end
end

context "request methods" do
it "should pick a specific request_method" do
context "with request methods" do
it "should recognize" do
route = @router.post("/test").to(:test)
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'POST')).route.should == route
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).status.should == 405
@router.recognize(Rack::MockRequest.env_for('/test', :method => 'GET')).headers['Allow'].should == "POST"
end

it "should pick a specific request_method with other paths all through it" do
it "should recognize deeply" do
@router.post("/test").to(:test_post)
@router.post("/test/post").to(:test_post_post)
@router.get("/test").to(:test_get)
Expand Down Expand Up @@ -153,7 +153,7 @@

end

context("dynamic paths") do
context("with dynamic paths") do
it "should recognize '/:variable'" do
route = @router.add('/:variable').to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/value'))
Expand Down Expand Up @@ -200,14 +200,14 @@
response.params_as_hash[:test].should == 'hey'
end

context "globs" do
it "should recognize a glob" do
context "with globs" do
it "should recognize" do
route = @router.add('/test/*variable').to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/test/one/two/three'))
response.route.should == route
response.params.should == [['one', 'two', 'three']]
end
it "should recognize a glob with a regexp" do
it "should recognize with a regexp" do
route = @router.add('/test/*variable/anymore').matching(:variable => /^\d+$/).to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/test/123/345/567/anymore'))
response.route.should == route
Expand All @@ -219,23 +219,23 @@

end

context("interstitial variables") do
it "should recognize interstitial variables" do
context("with interstitial variables") do
it "should recognize" do
route = @router.add('/one-:variable-time').to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/one-value-time'))
response.route.should == route
response.params_as_hash[:variable].should == 'value'
end

it "should recognize interstitial variables with a regex" do
it "should recognize with a regex" do
route = @router.add('/one-:variable-time').matching(:variable => /^\d+/).to(:test)
@router.recognize(Rack::MockRequest.env_for('/one-value-time')).should be_nil
response = @router.recognize(Rack::MockRequest.env_for('/one-123-time'))
response.route.should == route
response.params_as_hash[:variable].should == '123'
end

it "should recognize interstitial variable when there is an extension" do
it "should recognize when there is an extension" do
route = @router.add('/hey.:greed.html').to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/hey.greedyboy.html'))
response.route.should == route
Expand All @@ -244,8 +244,8 @@

end

context("dynamic greedy paths") do
it "should recognize greedy variables" do
context("with dynamic greedy paths") do
it "should recognize" do
route = @router.add('/:variable').matching(:variable => /\d+/).to(:test)
response = @router.recognize(Rack::MockRequest.env_for('/123'))
response.route.should == route
Expand All @@ -255,12 +255,23 @@
response.should be_nil
end

it "should capture the trailing slash in a greedy variable" do
it "should continue on with normal if regex fails to match" do
@router.add("/:test/number").matching(:test => /\d+/).to(:test_number)
target = @router.add("/:test/anything").to(:test_anything)
@router.recognize(Rack::MockRequest.env_for('/123/anything')).route.should == target
end

it "should capture the trailing slash" do
route = @router.add("/:test").matching(:test => /.*/).to(:test)
@router.recognize(Rack::MockRequest.env_for('/test/')).params.first.should == 'test/'
end

it "should capture the extension in a greedy variable" do
it "should require the match to begin at the beginning" do
route = @router.add("/:test").matching(:test => /\d+/).to(:test)
@router.recognize(Rack::MockRequest.env_for('/a123')).should be_nil
end

it "should capture the extension" do
route = @router.add("/:test").matching(:test => /.*/).to(:test)
@router.recognize(Rack::MockRequest.env_for('/test.html')).params.first.should == 'test.html'
end
Expand Down

0 comments on commit 83bd9dd

Please sign in to comment.