rack-mount 0.0.1 → 0.8.3

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 (43) hide show
  1. data/README.rdoc +12 -4
  2. data/lib/rack/mount/analysis/histogram.rb +55 -6
  3. data/lib/rack/mount/analysis/splitting.rb +103 -89
  4. data/lib/rack/mount/code_generation.rb +120 -0
  5. data/lib/rack/mount/generatable_regexp.rb +95 -48
  6. data/lib/rack/mount/multimap.rb +84 -41
  7. data/lib/rack/mount/prefix.rb +13 -8
  8. data/lib/rack/mount/regexp_with_named_groups.rb +27 -7
  9. data/lib/rack/mount/route.rb +75 -18
  10. data/lib/rack/mount/route_set.rb +308 -22
  11. data/lib/rack/mount/strexp/parser.rb +160 -0
  12. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  13. data/lib/rack/mount/strexp.rb +54 -79
  14. data/lib/rack/mount/utils.rb +65 -174
  15. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  16. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  17. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  18. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  19. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  20. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  21. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  22. data/lib/rack/mount/vendor/regin/regin/group.rb +90 -0
  23. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  24. data/lib/rack/mount/vendor/regin/regin/parser.rb +546 -0
  25. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +255 -0
  26. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  27. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  28. data/lib/rack/mount/version.rb +3 -0
  29. data/lib/rack/mount.rb +13 -17
  30. metadata +88 -35
  31. data/lib/rack/mount/analysis/frequency.rb +0 -51
  32. data/lib/rack/mount/const.rb +0 -45
  33. data/lib/rack/mount/exceptions.rb +0 -3
  34. data/lib/rack/mount/generation/route.rb +0 -57
  35. data/lib/rack/mount/generation/route_set.rb +0 -163
  36. data/lib/rack/mount/meta_method.rb +0 -104
  37. data/lib/rack/mount/mixover.rb +0 -47
  38. data/lib/rack/mount/recognition/code_generation.rb +0 -99
  39. data/lib/rack/mount/recognition/route.rb +0 -59
  40. data/lib/rack/mount/recognition/route_set.rb +0 -88
  41. data/lib/rack/mount/vendor/multimap/multimap.rb +0 -466
  42. data/lib/rack/mount/vendor/multimap/multiset.rb +0 -153
  43. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +0 -156
@@ -3,16 +3,12 @@ require 'rack/mount/route'
3
3
  require 'rack/mount/utils'
4
4
 
5
5
  module Rack::Mount
6
- class RouteSet
7
- extend Mixover
8
-
9
- # Include generation and recognition concerns
10
- include Generation::RouteSet, Recognition::RouteSet
11
- include Recognition::CodeGeneration
6
+ class RoutingError < StandardError; end
12
7
 
8
+ class RouteSet
13
9
  # Initialize a new RouteSet without optimizations
14
- def self.new_without_optimizations(*args, &block)
15
- new_without_module(Recognition::CodeGeneration, *args, &block)
10
+ def self.new_without_optimizations(options = {}, &block)
11
+ new(options.merge(:_optimize => false), &block)
16
12
  end
17
13
 
18
14
  # Basic RouteSet initializer.
@@ -23,7 +19,22 @@ module Rack::Mount
23
19
  # - <tt>Generation::RouteSet.new</tt>
24
20
  # - <tt>Recognition::RouteSet.new</tt>
25
21
  def initialize(options = {}, &block)
22
+ @parameters_key = options.delete(:parameters_key) || 'rack.routing_args'
23
+ @parameters_key.freeze
24
+
25
+ @named_routes = {}
26
+
27
+ @recognition_key_analyzer = Analysis::Splitting.new
28
+
29
+ @generation_keys = [:controller, :action]
30
+ @generation_route_keys = []
31
+
26
32
  @request_class = options.delete(:request_class) || Rack::Request
33
+ @valid_conditions = @request_class.public_instance_methods.map! { |m| m.to_sym }
34
+
35
+ extend CodeGeneration unless options[:_optimize] == false
36
+ @optimized_recognize_defined = false
37
+
27
38
  @routes = []
28
39
  expire!
29
40
 
@@ -40,28 +51,201 @@ module Rack::Mount
40
51
  # Conditions may be expressed as strings or
41
52
  # regexps to match against.
42
53
  # <tt>defaults</tt>:: A hash of values that always gets merged in
43
- # <tt>name</tt>:: Symbol identifier for the route used with named
54
+ # <tt>name</tt>:: Symbol identifier for the route used with named
44
55
  # route generations
45
56
  def add_route(app, conditions = {}, defaults = {}, name = nil)
46
- route = Route.new(self, app, conditions, defaults, name)
57
+ unless conditions.is_a?(Hash)
58
+ raise ArgumentError, 'conditions must be a Hash'
59
+ end
60
+
61
+ unless conditions.all? { |method, pattern|
62
+ @valid_conditions.include?(method)
63
+ }
64
+ raise ArgumentError, 'conditions may only include ' +
65
+ @valid_conditions.inspect
66
+ end
67
+
68
+ route = Route.new(app, conditions, defaults, name)
47
69
  @routes << route
70
+
71
+ @recognition_key_analyzer << route.conditions
72
+
73
+ @named_routes[route.name] = route if route.name
74
+ @generation_route_keys << route.generation_keys
75
+
48
76
  expire!
49
77
  route
50
78
  end
51
79
 
52
- # See <tt>Recognition::RouteSet#call</tt>
80
+ def recognize(obj)
81
+ raise 'route set not finalized' unless @recognition_graph
82
+
83
+ cache = {}
84
+ keys = @recognition_keys.map { |key|
85
+ if key.respond_to?(:call)
86
+ key.call(cache, obj)
87
+ else
88
+ obj.send(key)
89
+ end
90
+ }
91
+
92
+ @recognition_graph[*keys].each do |route|
93
+ matches = {}
94
+ params = route.defaults.dup
95
+
96
+ if route.conditions.all? { |method, condition|
97
+ value = obj.send(method)
98
+ if condition.is_a?(Regexp) && (m = value.match(condition))
99
+ matches[method] = m
100
+ captures = m.captures
101
+ route.named_captures[method].each do |k, i|
102
+ if v = captures[i]
103
+ params[k] = v
104
+ end
105
+ end
106
+ true
107
+ elsif value == condition
108
+ true
109
+ else
110
+ false
111
+ end
112
+ }
113
+ if block_given?
114
+ yield route, matches, params
115
+ else
116
+ return route, matches, params
117
+ end
118
+ end
119
+ end
120
+
121
+ nil
122
+ end
123
+
124
+ X_CASCADE = 'X-Cascade'.freeze
125
+ PASS = 'pass'.freeze
126
+ PATH_INFO = 'PATH_INFO'.freeze
127
+
128
+ # Rack compatible recognition and dispatching method. Routes are
129
+ # tried until one returns a non-catch status code. If no routes
130
+ # match, then catch status code is returned.
131
+ #
132
+ # This method can only be invoked after the RouteSet has been
133
+ # finalized.
53
134
  def call(env)
54
- raise NotImplementedError
135
+ raise 'route set not finalized' unless @recognition_graph
136
+
137
+ env[PATH_INFO] = Utils.normalize_path(env[PATH_INFO])
138
+
139
+ request = nil
140
+ req = @request_class.new(env)
141
+ recognize(req) do |route, matches, params|
142
+ # TODO: We only want to unescape params from uri related methods
143
+ params.each { |k, v| params[k] = Utils.unescape_uri(v) if v.is_a?(String) }
144
+
145
+ if route.prefix?
146
+ env[Prefix::KEY] = matches[:path_info].to_s
147
+ end
148
+
149
+ old_params = env[@parameters_key]
150
+ env[@parameters_key] = (old_params || {}).merge(params)
151
+
152
+ result = route.app.call(env)
153
+
154
+ if result[1][X_CASCADE] == PASS
155
+ env[@parameters_key] = old_params
156
+ else
157
+ return result
158
+ end
159
+ end
160
+
161
+ request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']]
55
162
  end
56
163
 
57
- # See <tt>Generation::RouteSet#url</tt>
58
- def url(*args)
59
- raise NotImplementedError
164
+ # Generates a url from Rack env and identifiers or significant keys.
165
+ #
166
+ # To generate a url by named route, pass the name in as a +Symbol+.
167
+ # url(env, :dashboard) # => "/dashboard"
168
+ #
169
+ # Additional parameters can be passed in as a hash
170
+ # url(env, :people, :id => "1") # => "/people/1"
171
+ #
172
+ # If no named route is given, it will fall back to a slower
173
+ # generation search.
174
+ # url(env, :controller => "people", :action => "show", :id => "1")
175
+ # # => "/people/1"
176
+ def url(env, *args)
177
+ named_route, params = nil, {}
178
+
179
+ case args.length
180
+ when 2
181
+ named_route, params = args[0], args[1].dup
182
+ when 1
183
+ if args[0].is_a?(Hash)
184
+ params = args[0].dup
185
+ else
186
+ named_route = args[0]
187
+ end
188
+ else
189
+ raise ArgumentError
190
+ end
191
+
192
+ only_path = params.delete(:only_path)
193
+ recall = env[@parameters_key] || {}
194
+
195
+ unless result = generate(:all, named_route, params, recall,
196
+ :parameterize => lambda { |name, param| Utils.escape_uri(param) })
197
+ return
198
+ end
199
+
200
+ parts, params = result
201
+ return unless parts
202
+
203
+ params.each do |k, v|
204
+ if v
205
+ params[k] = v
206
+ else
207
+ params.delete(k)
208
+ end
209
+ end
210
+
211
+ req = stubbed_request_class.new(env)
212
+ req._stubbed_values = parts.merge(:query_string => Utils.build_nested_query(params))
213
+ only_path ? req.fullpath : req.url
60
214
  end
61
215
 
62
- # See <tt>Generation::RouteSet#generate</tt>
63
- def generate(*args)
64
- raise NotImplementedError
216
+ def generate(method, *args) #:nodoc:
217
+ raise 'route set not finalized' unless @generation_graph
218
+
219
+ method = nil if method == :all
220
+ named_route, params, recall, options = extract_params!(*args)
221
+ merged = recall.merge(params)
222
+ route = nil
223
+
224
+ if named_route
225
+ if route = @named_routes[named_route.to_sym]
226
+ recall = route.defaults.merge(recall)
227
+ url = route.generate(method, params, recall, options)
228
+ [url, params]
229
+ else
230
+ raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
231
+ end
232
+ else
233
+ keys = @generation_keys.map { |key|
234
+ if k = merged[key]
235
+ k.to_s
236
+ else
237
+ nil
238
+ end
239
+ }
240
+ @generation_graph[*keys].each do |r|
241
+ next unless r.significant_params?
242
+ if url = r.generate(method, params, recall, options)
243
+ return [url, params]
244
+ end
245
+ end
246
+
247
+ raise RoutingError, "No route matches #{params.inspect}"
248
+ end
65
249
  end
66
250
 
67
251
  # Number of routes in the set
@@ -70,6 +254,13 @@ module Rack::Mount
70
254
  end
71
255
 
72
256
  def rehash #:nodoc:
257
+ Utils.debug "rehashing"
258
+
259
+ @recognition_keys = build_recognition_keys
260
+ @recognition_graph = build_recognition_graph
261
+ @generation_graph = build_generation_graph
262
+
263
+ self
73
264
  end
74
265
 
75
266
  # Finalizes the set and builds optimized data structures. You *must*
@@ -78,6 +269,12 @@ module Rack::Mount
78
269
  def freeze
79
270
  unless frozen?
80
271
  rehash
272
+ stubbed_request_class
273
+
274
+ @recognition_key_analyzer = nil
275
+ @generation_route_keys = nil
276
+ @valid_conditions = nil
277
+
81
278
  @routes.each { |route| route.freeze }
82
279
  @routes.freeze
83
280
  end
@@ -85,8 +282,21 @@ module Rack::Mount
85
282
  super
86
283
  end
87
284
 
285
+ protected
286
+ def recognition_stats
287
+ { :keys => @recognition_keys,
288
+ :keys_size => @recognition_keys.size,
289
+ :graph_size => @recognition_graph.size,
290
+ :graph_height => @recognition_graph.height,
291
+ :graph_average_height => @recognition_graph.average_height }
292
+ end
293
+
88
294
  private
89
295
  def expire! #:nodoc:
296
+ @recognition_keys = @recognition_graph = nil
297
+ @recognition_key_analyzer.expire!
298
+
299
+ @generation_graph = nil
90
300
  end
91
301
 
92
302
  # An internal helper method for constructing a nested set from
@@ -98,12 +308,88 @@ module Rack::Mount
98
308
  def build_nested_route_set(keys, &block)
99
309
  graph = Multimap.new
100
310
  @routes.each_with_index do |route, index|
101
- k = keys.map { |key| block.call(key, index) }
102
- Utils.pop_trailing_nils!(k)
103
- k.map! { |key| key || /.+/ }
104
- graph[*k] = route
311
+ catch(:skip) do
312
+ k = keys.map { |key| block.call(key, index) }
313
+ Utils.pop_trailing_blanks!(k)
314
+ k.map! { |key| key || /.*/ }
315
+ graph[*k] = route
316
+ end
105
317
  end
106
318
  graph
107
319
  end
320
+
321
+ def build_recognition_graph
322
+ build_nested_route_set(@recognition_keys) { |k, i|
323
+ @recognition_key_analyzer.possible_keys[i][k]
324
+ }
325
+ end
326
+
327
+ def build_recognition_keys
328
+ keys = @recognition_key_analyzer.report
329
+ Utils.debug "recognition keys - #{keys.inspect}"
330
+ keys
331
+ end
332
+
333
+ def build_generation_graph
334
+ build_nested_route_set(@generation_keys) { |k, i|
335
+ throw :skip unless @routes[i].significant_params?
336
+
337
+ if k = @generation_route_keys[i][k]
338
+ k.to_s
339
+ else
340
+ nil
341
+ end
342
+ }
343
+ end
344
+
345
+ def extract_params!(*args)
346
+ case args.length
347
+ when 4
348
+ named_route, params, recall, options = args
349
+ when 3
350
+ if args[0].is_a?(Hash)
351
+ params, recall, options = args
352
+ else
353
+ named_route, params, recall = args
354
+ end
355
+ when 2
356
+ if args[0].is_a?(Hash)
357
+ params, recall = args
358
+ else
359
+ named_route, params = args
360
+ end
361
+ when 1
362
+ if args[0].is_a?(Hash)
363
+ params = args[0]
364
+ else
365
+ named_route = args[0]
366
+ end
367
+ else
368
+ raise ArgumentError
369
+ end
370
+
371
+ named_route ||= nil
372
+ params ||= {}
373
+ recall ||= {}
374
+ options ||= {}
375
+
376
+ [named_route, params.dup, recall.dup, options.dup]
377
+ end
378
+
379
+ def stubbed_request_class
380
+ @stubbed_request_class ||= begin
381
+ klass = Class.new(@request_class)
382
+ klass.public_instance_methods.each do |method|
383
+ next if method =~ /^__|object_id/
384
+ klass.class_eval <<-RUBY
385
+ def #{method}(*args, &block)
386
+ @_stubbed_values[:#{method}] || super
387
+ end
388
+ RUBY
389
+ end
390
+ klass.class_eval { attr_accessor :_stubbed_values }
391
+ klass
392
+ end
393
+ end
108
394
  end
109
395
  end
@@ -0,0 +1,160 @@
1
+ #
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by Racc 1.4.6
4
+ # from Racc grammer file "".
5
+ #
6
+
7
+ require 'racc/parser.rb'
8
+
9
+ require 'rack/mount/utils'
10
+ require 'rack/mount/strexp/tokenizer'
11
+
12
+ module Rack
13
+ module Mount
14
+ class StrexpParser < Racc::Parser
15
+
16
+
17
+ if Regin.regexp_supports_named_captures?
18
+ REGEXP_NAMED_CAPTURE = '(?<%s>%s)'.freeze
19
+ else
20
+ REGEXP_NAMED_CAPTURE = '(?:<%s>%s)'.freeze
21
+ end
22
+
23
+ attr_accessor :anchor, :requirements
24
+ ##### State transition tables begin ###
25
+
26
+ racc_action_table = [
27
+ 1, 2, 3, 9, 4, 1, 2, 3, 12, 4,
28
+ 1, 2, 3, 11, 4, 1, 2, 3, nil, 4 ]
29
+
30
+ racc_action_check = [
31
+ 0, 0, 0, 5, 0, 3, 3, 3, 9, 3,
32
+ 8, 8, 8, 8, 8, 6, 6, 6, nil, 6 ]
33
+
34
+ racc_action_pointer = [
35
+ -2, nil, nil, 3, nil, 3, 13, nil, 8, 8,
36
+ nil, nil, nil ]
37
+
38
+ racc_action_default = [
39
+ -8, -4, -5, -8, -7, -8, -1, -3, -8, -8,
40
+ -2, -6, 13 ]
41
+
42
+ racc_goto_table = [
43
+ 6, 5, 10, 8, 10 ]
44
+
45
+ racc_goto_check = [
46
+ 2, 1, 3, 2, 3 ]
47
+
48
+ racc_goto_pointer = [
49
+ nil, 1, 0, -4 ]
50
+
51
+ racc_goto_default = [
52
+ nil, nil, nil, 7 ]
53
+
54
+ racc_reduce_table = [
55
+ 0, 0, :racc_error,
56
+ 1, 8, :_reduce_1,
57
+ 2, 9, :_reduce_2,
58
+ 1, 9, :_reduce_none,
59
+ 1, 10, :_reduce_4,
60
+ 1, 10, :_reduce_5,
61
+ 3, 10, :_reduce_6,
62
+ 1, 10, :_reduce_7 ]
63
+
64
+ racc_reduce_n = 8
65
+
66
+ racc_shift_n = 13
67
+
68
+ racc_token_table = {
69
+ false => 0,
70
+ :error => 1,
71
+ :PARAM => 2,
72
+ :GLOB => 3,
73
+ :LPAREN => 4,
74
+ :RPAREN => 5,
75
+ :CHAR => 6 }
76
+
77
+ racc_nt_base = 7
78
+
79
+ racc_use_result_var = true
80
+
81
+ Racc_arg = [
82
+ racc_action_table,
83
+ racc_action_check,
84
+ racc_action_default,
85
+ racc_action_pointer,
86
+ racc_goto_table,
87
+ racc_goto_check,
88
+ racc_goto_default,
89
+ racc_goto_pointer,
90
+ racc_nt_base,
91
+ racc_reduce_table,
92
+ racc_token_table,
93
+ racc_shift_n,
94
+ racc_reduce_n,
95
+ racc_use_result_var ]
96
+
97
+ Racc_token_to_s_table = [
98
+ "$end",
99
+ "error",
100
+ "PARAM",
101
+ "GLOB",
102
+ "LPAREN",
103
+ "RPAREN",
104
+ "CHAR",
105
+ "$start",
106
+ "target",
107
+ "expr",
108
+ "token" ]
109
+
110
+ Racc_debug_parser = false
111
+
112
+ ##### State transition tables end #####
113
+
114
+ # reduce 0 omitted
115
+
116
+ def _reduce_1(val, _values, result)
117
+ result = anchor ? "\\A#{val.join}\\Z" : "\\A#{val.join}"
118
+ result
119
+ end
120
+
121
+ def _reduce_2(val, _values, result)
122
+ result = val.join
123
+ result
124
+ end
125
+
126
+ # reduce 3 omitted
127
+
128
+ def _reduce_4(val, _values, result)
129
+ name = val[0].to_sym
130
+ requirement = requirements[name]
131
+ result = REGEXP_NAMED_CAPTURE % [name, requirement]
132
+
133
+ result
134
+ end
135
+
136
+ def _reduce_5(val, _values, result)
137
+ name = val[0].to_sym
138
+ requirement = requirements.key?(name) ? requirements[name] : '.+'
139
+ result = REGEXP_NAMED_CAPTURE % [name, requirement]
140
+
141
+ result
142
+ end
143
+
144
+ def _reduce_6(val, _values, result)
145
+ result = "(?:#{val[1]})?"
146
+ result
147
+ end
148
+
149
+ def _reduce_7(val, _values, result)
150
+ result = Regexp.escape(val[0])
151
+ result
152
+ end
153
+
154
+ def _reduce_none(val, _values, result)
155
+ val[0]
156
+ end
157
+
158
+ end # class StrexpParser
159
+ end # module Mount
160
+ end # module Rack
@@ -0,0 +1,83 @@
1
+ #--
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by rex 1.0.5.beta1
4
+ # from lexical definition file "lib/rack/mount/strexp/tokenizer.rex".
5
+ #++
6
+
7
+ require 'racc/parser'
8
+ class Rack::Mount::StrexpParser < Racc::Parser
9
+ require 'strscan'
10
+
11
+ class ScanError < StandardError ; end
12
+
13
+ attr_reader :lineno
14
+ attr_reader :filename
15
+ attr_accessor :state
16
+
17
+ def scan_setup(str)
18
+ @ss = StringScanner.new(str)
19
+ @lineno = 1
20
+ @state = nil
21
+ end
22
+
23
+ def action
24
+ yield
25
+ end
26
+
27
+ def scan_str(str)
28
+ scan_setup(str)
29
+ do_parse
30
+ end
31
+ alias :scan :scan_str
32
+
33
+ def load_file( filename )
34
+ @filename = filename
35
+ open(filename, "r") do |f|
36
+ scan_setup(f.read)
37
+ end
38
+ end
39
+
40
+ def scan_file( filename )
41
+ load_file(filename)
42
+ do_parse
43
+ end
44
+
45
+
46
+ def next_token
47
+ return if @ss.eos?
48
+
49
+ text = @ss.peek(1)
50
+ @lineno += 1 if text == "\n"
51
+ token = case @state
52
+ when nil
53
+ case
54
+ when (text = @ss.scan(/\\(\(|\)|:|\*)/))
55
+ action { [:CHAR, @ss[1]] }
56
+
57
+ when (text = @ss.scan(/\:([a-zA-Z_]\w*)/))
58
+ action { [:PARAM, @ss[1]] }
59
+
60
+ when (text = @ss.scan(/\*([a-zA-Z_]\w*)/))
61
+ action { [:GLOB, @ss[1]] }
62
+
63
+ when (text = @ss.scan(/\(/))
64
+ action { [:LPAREN, text] }
65
+
66
+ when (text = @ss.scan(/\)/))
67
+ action { [:RPAREN, text] }
68
+
69
+ when (text = @ss.scan(/./))
70
+ action { [:CHAR, text] }
71
+
72
+ else
73
+ text = @ss.string[@ss.pos .. -1]
74
+ raise ScanError, "can not match: '" + text + "'"
75
+ end # if
76
+
77
+ else
78
+ raise ScanError, "undefined state: '" + state.to_s + "'"
79
+ end # case state
80
+ token
81
+ end # def next_token
82
+
83
+ end # class