diff --git a/lib/newark/app.rb b/lib/newark/app.rb
index b2e1bf7..aa274df 100644
--- a/lib/newark/app.rb
+++ b/lib/newark/app.rb
@@ -51,6 +51,17 @@ def initialize(*)
@before_hooks = self.class.instance_variable_get(:@before_hooks)
@after_hooks = self.class.instance_variable_get(:@after_hooks)
@routes = self.class.instance_variable_get(:@routes)
+ # @multiplex_routes is a Hash, used to store all the multiplex routes
+ # each verb has an array of multiplex routes that represent all the
+ # sequential combination of route patterns.
+ @multiplex_routes = {}.tap do |values|
+ @routes.each do |verb, rtes|
+ values[verb] = (0..rtes.size-1).map do |index|
+ regexps = rtes[index..-1].map.with_index {|route, i| /(?<_#{i}>#{route.regex})/ }
+ /\A#{Regexp.union(regexps)}\z/
+ end
+ end
+ end
end
def call(env)
@@ -75,6 +86,13 @@ def params
def route
route = match_route
if route
+ # updates the params in the request based on the keys in the matched route
+ unless route.keys.empty?
+ match_data = route.regex.match(request.path_info)
+ route.keys.each_with_index do |key, i|
+ request.params[key] = match_data[i + 1]
+ end
+ end
if exec_before_hooks
response.body = exec(route.handler)
exec_after_hooks
@@ -88,8 +106,24 @@ def route
private
- def match_route
- Router.new(routes, request).route
+ # a recursive method finds the route by matching the request path
+ # by a multiplex matcher
+ def match_route(index = 0)
+ routes_matcher = routes_matchers[index]
+ if routes_matcher === @request.path_info
+ last_match_index = Regexp.last_match.captures.find_index {|x| x} + index
+ route = routes[last_match_index]
+ if route.constraints_match?(@request)
+ return route
+ else
+ return match_route(last_match_index+1)
+ end
+ end
+ nil
+ end
+
+ def routes_matchers
+ @multiplex_routes[@request.request_method]
end
def routes
diff --git a/lib/newark/route.rb b/lib/newark/route.rb
index 9401784..0c4c786 100644
--- a/lib/newark/route.rb
+++ b/lib/newark/route.rb
@@ -1,61 +1,46 @@
module Newark
class Route
- PARAM_MATCHER = /:(?[^\/]*)/.freeze
- PARAM_SUB = /:[^\/]*/.freeze
- PATH_MATCHER = /\*(?.*)/.freeze
- PATH_SUB = /\*.*/.freeze
+ PLACEHOLDER_REGEXP = {
+ /:(\w+)/ => "([^#?/]+)", # any wildcard param that starts with ":"
+ /\\\*(\w+)/ => "([^#?]+)" # any wildcard param that starts with "*"
+ }
- attr_reader :handler
+ attr_reader :handler, :regex, :keys
def initialize(path, constraints, handler)
fail ArgumentError, 'You must define a route handler' if handler.nil?
@constraints = Constraint.load(constraints)
@handler = handler
- @path = path_matcher(path)
+ @regex, @keys = path_matcher(path)
end
- def match?(request)
- path_data = path_match?(request)
- (path_data && constraints_match?(request)).tap do |matched|
- if matched
- request.params.merge! Hash[ path_data.names.zip( path_data.captures ) ]
- end
- end
- end
-
- private
-
def constraints_match?(request)
@constraints.all? { |constraint| constraint.match?(request) }
end
- def path_match?(request)
- @path.match(request.path_info)
- end
+ private
def path_matcher(path)
- return path if path.is_a? Regexp
- /^#{path_params(path.to_s)}$/
+ path.is_a?(Regexp) ? [path, []] : compile(path)
end
- def path_params(path)
- match_path(path)
- match_params(path)
- path != '/' ? path.sub(/\/$/, '') : path
- end
-
- def match_path(path)
- if match = PATH_MATCHER.match(path)
- path.sub!(PATH_SUB, "(?<#{match[:path]}>.*)")
- end
- end
-
- def match_params(path)
- while match = PARAM_MATCHER.match(path)
- path.sub!(PARAM_SUB, "(?<#{match[:param]}>[^\/]*)")
+ # compiles a path pattern to derive a regex and all the keys
+ def compile(path_pattern)
+ keys = []
+ segments = []
+ path_pattern.split("/").each do |segment|
+ segments << Regexp.escape(segment).tap do |reg|
+ PLACEHOLDER_REGEXP.each do |placeholder, replacement|
+ reg.gsub!(placeholder) do
+ keys << $1
+ replacement
+ end
+ end
+ end
end
+ return Regexp.new(segments.any? ? segments.join(?/) : ?/), keys
end
class Constraint
diff --git a/test/test_router.rb b/test/test_router.rb
index 6095bc6..2e0b9d1 100644
--- a/test/test_router.rb
+++ b/test/test_router.rb
@@ -60,6 +60,18 @@ class TestingApp
'no_trailing_slash'
end
+ get '/extension_test/:id.xml' do
+ "matched in xml: #{params[:id]}"
+ end
+
+ get '/multiple_params_extension_test/:id.:format' do
+ "matched in #{params[:format]}: #{params[:id]}"
+ end
+
+ get '/wildcard_path_test/*path.xml' do
+ "matched in #{params[:path]}"
+ end
+
end
class TestRouter < MiniTest::Unit::TestCase
@@ -153,4 +165,26 @@ def test_deals_with_no_trailing_slashes
assert last_response.ok?
assert_equal 'no_trailing_slash', last_response.body
end
+
+ def test_xml_extension_matching
+ get '/extension_test/123.xml'
+ assert last_response.ok?
+ assert_equal 'matched in xml: 123', last_response.body
+ end
+
+ def test_xml_extension_matching
+ get '/multiple_params_extension_test/abc.json'
+ assert last_response.ok?
+ assert_equal 'matched in json: abc', last_response.body
+ end
+
+ def test_wildcard_path_matching
+ get '/wildcard_path_test/abc/def/g.xml'
+ assert last_response.ok?
+ assert_equal 'matched in abc/def/g', last_response.body
+
+ get '/wildcard_path_test/abc/def/g.json'
+ assert_equal 404, last_response.status
+ end
+
end