lgierth-rack-mount 0.6.13

Sign up to get free protection for your applications and to get access to all the features.
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