http_router 0.10.2 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.gitignore +2 -1
  2. data/Rakefile +7 -5
  3. data/benchmarks/gen2.rb +1 -1
  4. data/benchmarks/rack_mount.rb +8 -14
  5. data/examples/rack_mapper.ru +12 -13
  6. data/examples/variable_with_regex.ru +1 -1
  7. data/http_router.gemspec +1 -1
  8. data/lib/http_router.rb +159 -62
  9. data/lib/http_router/generation_helper.rb +29 -0
  10. data/lib/http_router/generator.rb +150 -0
  11. data/lib/http_router/node.rb +27 -17
  12. data/lib/http_router/node/abstract_request_node.rb +31 -0
  13. data/lib/http_router/node/host.rb +9 -0
  14. data/lib/http_router/node/lookup.rb +8 -10
  15. data/lib/http_router/node/path.rb +23 -38
  16. data/lib/http_router/node/request_method.rb +16 -0
  17. data/lib/http_router/node/root.rb +104 -10
  18. data/lib/http_router/node/scheme.rb +9 -0
  19. data/lib/http_router/node/user_agent.rb +9 -0
  20. data/lib/http_router/regex_route_generation.rb +10 -0
  21. data/lib/http_router/request.rb +7 -17
  22. data/lib/http_router/response.rb +4 -0
  23. data/lib/http_router/route.rb +16 -277
  24. data/lib/http_router/route_helper.rb +126 -0
  25. data/lib/http_router/util.rb +1 -37
  26. data/lib/http_router/version.rb +1 -1
  27. data/test/common/generate.txt +1 -1
  28. data/test/generation.rb +15 -11
  29. data/test/generic.rb +2 -3
  30. data/test/helper.rb +15 -10
  31. data/test/rack/test_route.rb +0 -5
  32. data/test/test_misc.rb +50 -40
  33. data/test/test_mounting.rb +27 -26
  34. data/test/test_recognition.rb +1 -76
  35. metadata +104 -161
  36. data/.rspec +0 -1
  37. data/lib/http_router/node/arbitrary.rb +0 -30
  38. data/lib/http_router/node/request.rb +0 -52
  39. data/lib/http_router/rack.rb +0 -19
  40. data/lib/http_router/rack/builder.rb +0 -61
  41. data/lib/http_router/rack/url_map.rb +0 -16
  42. data/lib/http_router/regex_route.rb +0 -39
@@ -0,0 +1,29 @@
1
+ class HttpRouter
2
+ module GenerationHelper
3
+ def max_param_count
4
+ @generator.max_param_count
5
+ end
6
+
7
+ def url(*args)
8
+ @generator.url(*args)
9
+ rescue InvalidRouteException
10
+ nil
11
+ end
12
+
13
+ def url_ns(*args)
14
+ @generator.url_ns(*args)
15
+ rescue InvalidRouteException
16
+ nil
17
+ end
18
+
19
+ def path(*args)
20
+ @generator.path(*args)
21
+ rescue InvalidRouteException
22
+ nil
23
+ end
24
+
25
+ def param_names
26
+ @generator.param_names
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,150 @@
1
+ class HttpRouter
2
+ class Generator
3
+ SCHEME_PORTS = {'http' => 80, 'https' => 443}
4
+
5
+ class PathGenerator
6
+ attr_reader :path
7
+ attr_accessor :param_names
8
+ def initialize(route, path, validation_regex = nil)
9
+ @route = route
10
+ @path = path.dup
11
+ @param_names = []
12
+ if path.is_a?(String)
13
+ path[0, 0] = '/' unless path[0] == ?/
14
+ regex_parts = path.split(/([:\*][a-zA-Z0-9_]+)/)
15
+ regex, code = '', ''
16
+ dynamic = false
17
+ regex_parts.each_with_index do |part, index|
18
+ case part[0]
19
+ when ?:, ?*
20
+ if index != 0 && regex_parts[index - 1][-1] == ?\\
21
+ regex << Regexp.quote(part) unless validation_regex
22
+ code << part
23
+ dynamic = true
24
+ else
25
+ regex << (@route.matches_with(part[1, part.size].to_sym) || '.*?').to_s unless validation_regex
26
+ code << "\#{args.shift || (options && options.delete(:#{part[1, part.size]})) || return}"
27
+ dynamic = true
28
+ end
29
+ else
30
+ regex << Regexp.quote(part) unless validation_regex
31
+ code << part
32
+ end
33
+ end
34
+ validation_regex ||= Regexp.new("^#{regex}$") if dynamic
35
+ if validation_regex
36
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
37
+ def generate(args, options)
38
+ generated_path = \"#{code}\"
39
+ #{validation_regex.inspect}.match(generated_path) ? URI.escape(generated_path) : nil
40
+ end
41
+ EOT
42
+ else
43
+ instance_eval <<-EOT, __FILE__, __LINE__ + 1
44
+ def generate(args, options)
45
+ URI.escape(\"#{code}\")
46
+ end
47
+ EOT
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def initialize(route, paths)
54
+ @route, @paths = route, paths
55
+ @router = @route.router
56
+ @route.generator = self
57
+ @path_generators = @paths.map do |p|
58
+ generator = PathGenerator.new(route, p.is_a?(String) ? p : route.path_for_generation, p.is_a?(Regexp) ? p : nil)
59
+ end
60
+ end
61
+
62
+ def param_names
63
+ @param_names ||= @path_generators.map{|path| path.param_names}.flatten.uniq
64
+ end
65
+
66
+ def max_param_count
67
+ @max_param_count ||= @path_generators.map{|p| p.param_names.size}.max
68
+ end
69
+
70
+ def each_path
71
+ @path_generators.each {|p| yield p }
72
+ @path_generators.sort! do |p1, p2|
73
+ p2.param_names.size <=> p1.param_names.size
74
+ end
75
+ end
76
+
77
+ def url(*args)
78
+ "#{scheme_port.first}#{url_ns(*args)}"
79
+ end
80
+
81
+ def url_ns(*args)
82
+ "://#{@route.host || @router.default_host}#{scheme_port.last}#{path(*args)}"
83
+ end
84
+
85
+ def path(*args)
86
+ result, extra_params = path_with_params(*args)
87
+ append_querystring(result, extra_params)
88
+ end
89
+
90
+ private
91
+ def scheme_port
92
+ @scheme_port ||= begin
93
+ scheme = @route.scheme || @router.default_scheme
94
+ port = @router.default_port
95
+ port_part = SCHEME_PORTS.key?(scheme) && SCHEME_PORTS[scheme] == port ? '' : ":#{port}"
96
+ [scheme, port_part]
97
+ end
98
+ end
99
+
100
+ def path_with_params(*a)
101
+ path_args_processing(a) do |args, options|
102
+ path = args.empty? ? matching_path(options) : matching_path(args, options)
103
+ path &&= path.generate(args, options)
104
+ raise TooManyParametersException unless args.empty?
105
+ raise InvalidRouteException.new("Error generating #{@route.path_for_generation}") unless path
106
+ path ? [path, options] : nil
107
+ end
108
+ end
109
+
110
+ def path_args_processing(args)
111
+ options = args.last.is_a?(Hash) ? args.pop : nil
112
+ options = options.nil? ? @route.default_values.dup : @route.default_values.merge(options) if @route.default_values
113
+ options.delete_if{ |k,v| v.nil? } if options
114
+ result, params = yield args, options
115
+ mount_point = @router.url_mount && (options ? @router.url_mount.url(options) : @router.url_mount.url)
116
+ mount_point ? [File.join(mount_point, result), params] : [result, params]
117
+ end
118
+
119
+ def matching_path(params, other_hash = nil)
120
+ return @path_generators.first if @path_generators.size == 1
121
+ case params
122
+ when Array, nil
123
+ @path_generators.find do |path|
124
+ significant_key_count = params ? params.size : 0
125
+ significant_key_count += (path.param_names & other_hash.keys).size if other_hash
126
+ significant_key_count >= path.param_names.size
127
+ end
128
+ when Hash
129
+ @path_generators.find { |path| (params && !params.empty? && (path.param_names & params.keys).size == path.param_names.size) || path.param_names.empty? }
130
+ end
131
+ end
132
+
133
+ def append_querystring_value(uri, key, value)
134
+ case value
135
+ when Array then value.each{ |v| append_querystring_value(uri, "#{key}[]", v) }
136
+ when Hash then value.each{ |k, v| append_querystring_value(uri, "#{key}[#{k}]", v) }
137
+ else uri << "&#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"
138
+ end
139
+ end
140
+
141
+ def append_querystring(uri, params)
142
+ if params && !params.empty?
143
+ uri_size = uri.size
144
+ params.each{ |k,v| append_querystring_value(uri, k, v) }
145
+ uri[uri_size] = ??
146
+ end
147
+ uri
148
+ end
149
+ end
150
+ end
@@ -1,17 +1,20 @@
1
1
  class HttpRouter
2
2
  class Node
3
- autoload :Root, 'http_router/node/root'
4
- autoload :Glob, 'http_router/node/glob'
5
- autoload :GlobRegex, 'http_router/node/glob_regex'
6
- autoload :Variable, 'http_router/node/variable'
7
- autoload :Regex, 'http_router/node/regex'
8
- autoload :SpanningRegex, 'http_router/node/spanning_regex'
9
- autoload :GlobRegex, 'http_router/node/glob_regex'
10
- autoload :FreeRegex, 'http_router/node/free_regex'
11
- autoload :Arbitrary, 'http_router/node/arbitrary'
12
- autoload :Request, 'http_router/node/request'
13
- autoload :Lookup, 'http_router/node/lookup'
14
- autoload :Path, 'http_router/node/path'
3
+ autoload :Root, 'http_router/node/root'
4
+ autoload :Glob, 'http_router/node/glob'
5
+ autoload :GlobRegex, 'http_router/node/glob_regex'
6
+ autoload :Variable, 'http_router/node/variable'
7
+ autoload :Regex, 'http_router/node/regex'
8
+ autoload :SpanningRegex, 'http_router/node/spanning_regex'
9
+ autoload :GlobRegex, 'http_router/node/glob_regex'
10
+ autoload :FreeRegex, 'http_router/node/free_regex'
11
+ autoload :AbstractRequestNode, 'http_router/node/abstract_request_node'
12
+ autoload :Host, 'http_router/node/host'
13
+ autoload :UserAgent, 'http_router/node/user_agent'
14
+ autoload :RequestMethod, 'http_router/node/request_method'
15
+ autoload :Scheme, 'http_router/node/scheme'
16
+ autoload :Lookup, 'http_router/node/lookup'
17
+ autoload :Path, 'http_router/node/path'
15
18
 
16
19
  attr_reader :router
17
20
 
@@ -31,13 +34,20 @@ class HttpRouter
31
34
  add(GlobRegex.new(@router, self, matcher))
32
35
  end
33
36
 
34
- def add_request(opts)
35
- raise unless opts
36
- add(Request.new(@router, self, opts))
37
+ def add_host(hosts)
38
+ add(Host.new(@router, self, hosts))
37
39
  end
38
40
 
39
- def add_arbitrary(blk, allow_partial, param_names)
40
- add(Arbitrary.new(@router, self, allow_partial, blk, param_names))
41
+ def add_user_agent(uas)
42
+ add(UserAgent.new(@router, self, uas))
43
+ end
44
+
45
+ def add_request_method(rm)
46
+ add(RequestMethod.new(@router, self, rm))
47
+ end
48
+
49
+ def add_scheme(scheme)
50
+ add(Scheme.new(@router, self, scheme))
41
51
  end
42
52
 
43
53
  def add_match(regexp, matching_indicies = [0], splitting_indicies = nil)
@@ -0,0 +1,31 @@
1
+ class HttpRouter
2
+ class Node
3
+ class AbstractRequestNode < Node
4
+ attr_reader :request_method, :tests
5
+
6
+ def initialize(route, parent, tests, request_method)
7
+ @request_method = request_method
8
+ @tests = case tests
9
+ when Array then tests
10
+ when Set then tests.to_a
11
+ else [tests]
12
+ end
13
+ super(route, parent)
14
+ end
15
+
16
+ def usable?(other)
17
+ other.class == self.class && other.tests == tests && other.request_method == request_method
18
+ end
19
+
20
+ def to_code
21
+ "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '}
22
+ #{super}
23
+ end"
24
+ end
25
+
26
+ def inspect_label
27
+ "#{self.class.name.split("::").last} #{tests.inspect} (#{@matchers.size} matchers)"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ class HttpRouter
2
+ class Node
3
+ class Host < AbstractRequestNode
4
+ def initialize(router, parent, hosts)
5
+ super(router, parent, hosts, :host)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -25,16 +25,14 @@ class HttpRouter
25
25
  end
26
26
 
27
27
  def to_code
28
- lookup_ivar = inject_root_ivar(@map)
29
- method_prefix = "lookup_#{root.next_counter} "
30
- inject_root_methods @map.keys.map {|k|
31
- method = :"#{method_prefix}#{k}"
32
- "define_method(#{method.inspect}) do |request|
33
- part = request.path.shift
34
- #{@map[k].map{|n| n.to_code} * "\n"}
35
- request.path.unshift part
36
- end"}.join("\n")
37
- "send(\"#{method_prefix}\#{request.path.first}\", request) if !request.path_finished? && #{lookup_ivar}.key?(request.path.first)"
28
+ part_name = "part#{root.next_counter}"
29
+ "unless request.path_finished?
30
+ #{part_name} = request.path.shift
31
+ case #{part_name}
32
+ #{@map.map{|k, v| "when #{k.inspect}; #{v.map(&:to_code) * "\n"};"} * "\n"}
33
+ end
34
+ request.path.unshift #{part_name}
35
+ end"
38
36
  end
39
37
  end
40
38
  end
@@ -1,52 +1,42 @@
1
1
  class HttpRouter
2
2
  class Node
3
3
  class Path < Node
4
- attr_reader :route, :param_names, :dynamic, :original_path
4
+ attr_reader :route, :param_names, :dynamic, :path
5
5
  alias_method :dynamic?, :dynamic
6
6
  def initialize(router, parent, route, path, param_names = [])
7
- @route, @original_path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
7
+ @route, @path, @param_names, @dynamic = route, path, param_names, !param_names.empty?
8
+ @route.add_path(self)
8
9
  raise AmbiguousVariableException, "You have duplicate variable name present: #{param_names.join(', ')}" if param_names.uniq.size != param_names.size
9
- Util.add_path_generation(self, route, @original_path) if @original_path.respond_to?(:split)
10
10
  super router, parent
11
- root.uncompile
11
+ router.uncompile
12
12
  end
13
13
 
14
14
  def hashify_params(params)
15
15
  @dynamic && params ? Hash[param_names.zip(params)] : {}
16
16
  end
17
17
 
18
- def url(args, options)
19
- if path = raw_url(args, options)
20
- raise TooManyParametersException unless args.empty?
21
- [URI.escape(path), options]
22
- end
23
- end
24
-
25
18
  def to_code
26
19
  path_ivar = inject_root_ivar(self)
27
- "#{"if request.path_finished?" unless route.match_partially?}
28
- catch(:pass) do
29
- #{"if request.path.size == 1 && request.path.first == '' && (request.rack_request.head? || request.rack_request.get?) && request.rack_request.path_info[-1] == ?/
30
- response = ::Rack::Response.new
31
- response.redirect(request.rack_request.path_info[0, request.rack_request.path_info.size - 1], 302)
32
- throw :success, response.finish
33
- end" if router.redirect_trailing_slash?}
20
+ "#{"if !callback && request.path.size == 1 && request.path.first == '' && (request.rack_request.head? || request.rack_request.get?) && request.rack_request.path_info[-1] == ?/
21
+ response = ::Rack::Response.new
22
+ response.redirect(request.rack_request.path_info[0, request.rack_request.path_info.size - 1], 302)
23
+ return response.finish
24
+ end" if router.redirect_trailing_slash?}
34
25
 
35
- #{"if request.path.empty?#{" or (request.path.size == 1 and request.path.first == '')" if router.ignore_trailing_slash?}" unless route.match_partially?}
36
- if request.perform_call
37
- env = request.rack_request.dup.env
38
- env['router.request'] = request
39
- env['router.params'] ||= {}
40
- #{"env['router.params'].merge!(Hash[#{param_names.inspect}.zip(request.params)])" if dynamic?}
41
- @router.rewrite#{"_partial" if route.match_partially?}_path_info(env, request)
42
- response = @router.process_destination_path(#{path_ivar}, env)
43
- router.pass_on_response(response) ? throw(:pass) : throw(:success, response)
44
- else
45
- request.matched_route(Response.new(request, #{path_ivar}))
46
- end
47
- #{"end" unless route.match_partially?}
26
+ #{"if request.#{router.ignore_trailing_slash? ? 'path_finished?' : 'path.empty?'}" unless route.match_partially}
27
+ if callback
28
+ request.called = true
29
+ callback.call(Response.new(request, #{path_ivar}))
30
+ else
31
+ env = request.rack_request.dup.env
32
+ env['router.request'] = request
33
+ env['router.params'] ||= {}
34
+ #{"env['router.params'].merge!(Hash[#{param_names.inspect}.zip(request.params)])" if dynamic?}
35
+ @router.rewrite#{"_partial" if route.match_partially}_path_info(env, request)
36
+ response = @router.process_destination_path(#{path_ivar}, env)
37
+ return response unless router.pass_on_response(response)
48
38
  end
49
- #{"end" unless route.match_partially?}"
39
+ #{"end" unless route.match_partially}"
50
40
  end
51
41
 
52
42
  def usable?(other)
@@ -54,12 +44,7 @@ class HttpRouter
54
44
  end
55
45
 
56
46
  def inspect_label
57
- "Path: #{original_path.inspect} for route #{route.named || 'unnamed route'} to #{route.dest.inspect}"
58
- end
59
-
60
- private
61
- def raw_url(args, options)
62
- raise InvalidRouteException
47
+ "Path: #{path.inspect} for route #{route.name || 'unnamed route'} to #{route.dest.inspect}"
63
48
  end
64
49
  end
65
50
  end
@@ -0,0 +1,16 @@
1
+ class HttpRouter
2
+ class Node
3
+ class RequestMethod < AbstractRequestNode
4
+ def initialize(router, parent, request_methods)
5
+ super(router, parent, request_methods, :request_method)
6
+ end
7
+
8
+ def to_code
9
+ "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '}
10
+ #{super}
11
+ end
12
+ request.acceptable_methods.merge(#{@tests.inspect})"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -8,14 +8,8 @@ class HttpRouter
8
8
  @counter, @methods_module = 0, Module.new
9
9
  end
10
10
 
11
- def [](request)
12
- compile
13
- self[request]
14
- end
15
- alias_method :compiling_lookup, :[]
16
-
17
11
  def uncompile
18
- instance_eval "undef :[]; alias :[] :compiling_lookup", __FILE__, __LINE__ if compiled?
12
+ instance_eval "undef :call; def call(req); raise 'uncompiled root'; end", __FILE__, __LINE__ if compiled?
19
13
  end
20
14
 
21
15
  def next_counter
@@ -36,12 +30,112 @@ class HttpRouter
36
30
  "Root (#{@matchers.size} matchers)"
37
31
  end
38
32
 
39
- private
40
- def compile
33
+ def compile(routes)
34
+ routes.each {|route| add_route(route)}
41
35
  root.extend(root.methods_module)
42
- instance_eval "def [](request)\n#{to_code}\nnil\nend", __FILE__, __LINE__
36
+ instance_eval "def call(request, &callback)\n#{to_code}\nnil\nend"
43
37
  @compiled = true
44
38
  end
39
+
40
+ private
41
+ def add_route(route)
42
+ paths = if route.path_for_generation.nil?
43
+ route.match_partially = true
44
+ []
45
+ elsif route.path_for_generation.is_a?(Regexp)
46
+ [route.path_for_generation]
47
+ else
48
+ path_for_generation = route.path_for_generation.dup
49
+ start_index, end_index = 0, 1
50
+ raw_paths, chars = [""], path_for_generation.split('')
51
+ until chars.empty?
52
+ case chars.first[0]
53
+ when ?(
54
+ chars.shift
55
+ (start_index...end_index).each { |path_index| raw_paths << raw_paths[path_index].dup }
56
+ start_index = end_index
57
+ end_index = raw_paths.size
58
+ when ?)
59
+ chars.shift
60
+ start_index -= end_index - start_index
61
+ else
62
+ c = if chars[0][0] == ?\\ && (chars[1][0] == ?( || chars[1][0] == ?)); chars.shift; chars.shift; else; chars.shift; end
63
+ (start_index...end_index).each { |path_index| raw_paths[path_index] << c }
64
+ end
65
+ end
66
+ raw_paths
67
+ end
68
+ paths.reverse!
69
+ if paths.empty?
70
+ add_non_path_to_tree(route, @router.root, nil, [])
71
+ else
72
+ Generator.new(route, paths).each_path do |path_generator|
73
+ case path_generator.path
74
+ when Regexp
75
+ path_generator.param_names = path_generator.path.names.map(&:to_sym) if path_generator.path.respond_to?(:names)
76
+ add_non_path_to_tree(route, add_free_match(path_generator.path), path_generator.path, path_generator.param_names)
77
+ else
78
+ node = self
79
+ path_generator.path.split(/\//).each do |part|
80
+ next if part == ''
81
+ parts = part.scan(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
82
+ node = parts.size == 1 ? add_normal_part(route, node, part, path_generator) : add_complex_part(route, node, parts, path_generator)
83
+ end
84
+ add_non_path_to_tree(route, node, path_generator.path, path_generator.param_names)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def add_normal_part(route, node, part, path_generator)
91
+ name = part[1, part.size]
92
+ node = case part[0]
93
+ when ?\\
94
+ node.add_lookup(part[1].chr)
95
+ when ?:
96
+ path_generator.param_names << name.to_sym
97
+ route.matches_with(name) ? node.add_spanning_match(route.matches_with(name)) : node.add_variable
98
+ when ?*
99
+ path_generator.param_names << name.to_sym
100
+ route.matches_with(name) ? node.add_glob_regexp(route.matches_with(name)) : node.add_glob
101
+ else
102
+ node.add_lookup(part)
103
+ end
104
+ end
105
+
106
+ def add_complex_part(route, node, parts, path_generator)
107
+ capturing_indicies, splitting_indicies, captures, spans = [], [], 0, false
108
+ regex = parts.inject('') do |reg, part|
109
+ reg << case part[0]
110
+ when ?\\ then Regexp.quote(part[1].chr)
111
+ when ?:, ?*
112
+ spans = true if part[0] == ?*
113
+ captures += 1
114
+ (part[0] == ?* ? splitting_indicies : capturing_indicies) << captures
115
+ name = part[1, part.size].to_sym
116
+ path_generator.param_names << name.to_sym
117
+ if spans
118
+ route.matches_with(name) ? "((?:#{route.matches_with(name)}\\/?)+)" : '(.*?)'
119
+ else
120
+ "(#{(route.matches_with(name) || '[^/]*?')})"
121
+ end
122
+ else
123
+ Regexp.quote(part)
124
+ end
125
+ end
126
+ spans ? node.add_spanning_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies) :
127
+ node.add_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies)
128
+ end
129
+
130
+ def add_non_path_to_tree(route, node, path, param_names)
131
+ node = node.add_host([route.host, route.other_hosts].flatten.compact) if route.host or route.other_hosts
132
+ node = node.add_user_agent(route.user_agent) if route.user_agent
133
+ node = node.add_scheme(route.scheme) if route.scheme
134
+ node = node.add_request_method(route.request_methods) if route.request_methods
135
+ path_obj = node.add_destination(route, path, param_names)
136
+ path_obj
137
+ end
138
+
45
139
  end
46
140
  end
47
141
  end