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.
- data/LICENSE +20 -0
- data/README.rdoc +36 -0
- data/lib/rack/mount.rb +32 -0
- data/lib/rack/mount/analysis/frequency.rb +60 -0
- data/lib/rack/mount/analysis/histogram.rb +74 -0
- data/lib/rack/mount/analysis/splitting.rb +159 -0
- data/lib/rack/mount/code_generation.rb +117 -0
- data/lib/rack/mount/generatable_regexp.rb +210 -0
- data/lib/rack/mount/multimap.rb +53 -0
- data/lib/rack/mount/prefix.rb +36 -0
- data/lib/rack/mount/regexp_with_named_groups.rb +69 -0
- data/lib/rack/mount/route.rb +130 -0
- data/lib/rack/mount/route_set.rb +420 -0
- data/lib/rack/mount/strexp.rb +68 -0
- data/lib/rack/mount/strexp/parser.rb +160 -0
- data/lib/rack/mount/strexp/parser.y +34 -0
- data/lib/rack/mount/strexp/tokenizer.rb +83 -0
- data/lib/rack/mount/strexp/tokenizer.rex +12 -0
- data/lib/rack/mount/utils.rb +162 -0
- data/lib/rack/mount/vendor/multimap/multimap.rb +569 -0
- data/lib/rack/mount/vendor/multimap/multiset.rb +185 -0
- data/lib/rack/mount/vendor/multimap/nested_multimap.rb +158 -0
- data/lib/rack/mount/vendor/regin/regin.rb +75 -0
- data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
- data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
- data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
- data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
- data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
- data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
- data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
- data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
- data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
- data/lib/rack/mount/vendor/regin/regin/parser.rb +520 -0
- data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +246 -0
- data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
- data/lib/rack/mount/version.rb +3 -0
- 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
|