http_router 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,9 @@
1
+ class HttpRouter
2
+ class Node
3
+ class Scheme < AbstractRequestNode
4
+ def initialize(router, parent, schemes)
5
+ super(router, parent, schemes, :scheme)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class HttpRouter
2
+ class Node
3
+ class UserAgent < AbstractRequestNode
4
+ def initialize(router, parent, user_agents)
5
+ super(router, parent, user_agents, :user_agent)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ class HttpRouter
2
+ module RegexRouteGeneration
3
+ def url_with_params(*a)
4
+ url_args_processing(a) do |args, options|
5
+ respond_to?(:raw_url) or raise InvalidRouteException
6
+ raw_url(args, options)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -1,34 +1,24 @@
1
1
  class HttpRouter
2
2
  class Request
3
- attr_accessor :path, :params, :rack_request, :extra_env, :continue, :passed_with
4
- attr_reader :matches
3
+ attr_accessor :path, :params, :rack_request, :extra_env, :continue, :passed_with, :called
4
+ attr_reader :acceptable_methods
5
5
  alias_method :rack, :rack_request
6
- def initialize(path, rack_request, perform_call)
7
- @rack_request, @perform_call = rack_request, perform_call
6
+ alias_method :called?, :called
7
+
8
+ def initialize(path, rack_request)
9
+ @rack_request = rack_request
8
10
  @path = URI.unescape(path).split(/\//)
9
11
  @path.shift if @path.first == ''
10
12
  @path.push('') if path[-1] == ?/
11
13
  @extra_env = {}
12
14
  @params = []
13
- @matches = []
15
+ @acceptable_methods = Set.new
14
16
  end
15
17
 
16
18
  def joined_path
17
19
  @path * '/'
18
20
  end
19
21
 
20
- def matched_route(response)
21
- @matches << response
22
- end
23
-
24
- def perform_call
25
- @perform_call == true
26
- end
27
-
28
- def testing_405?
29
- @perform_call == 405
30
- end
31
-
32
22
  def to_s
33
23
  "request path, #{path.inspect}"
34
24
  end
@@ -6,6 +6,10 @@ class HttpRouter
6
6
  @params = path.hashify_params(request.params)
7
7
  end
8
8
 
9
+ def route
10
+ path.route
11
+ end
12
+
9
13
  def param_values
10
14
  request.params
11
15
  end
@@ -1,291 +1,30 @@
1
+ require 'set'
2
+
1
3
  class HttpRouter
2
4
  class Route
3
- attr_reader :default_values, :router, :path, :conditions, :original_path, :match_partially, :dest, :regex, :named, :matches_with
4
- alias_method :match_partially?, :match_partially
5
- alias_method :regex?, :regex
6
-
7
- def initialize(router, path, opts = {})
8
- @router, @original_path, @opts = router, path, opts
9
- if @original_path
10
- @match_partially = true and path.slice!(-1) if @original_path[/[^\\]\*$/]
11
- @original_path[0, 0] = '/' if @original_path[0] != ?/
12
- else
13
- @match_partially = true
14
- end
15
- process_opts
16
- end
17
-
18
- def process_opts
19
- @default_values = @opts[:__default_values__] || @opts[:default_values] || {}
20
- @arbitrary = @opts[:__arbitrary__] || @opts[:arbitrary]
21
- @matches_with = significant_variable_names.include?(:matching) ? @opts : @opts[:__matching__] || @opts[:matching] || {}
22
- significant_variable_names.each do |name|
23
- @matches_with[name] = @opts[name] if @opts.key?(name) && !@matches_with.key?(name)
24
- end
25
- @conditions = @opts[:__conditions__] || @opts[:conditions] || {}
26
- @match_partially = @opts[:__partial__] if @match_partially.nil? && !@opts[:__partial__].nil?
27
- @match_partially = @opts[:partial] if @match_partially.nil? && !@opts[:partial].nil?
28
- name(@opts[:__name__] || @opts[:name]) if @opts.key?(:__name__) || @opts.key?(:name)
29
- @needed_keys = significant_variable_names - @default_values.keys
30
- end
31
-
32
- def as_options
33
- {:__matching__ => @matches_with, :__conditions__ => @conditions, :__default_values__ => @default_values, :__name__ => @named, :__partial__ => @partially_match, :__arbitrary__ => @arbitrary}
34
- end
5
+ VALID_HTTP_VERBS = %w{GET POST PUT DELETE HEAD OPTIONS TRACE}
35
6
 
36
- def compiled?
37
- !@paths.nil?
38
- end
7
+ attr_reader :default_values, :router, :match_partially, :other_hosts, :paths, :request_methods, :name
8
+ attr_accessor :match_partially, :router, :host, :user_agent, :ignore_trailing_slash,
9
+ :path_for_generation, :path_validation_regex, :generator, :scheme, :original_path, :dest
39
10
 
40
- def partial(match_partially = true)
41
- @match_partially = match_partially
42
- self
43
- end
44
-
45
- def to(dest = nil, &dest2)
46
- @dest = dest || dest2
47
- add_path_to_tree
48
- self
49
- end
50
-
51
- def name(n)
52
- @named = n
53
- @router.named_routes[n] << self
54
- @router.named_routes[n].sort!{|r1, r2| r2.significant_variable_names.size <=> r1.significant_variable_names.size }
55
- self
56
- end
57
-
58
- def request_method(*method)
59
- add_to_contitions(:request_method, method)
60
- end
61
-
62
- def host(*host)
63
- add_to_contitions(:host, host)
64
- end
65
-
66
- def scheme(*scheme)
67
- add_to_contitions(:scheme, scheme)
68
- end
69
-
70
- def user_agent(*user_agent)
71
- add_to_contitions(:user_agent, user_agent)
72
- end
73
-
74
- def add_to_contitions(name, *vals)
75
- ((@conditions ||= {})[name] ||= []).concat(vals.flatten)
76
- self
77
- end
78
-
79
- def matching(matchers)
80
- @matches_with.merge!(matchers.is_a?(Array) ? Hash[*matchers] : matchers)
81
- self
82
- end
83
-
84
- def default(defaults)
85
- (@default_values ||= {}).merge!(defaults)
86
- self
87
- end
88
-
89
- # Sets the destination of this route to redirect to an arbitrary URL.
90
- def redirect(path, status = 302)
91
- raise ArgumentError, "Status has to be an integer between 300 and 399" unless (300..399).include?(status)
92
- to { |env|
93
- params = env['router.params']
94
- response = ::Rack::Response.new
95
- response.redirect(eval(%|"#{path}"|), status)
96
- response.finish
97
- }
98
- self
99
- end
100
-
101
- # Sets the destination of this route to serve static files from either a directory or a single file.
102
- def static(root)
103
- if File.directory?(root)
104
- partial.to ::Rack::File.new(root)
105
- else
106
- to {|env| env['PATH_INFO'] = File.basename(root); ::Rack::File.new(File.dirname(root)).call(env) }
107
- end
108
- self
109
- end
110
-
111
- def post; request_method('POST'); end
112
- def get; request_method('GET'); end
113
- def put; request_method('PUT'); end
114
- def delete; request_method('DELETE'); end
115
- def head; request_method('HEAD'); end
116
- def options; request_method('OPTIONS'); end
117
- def patch; request_method('PATCH'); end
118
- def trace; request_method('TRACE'); end
119
- def conenct; request_method('CONNECT'); end
120
-
121
- def arbitrary(blk = nil, &blk2)
122
- arbitrary_with_continue { |req, params|
123
- req.continue[(blk || blk2)[req, params]]
124
- }
125
- end
126
-
127
- def arbitrary_with_continue(blk = nil, &blk2)
128
- (@arbitrary ||= []) << (blk || blk2)
129
- self
130
- end
131
-
132
- def url(*args)
133
- result, extra_params = url_with_params(*args)
134
- append_querystring(result, extra_params)
135
- end
136
-
137
- def clone(new_router)
138
- Route.new(new_router, @original_path.dup, as_options)
139
- end
140
-
141
- def url_with_params(*a)
142
- url_args_processing(a) do |args, options|
143
- path = args.empty? ? matching_path(options) : matching_path(args, options)
144
- raise InvalidRouteException unless path
145
- path.url(args, options)
146
- end
147
- end
148
-
149
- def url_args_processing(args)
150
- options = args.last.is_a?(Hash) ? args.pop : nil
151
- options = options.nil? ? default_values.dup : default_values.merge(options) if default_values
152
- options.delete_if{ |k,v| v.nil? } if options
153
- result, params = yield args, options
154
- mount_point = router.url_mount && router.url_mount.url(options)
155
- mount_point ? [File.join(mount_point, result), params] : [result, params]
156
- end
157
-
158
- def significant_variable_names
159
- @significant_variable_names ||= @original_path.nil? ? [] : @original_path.scan(/(^|[^\\])[:\*]([a-zA-Z0-9_]+)/).map{|p| p.last.to_sym}
160
- end
161
-
162
- def matching_path(params, other_hash = nil)
163
- return @paths.first if @paths.size == 1
164
- case params
165
- when Array
166
- significant_keys = other_hash && significant_variable_names & other_hash.keys
167
- @paths.find { |path| path.param_names.size == (significant_keys ? params.size + significant_keys.size : params.size) }
168
- when Hash
169
- @paths.find { |path| (params && !params.empty? && (path.param_names & params.keys).size == path.param_names.size) || path.param_names.empty? }
170
- end
11
+ def create_clone(new_router)
12
+ r = clone
13
+ r.dest = (begin; dest.clone; rescue; dest; end)
14
+ r
171
15
  end
172
16
 
173
17
  def to_s
174
- "#<HttpRouter:Route #{object_id} @original_path=#{@original_path.inspect} @conditions=#{@conditions.inspect} @arbitrary=#{@arbitrary.inspect}>"
175
- end
176
-
177
- private
178
- def raw_paths
179
- return [] if @original_path.nil?
180
- @raw_paths ||= begin
181
- start_index, end_index = 0, 1
182
- @raw_paths, chars = [""], @original_path.split('')
183
- until chars.empty?
184
- case fc = chars.first[0]
185
- when ?(
186
- chars.shift
187
- (start_index...end_index).each { |path_index| raw_paths << raw_paths[path_index].dup }
188
- start_index = end_index
189
- end_index = raw_paths.size
190
- when ?)
191
- chars.shift
192
- start_index -= end_index - start_index
193
- else
194
- c = if chars[0][0] == ?\\ && (chars[1][0] == ?( || chars[1][0] == ?)); chars.shift; chars.shift; else; chars.shift; end
195
- (start_index...end_index).each { |path_index| raw_paths[path_index] << c }
196
- end
197
- end
198
- @raw_paths.reverse!
199
- end
200
- end
201
-
202
- def add_normal_part(node, part, param_names)
203
- name = part[1, part.size]
204
- node = case part[0]
205
- when ?\\
206
- node.add_lookup(part[1].chr)
207
- when ?:
208
- param_names << name.to_sym
209
- @matches_with[name.to_sym] ? node.add_spanning_match(@matches_with[name.to_sym]) : node.add_variable
210
- when ?*
211
- param_names << name.to_sym
212
- @matches_with[name.to_sym] ? node.add_glob_regexp(@matches_with[name.to_sym]) : node.add_glob
213
- else
214
- node.add_lookup(part)
215
- end
216
- end
217
-
218
- def add_complex_part(node, parts, param_names)
219
- capturing_indicies, splitting_indicies, captures, spans = [], [], 0, false
220
- regex = parts.inject('') do |reg, part|
221
- reg << case part[0]
222
- when ?\\ then Regexp.quote(part[1].chr)
223
- when ?:, ?*
224
- spans = true if part[0] == ?*
225
- captures += 1
226
- (part[0] == ?* ? splitting_indicies : capturing_indicies) << captures
227
- name = part[1, part.size].to_sym
228
- param_names << name
229
- if spans
230
- @matches_with[name] ? "((?:#{@matches_with[name]}\\/?)+)" : '(.*?)'
231
- else
232
- "(#{(@matches_with[name] || '[^/]*?')})"
233
- end
234
- else
235
- Regexp.quote(part)
236
- end
237
- end
238
- spans ? node.add_spanning_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies) :
239
- node.add_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies)
240
- end
241
-
242
- def add_path_to_tree
243
- raise DoubleCompileError if compiled?
244
- @paths ||= begin
245
- if raw_paths.empty?
246
- add_non_path_to_tree(@router.root, nil, [])
247
- else
248
- raw_paths.map do |path|
249
- param_names = []
250
- node = @router.root
251
- path.split(/\//).each do |part|
252
- next if part == ''
253
- parts = part.scan(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
254
- node = parts.size == 1 ? add_normal_part(node, part, param_names) : add_complex_part(node, parts, param_names)
255
- end
256
- add_non_path_to_tree(node, path, param_names)
257
- end
258
- end
259
- end
260
- end
261
-
262
- def add_non_path_to_tree(node, path, names)
263
- node = node.add_request(@conditions) unless @conditions.empty?
264
- @arbitrary.each{|a| node = node.add_arbitrary(a, match_partially?, names)} if @arbitrary
265
- path_obj = node.add_destination(self, path, names)
266
- if dest.respond_to?(:url_mount=)
267
- urlmount = UrlMount.new(@original_path, @default_values)
268
- urlmount.url_mount = router.url_mount if router.url_mount
269
- dest.url_mount = urlmount
270
- end
271
- path_obj
18
+ "#<HttpRouter:Route #{object_id} @path_for_generation=#{path_for_generation.inspect}>"
272
19
  end
273
20
 
274
- def append_querystring_value(uri, key, value)
275
- case value
276
- when Array then value.each{ |v| append_querystring_value(uri, "#{key}[]", v) }
277
- when Hash then value.each{ |k, v| append_querystring_value(uri, "#{key}[#{k}]", v) }
278
- else uri << '&' << CGI.escape(key.to_s) << '=' << CGI.escape(value.to_s)
279
- end
21
+ def matches_with(var_name)
22
+ @match_with && @match_with[:"#{var_name}"]
280
23
  end
281
24
 
282
- def append_querystring(uri, params)
283
- if params && !params.empty?
284
- uri_size = uri.size
285
- params.each{ |k,v| append_querystring_value(uri, k, v) }
286
- uri[uri_size] = ??
287
- end
288
- uri
25
+ def name=(name)
26
+ @name = name
27
+ router.named_routes[name] << self
289
28
  end
290
29
  end
291
30
  end
@@ -0,0 +1,126 @@
1
+ class HttpRouter
2
+ module RouteHelper
3
+ def path
4
+ @route.path_for_generation
5
+ end
6
+
7
+ def path=(path)
8
+ @original_path = path
9
+ if path.respond_to?(:[]) and path[/[^\\]\*$/]
10
+ @match_partially = true
11
+ @path_for_generation = path[0..path.size - 2]
12
+ else
13
+ @path_for_generation = path
14
+ end
15
+ end
16
+
17
+ def name(name = nil)
18
+ if name
19
+ self.name = name
20
+ self
21
+ else
22
+ @name
23
+ end
24
+ end
25
+
26
+ def add_default_values(hash)
27
+ @default_values ||= {}
28
+ @default_values.merge!(hash)
29
+ end
30
+
31
+ def add_match_with(matchers)
32
+ @match_with ||= {}
33
+ @match_with.merge!(matchers)
34
+ end
35
+
36
+ def add_other_host(hosts)
37
+ (@other_hosts ||= []).concat(hosts)
38
+ end
39
+
40
+ def add_path(path)
41
+ (@paths ||= []) << path
42
+ end
43
+
44
+ def add_request_method(methods)
45
+ @request_methods ||= Set.new
46
+ methods = [methods] unless methods.is_a?(Array)
47
+ methods.each do |method|
48
+ method = method.to_s.upcase
49
+ raise unless Route::VALID_HTTP_VERBS.include?(method)
50
+ @request_methods << method
51
+ end
52
+ end
53
+
54
+ def process_opts(opts)
55
+ if opts[:conditions]
56
+ opts.merge!(opts[:conditions])
57
+ opts.delete(:conditions)
58
+ end
59
+ opts.each do |k, v|
60
+ if respond_to?(:"#{k}=")
61
+ send(:"#{k}=", v)
62
+ elsif respond_to?(:"add_#{k}")
63
+ send(:"add_#{k}", v)
64
+ else
65
+ add_match_with(k => v)
66
+ end
67
+ end
68
+ end
69
+
70
+ def to(dest = nil, &dest_block)
71
+ @dest = dest || dest_block || raise("you didn't specify a destination")
72
+ if @dest.respond_to?(:url_mount=)
73
+ urlmount = UrlMount.new(@path_for_generation, @default_values || {}) # TODO url mount should accept nil here.
74
+ urlmount.url_mount = @router.url_mount if @router.url_mount
75
+ @dest.url_mount = urlmount
76
+ end
77
+ self
78
+ end
79
+
80
+ def head
81
+ add_request_method "HEAD"
82
+ self
83
+ end
84
+
85
+ def get
86
+ add_request_method "GET"
87
+ self
88
+ end
89
+
90
+ def post
91
+ add_request_method "POST"
92
+ self
93
+ end
94
+
95
+ def put
96
+ add_request_method "PUT"
97
+ self
98
+ end
99
+
100
+ def delete
101
+ add_request_method "DELETE"
102
+ self
103
+ end
104
+
105
+ def redirect(path, status = 302)
106
+ raise ArgumentError, "Status has to be an integer between 300 and 399" unless (300..399).include?(status)
107
+ to { |env|
108
+ params = env['router.params']
109
+ response = ::Rack::Response.new
110
+ response.redirect(eval(%|"#{path}"|), status)
111
+ response.finish
112
+ }
113
+ end
114
+
115
+ # Sets the destination of this route to serve static files from either a directory or a single file.
116
+ def static(root)
117
+ @match_partially = true if File.directory?(root)
118
+ to File.directory?(root) ?
119
+ ::Rack::File.new(root) :
120
+ proc {|env|
121
+ env['PATH_INFO'] = File.basename(root)
122
+ ::Rack::File.new(File.dirname(root)).call(env)
123
+ }
124
+ end
125
+ end
126
+ end