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