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,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