lgierth-rack-mount 0.6.13

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 (37) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +36 -0
  3. data/lib/rack/mount.rb +32 -0
  4. data/lib/rack/mount/analysis/frequency.rb +60 -0
  5. data/lib/rack/mount/analysis/histogram.rb +74 -0
  6. data/lib/rack/mount/analysis/splitting.rb +159 -0
  7. data/lib/rack/mount/code_generation.rb +117 -0
  8. data/lib/rack/mount/generatable_regexp.rb +210 -0
  9. data/lib/rack/mount/multimap.rb +53 -0
  10. data/lib/rack/mount/prefix.rb +36 -0
  11. data/lib/rack/mount/regexp_with_named_groups.rb +69 -0
  12. data/lib/rack/mount/route.rb +130 -0
  13. data/lib/rack/mount/route_set.rb +420 -0
  14. data/lib/rack/mount/strexp.rb +68 -0
  15. data/lib/rack/mount/strexp/parser.rb +160 -0
  16. data/lib/rack/mount/strexp/parser.y +34 -0
  17. data/lib/rack/mount/strexp/tokenizer.rb +83 -0
  18. data/lib/rack/mount/strexp/tokenizer.rex +12 -0
  19. data/lib/rack/mount/utils.rb +162 -0
  20. data/lib/rack/mount/vendor/multimap/multimap.rb +569 -0
  21. data/lib/rack/mount/vendor/multimap/multiset.rb +185 -0
  22. data/lib/rack/mount/vendor/multimap/nested_multimap.rb +158 -0
  23. data/lib/rack/mount/vendor/regin/regin.rb +75 -0
  24. data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
  25. data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
  26. data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
  27. data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
  28. data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
  29. data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
  30. data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
  31. data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
  32. data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
  33. data/lib/rack/mount/vendor/regin/regin/parser.rb +520 -0
  34. data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +246 -0
  35. data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
  36. data/lib/rack/mount/version.rb +3 -0
  37. metadata +140 -0
@@ -0,0 +1,420 @@
1
+ require 'rack/mount/multimap'
2
+ require 'rack/mount/route'
3
+ require 'rack/mount/utils'
4
+
5
+ module Rack::Mount
6
+ class RoutingError < StandardError; end
7
+
8
+ class RouteSet
9
+ # Initialize a new RouteSet without optimizations
10
+ def self.new_without_optimizations(options = {}, &block)
11
+ new(options.merge(:_optimize => false), &block)
12
+ end
13
+
14
+ # Basic RouteSet initializer.
15
+ #
16
+ # If a block is given, the set is yielded and finalized.
17
+ #
18
+ # See other aspects for other valid options:
19
+ # - <tt>Generation::RouteSet.new</tt>
20
+ # - <tt>Recognition::RouteSet.new</tt>
21
+ def initialize(options = {}, &block)
22
+ @parameters_key = options.delete(:parameters_key) || 'rack.routing_args'
23
+ @parameters_key.freeze
24
+
25
+ @route_key = options.delete(:route_key) || "rack.route"
26
+
27
+ @named_routes = {}
28
+
29
+ @recognition_key_analyzer = Analysis::Splitting.new
30
+ @generation_key_analyzer = Analysis::Frequency.new
31
+
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
+
38
+ @routes = []
39
+ expire!
40
+
41
+ if block_given?
42
+ yield self
43
+ rehash
44
+ end
45
+ end
46
+
47
+ # Builder method to add a route to the set
48
+ #
49
+ # <tt>app</tt>:: A valid Rack app to call if the conditions are met.
50
+ # <tt>conditions</tt>:: A hash of conditions to match against.
51
+ # Conditions may be expressed as strings or
52
+ # regexps to match against.
53
+ # <tt>defaults</tt>:: A hash of values that always gets merged in
54
+ # <tt>name</tt>:: Symbol identifier for the route used with named
55
+ # route generations
56
+ def add_route(app, conditions = {}, defaults = {}, name = nil)
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)
69
+ @routes << route
70
+
71
+ @recognition_key_analyzer << route.conditions
72
+
73
+ @named_routes[route.name] = route if route.name
74
+ @generation_key_analyzer << route.generation_keys
75
+
76
+ expire!
77
+ route
78
+ end
79
+
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, the catch status code is returned.
131
+ #
132
+ # This method can only be invoked after the RouteSet has been
133
+ # finalized.
134
+ def call(env)
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
+ env[@parameters_key] = params
150
+ env[@route_key] = route.name if route.name
151
+ result = route.app.call(env)
152
+ return result unless result[1][X_CASCADE] == PASS
153
+ end
154
+
155
+ request || [404, {'Content-Type' => 'text/html', 'X-Cascade' => 'pass'}, ['Not Found']]
156
+ end
157
+
158
+ # Generates a url from Rack env and identifiers or significant keys.
159
+ #
160
+ # To generate a url by named route, pass the name in as a +Symbol+.
161
+ # url(env, :dashboard) # => "/dashboard"
162
+ #
163
+ # Additional parameters can be passed in as a hash
164
+ # url(env, :people, :id => "1") # => "/people/1"
165
+ #
166
+ # If no name route is given, it will fall back to a slower
167
+ # generation search.
168
+ # url(env, :controller => "people", :action => "show", :id => "1")
169
+ # # => "/people/1"
170
+ def url(env, *args)
171
+ named_route, params = nil, {}
172
+
173
+ case args.length
174
+ when 2
175
+ named_route, params = args[0], args[1].dup
176
+ when 1
177
+ if args[0].is_a?(Hash)
178
+ params = args[0].dup
179
+ else
180
+ named_route = args[0]
181
+ end
182
+ else
183
+ raise ArgumentError
184
+ end
185
+
186
+ only_path = params.delete(:only_path)
187
+ recall = env[@parameters_key] || {}
188
+
189
+ unless result = generate(:all, named_route, params, recall,
190
+ :parameterize => lambda { |name, param| Utils.escape_uri(param) })
191
+ return
192
+ end
193
+
194
+ parts, params = result
195
+ return unless parts
196
+
197
+ params.each do |k, v|
198
+ if v
199
+ params[k] = v
200
+ else
201
+ params.delete(k)
202
+ end
203
+ end
204
+
205
+ req = stubbed_request_class.new(env)
206
+ req._stubbed_values = parts.merge(:query_string => Utils.build_nested_query(params))
207
+ only_path ? req.fullpath : req.url
208
+ end
209
+
210
+ def generate(method, *args) #:nodoc:
211
+ raise 'route set not finalized' unless @generation_graph
212
+
213
+ method = nil if method == :all
214
+ named_route, params, recall, options = extract_params!(*args)
215
+ merged = recall.merge(params)
216
+ route = nil
217
+
218
+ if named_route
219
+ if route = @named_routes[named_route.to_sym]
220
+ recall = route.defaults.merge(recall)
221
+ url = route.generate(method, params, recall, options)
222
+ [url, params]
223
+ else
224
+ raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
225
+ end
226
+ else
227
+ keys = @generation_keys.map { |key|
228
+ if k = merged[key]
229
+ k.to_s
230
+ else
231
+ nil
232
+ end
233
+ }
234
+ @generation_graph[*keys].each do |r|
235
+ next unless r.significant_params?
236
+ if url = r.generate(method, params, recall, options)
237
+ return [url, params]
238
+ end
239
+ end
240
+
241
+ raise RoutingError, "No route matches #{params.inspect}"
242
+ end
243
+ end
244
+
245
+ # Number of routes in the set
246
+ def length
247
+ @routes.length
248
+ end
249
+
250
+ def rehash #:nodoc:
251
+ Utils.debug "rehashing"
252
+
253
+ @recognition_keys = build_recognition_keys
254
+ @recognition_graph = build_recognition_graph
255
+ @generation_keys = build_generation_keys
256
+ @generation_graph = build_generation_graph
257
+
258
+ self
259
+ end
260
+
261
+ # Finalizes the set and builds optimized data structures. You *must*
262
+ # freeze the set before you can use <tt>call</tt> and <tt>url</tt>.
263
+ # So remember to call freeze after you are done adding routes.
264
+ def freeze
265
+ unless frozen?
266
+ rehash
267
+
268
+ @recognition_key_analyzer = nil
269
+ @generation_key_analyzer = nil
270
+ @valid_conditions = nil
271
+
272
+ @routes.each { |route| route.freeze }
273
+ @routes.freeze
274
+ end
275
+
276
+ super
277
+ end
278
+
279
+ def marshal_dump #:nodoc:
280
+ hash = {}
281
+
282
+ instance_variables_to_serialize.each do |ivar|
283
+ hash[ivar] = instance_variable_get(ivar)
284
+ end
285
+
286
+ if graph = hash[:@recognition_graph]
287
+ hash[:@recognition_graph] = graph.dup
288
+ end
289
+
290
+ hash
291
+ end
292
+
293
+ def marshal_load(hash) #:nodoc:
294
+ hash.each do |ivar, value|
295
+ instance_variable_set(ivar, value)
296
+ end
297
+ end
298
+
299
+ protected
300
+ def recognition_stats
301
+ { :keys => @recognition_keys,
302
+ :keys_size => @recognition_keys.size,
303
+ :graph_size => @recognition_graph.size,
304
+ :graph_height => @recognition_graph.height,
305
+ :graph_average_height => @recognition_graph.average_height }
306
+ end
307
+
308
+ private
309
+ def expire! #:nodoc:
310
+ @recognition_keys = @recognition_graph = nil
311
+ @recognition_key_analyzer.expire!
312
+
313
+ @generation_keys = @generation_graph = nil
314
+ @generation_key_analyzer.expire!
315
+ end
316
+
317
+ def instance_variables_to_serialize
318
+ instance_variables.map { |ivar| ivar.to_sym } - [:@stubbed_request_class, :@optimized_recognize_defined]
319
+ end
320
+
321
+ # An internal helper method for constructing a nested set from
322
+ # the linear route set.
323
+ #
324
+ # build_nested_route_set([:request_method, :path_info]) { |route, method|
325
+ # route.send(method)
326
+ # }
327
+ def build_nested_route_set(keys, &block)
328
+ graph = Multimap.new
329
+ @routes.each_with_index do |route, index|
330
+ catch(:skip) do
331
+ k = keys.map { |key| block.call(key, index) }
332
+ Utils.pop_trailing_blanks!(k)
333
+ k.map! { |key| key || /.*/ }
334
+ graph[*k] = route
335
+ end
336
+ end
337
+ graph
338
+ end
339
+
340
+ def build_recognition_graph
341
+ build_nested_route_set(@recognition_keys) { |k, i|
342
+ @recognition_key_analyzer.possible_keys[i][k]
343
+ }
344
+ end
345
+
346
+ def build_recognition_keys
347
+ keys = @recognition_key_analyzer.report
348
+ Utils.debug "recognition keys - #{keys.inspect}"
349
+ keys
350
+ end
351
+
352
+ def build_generation_graph
353
+ build_nested_route_set(@generation_keys) { |k, i|
354
+ throw :skip unless @routes[i].significant_params?
355
+
356
+ if k = @generation_key_analyzer.possible_keys[i][k]
357
+ k.to_s
358
+ else
359
+ nil
360
+ end
361
+ }
362
+ end
363
+
364
+ def build_generation_keys
365
+ keys = @generation_key_analyzer.report
366
+ Utils.debug "generation keys - #{keys.inspect}"
367
+ keys
368
+ end
369
+
370
+ def extract_params!(*args)
371
+ case args.length
372
+ when 4
373
+ named_route, params, recall, options = args
374
+ when 3
375
+ if args[0].is_a?(Hash)
376
+ params, recall, options = args
377
+ else
378
+ named_route, params, recall = args
379
+ end
380
+ when 2
381
+ if args[0].is_a?(Hash)
382
+ params, recall = args
383
+ else
384
+ named_route, params = args
385
+ end
386
+ when 1
387
+ if args[0].is_a?(Hash)
388
+ params = args[0]
389
+ else
390
+ named_route = args[0]
391
+ end
392
+ else
393
+ raise ArgumentError
394
+ end
395
+
396
+ named_route ||= nil
397
+ params ||= {}
398
+ recall ||= {}
399
+ options ||= {}
400
+
401
+ [named_route, params.dup, recall.dup, options.dup]
402
+ end
403
+
404
+ def stubbed_request_class
405
+ @stubbed_request_class ||= begin
406
+ klass = Class.new(@request_class)
407
+ klass.public_instance_methods.each do |method|
408
+ next if method =~ /^__|object_id/
409
+ klass.class_eval <<-RUBY
410
+ def #{method}(*args, &block)
411
+ @_stubbed_values[:#{method}] || super
412
+ end
413
+ RUBY
414
+ end
415
+ klass.class_eval { attr_accessor :_stubbed_values }
416
+ klass
417
+ end
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,68 @@
1
+ require 'rack/mount/strexp/parser'
2
+
3
+ module Rack::Mount
4
+ class Strexp
5
+ class << self
6
+ # Parses segmented string expression and converts it into a Regexp
7
+ #
8
+ # Strexp.compile('foo')
9
+ # # => %r{\Afoo\Z}
10
+ #
11
+ # Strexp.compile('foo/:bar', {}, ['/'])
12
+ # # => %r{\Afoo/(?<bar>[^/]+)\Z}
13
+ #
14
+ # Strexp.compile(':foo.example.com')
15
+ # # => %r{\A(?<foo>.+)\.example\.com\Z}
16
+ #
17
+ # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/'])
18
+ # # => %r{\Afoo/(?<bar>[a-z]+)\Z}
19
+ #
20
+ # Strexp.compile('foo(.:extension)')
21
+ # # => %r{\Afoo(\.(?<extension>.+))?\Z}
22
+ #
23
+ # Strexp.compile('src/*files')
24
+ # # => %r{\Asrc/(?<files>.+)\Z}
25
+ def compile(str, requirements = {}, separators = [], anchor = true)
26
+ return Regexp.compile(str) if str.is_a?(Regexp)
27
+
28
+ requirements = requirements ? requirements.dup : {}
29
+ normalize_requirements!(requirements, separators)
30
+
31
+ parser = StrexpParser.new
32
+ parser.anchor = anchor
33
+ parser.requirements = requirements
34
+
35
+ begin
36
+ re = parser.scan_str(str)
37
+ rescue Racc::ParseError => e
38
+ raise RegexpError, e.message
39
+ end
40
+
41
+ Regexp.compile(re)
42
+ end
43
+ alias_method :new, :compile
44
+
45
+ private
46
+ def normalize_requirements!(requirements, separators)
47
+ requirements.each do |key, value|
48
+ if value.is_a?(Regexp)
49
+ if regexp_has_modifiers?(value)
50
+ requirements[key] = value
51
+ else
52
+ requirements[key] = value.source
53
+ end
54
+ else
55
+ requirements[key] = Regexp.escape(value)
56
+ end
57
+ end
58
+ requirements.default ||= separators.any? ?
59
+ "[^#{separators.join}]+" : '.+'
60
+ requirements
61
+ end
62
+
63
+ def regexp_has_modifiers?(regexp)
64
+ regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
65
+ end
66
+ end
67
+ end
68
+ end