rack-mount 0.0.1
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 +28 -0
- data/lib/rack/mount/analysis/frequency.rb +51 -0
- data/lib/rack/mount/analysis/histogram.rb +25 -0
- data/lib/rack/mount/analysis/splitting.rb +145 -0
- data/lib/rack/mount/const.rb +45 -0
- data/lib/rack/mount/exceptions.rb +3 -0
- data/lib/rack/mount/generatable_regexp.rb +163 -0
- data/lib/rack/mount/generation/route.rb +57 -0
- data/lib/rack/mount/generation/route_set.rb +163 -0
- data/lib/rack/mount/meta_method.rb +104 -0
- data/lib/rack/mount/mixover.rb +47 -0
- data/lib/rack/mount/multimap.rb +94 -0
- data/lib/rack/mount/prefix.rb +31 -0
- data/lib/rack/mount/recognition/code_generation.rb +99 -0
- data/lib/rack/mount/recognition/route.rb +59 -0
- data/lib/rack/mount/recognition/route_set.rb +88 -0
- data/lib/rack/mount/regexp_with_named_groups.rb +49 -0
- data/lib/rack/mount/route.rb +69 -0
- data/lib/rack/mount/route_set.rb +109 -0
- data/lib/rack/mount/strexp.rb +93 -0
- data/lib/rack/mount/utils.rb +271 -0
- data/lib/rack/mount/vendor/multimap/multimap.rb +466 -0
- data/lib/rack/mount/vendor/multimap/multiset.rb +153 -0
- data/lib/rack/mount/vendor/multimap/nested_multimap.rb +156 -0
- data/lib/rack/mount.rb +35 -0
- metadata +100 -0
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Rack::Mount
|
5
|
+
module Generation
|
6
|
+
module RouteSet
|
7
|
+
# Adds generation related concerns to RouteSet.new.
|
8
|
+
def initialize(*args)
|
9
|
+
@named_routes = {}
|
10
|
+
@generation_key_analyzer = Analysis::Frequency.new
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds generation aspects to RouteSet#add_route.
|
16
|
+
def add_route(*args)
|
17
|
+
route = super
|
18
|
+
@named_routes[route.name] = route if route.name
|
19
|
+
@generation_key_analyzer << route.generation_keys
|
20
|
+
route
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generates path from identifiers or significant keys.
|
24
|
+
#
|
25
|
+
# To generate a url by named route, pass the name in as a +Symbol+.
|
26
|
+
# url(:dashboard) # => "/dashboard"
|
27
|
+
#
|
28
|
+
# Additional parameters can be passed in as a hash
|
29
|
+
# url(:people, :id => "1") # => "/people/1"
|
30
|
+
#
|
31
|
+
# If no name route is given, it will fall back to a slower
|
32
|
+
# generation search.
|
33
|
+
# url(:controller => "people", :action => "show", :id => "1")
|
34
|
+
# # => "/people/1"
|
35
|
+
def url(*args)
|
36
|
+
named_route, params, recall = extract_params!(*args)
|
37
|
+
|
38
|
+
params = URISegment.wrap_values(params)
|
39
|
+
recall = URISegment.wrap_values(recall)
|
40
|
+
|
41
|
+
unless result = generate(:path_info, named_route, params, recall)
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
uri, params = result
|
46
|
+
params.each do |k, v|
|
47
|
+
if v._value
|
48
|
+
params[k] = v._value
|
49
|
+
else
|
50
|
+
params.delete(k)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
uri << "?#{Utils.build_nested_query(params)}" if uri && params.any?
|
55
|
+
uri
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate(method, *args) #:nodoc:
|
59
|
+
raise 'route set not finalized' unless @generation_graph
|
60
|
+
|
61
|
+
named_route, params, recall = extract_params!(*args)
|
62
|
+
merged = recall.merge(params)
|
63
|
+
route = nil
|
64
|
+
|
65
|
+
if named_route
|
66
|
+
if route = @named_routes[named_route.to_sym]
|
67
|
+
recall = route.defaults.merge(recall)
|
68
|
+
url = route.generate(method, params, recall)
|
69
|
+
[url, params]
|
70
|
+
else
|
71
|
+
raise RoutingError, "#{named_route} failed to generate from #{params.inspect}"
|
72
|
+
end
|
73
|
+
else
|
74
|
+
keys = @generation_keys.map { |key|
|
75
|
+
if k = merged[key]
|
76
|
+
k.to_s
|
77
|
+
else
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
}
|
81
|
+
@generation_graph[*keys].each do |r|
|
82
|
+
if url = r.generate(method, params, recall)
|
83
|
+
return [url, params]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
raise RoutingError, "No route matches #{params.inspect}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def rehash #:nodoc:
|
92
|
+
@generation_keys = build_generation_keys
|
93
|
+
@generation_graph = build_generation_graph
|
94
|
+
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def expire!
|
100
|
+
@generation_keys = @generation_graph = nil
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_generation_graph
|
105
|
+
build_nested_route_set(@generation_keys) { |k, i|
|
106
|
+
if k = @generation_key_analyzer.possible_keys[i][k]
|
107
|
+
k.to_s
|
108
|
+
else
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
def build_generation_keys
|
115
|
+
@generation_key_analyzer.report
|
116
|
+
end
|
117
|
+
|
118
|
+
def extract_params!(*args)
|
119
|
+
case args.length
|
120
|
+
when 3
|
121
|
+
named_route, params, recall = args
|
122
|
+
when 2
|
123
|
+
if args[0].is_a?(Hash) && args[1].is_a?(Hash)
|
124
|
+
params, recall = args
|
125
|
+
else
|
126
|
+
named_route, params = args
|
127
|
+
end
|
128
|
+
when 1
|
129
|
+
if args[0].is_a?(Hash)
|
130
|
+
params = args[0]
|
131
|
+
else
|
132
|
+
named_route = args[0]
|
133
|
+
end
|
134
|
+
else
|
135
|
+
raise ArgumentError
|
136
|
+
end
|
137
|
+
|
138
|
+
named_route ||= nil
|
139
|
+
params ||= {}
|
140
|
+
recall ||= {}
|
141
|
+
|
142
|
+
[named_route, params.dup, recall.dup]
|
143
|
+
end
|
144
|
+
|
145
|
+
class URISegment < Struct.new(:_value)
|
146
|
+
def self.wrap_values(hash)
|
147
|
+
hash.inject({}) { |h, (k, v)| h[k] = new(v); h }
|
148
|
+
end
|
149
|
+
|
150
|
+
extend Forwardable
|
151
|
+
def_delegators :_value, :==, :eql?, :hash
|
152
|
+
|
153
|
+
def to_param
|
154
|
+
@to_param ||= begin
|
155
|
+
v = _value.respond_to?(:to_param) ? _value.to_param : _value
|
156
|
+
Utils.escape_uri(v)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
alias_method :to_s, :to_param
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Rack::Mount
|
2
|
+
class MetaMethod #:nodoc:
|
3
|
+
class Block < Array #:nodoc:
|
4
|
+
def initialize(*parts)
|
5
|
+
replace(parts)
|
6
|
+
yield(self) if block_given?
|
7
|
+
end
|
8
|
+
|
9
|
+
def multiline?
|
10
|
+
length > 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def inspect(indented = 2)
|
14
|
+
return Const::EMPTY_STRING if empty?
|
15
|
+
space = ' ' * indented
|
16
|
+
space + map { |p|
|
17
|
+
if p.is_a?(Condition)
|
18
|
+
p.inspect(indented)
|
19
|
+
else
|
20
|
+
p
|
21
|
+
end
|
22
|
+
}.join("\n#{space}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_str
|
26
|
+
map { |p| p.to_str }.join('; ')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Condition #:nodoc:
|
31
|
+
attr_accessor :body, :else
|
32
|
+
|
33
|
+
def initialize(*conditions)
|
34
|
+
@conditions = conditions.map { |c| c.is_a?(Block) ? c : Block.new(c) }
|
35
|
+
@body = Block.new
|
36
|
+
@else = Block.new
|
37
|
+
yield(@body) if block_given?
|
38
|
+
end
|
39
|
+
|
40
|
+
def <<(condition)
|
41
|
+
@conditions << condition
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect(indented = 2)
|
45
|
+
return @body.inspect(indented) if @conditions.empty?
|
46
|
+
space = ' ' * indented
|
47
|
+
str = 'if '
|
48
|
+
str << @conditions.map { |b|
|
49
|
+
b.multiline? ?
|
50
|
+
"begin\n#{b.inspect(indented + 4)}\n#{space} end" :
|
51
|
+
b.inspect(0)
|
52
|
+
}.join(' && ')
|
53
|
+
str << "\n#{@body.inspect(indented + 2)}" if @body.any?
|
54
|
+
if @else.any?
|
55
|
+
str << "\n#{space}else\n#{@else.inspect(indented + 2)}"
|
56
|
+
end
|
57
|
+
str << "\n#{space}end"
|
58
|
+
str
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_str
|
62
|
+
return @body.to_str if @conditions.empty?
|
63
|
+
str = 'if '
|
64
|
+
str << @conditions.map { |b|
|
65
|
+
b.multiline? ? "(#{b.to_str})" : b.to_str
|
66
|
+
}.join(' && ')
|
67
|
+
str << "; #{@body.to_str}" if @body.any?
|
68
|
+
if @else.any?
|
69
|
+
str << "; else; #{@else.to_str}"
|
70
|
+
end
|
71
|
+
str << "; end"
|
72
|
+
str
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(sym, *args)
|
77
|
+
@sym = sym
|
78
|
+
@args = args
|
79
|
+
@body = Block.new
|
80
|
+
end
|
81
|
+
|
82
|
+
def <<(line)
|
83
|
+
@body << line
|
84
|
+
end
|
85
|
+
|
86
|
+
def inspect
|
87
|
+
str = ""
|
88
|
+
str << "def #{@sym}"
|
89
|
+
str << "(#{@args.join(', ')})" if @args.any?
|
90
|
+
str << "\n#{@body.inspect}" if @body.any?
|
91
|
+
str << "\nend\n"
|
92
|
+
str
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_str
|
96
|
+
str = []
|
97
|
+
str << "def #{@sym}"
|
98
|
+
str << "(#{@args.join(', ')})" if @args.any?
|
99
|
+
str << "\n#{@body.to_str}" if @body.any?
|
100
|
+
str << "\nend"
|
101
|
+
str.join
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Rack::Mount
|
2
|
+
# A mixin that changes the behavior of +include+. Instead of modules
|
3
|
+
# being chained as a superclass, they are mixed into the objects
|
4
|
+
# metaclass. This allows mixins to be stacked ontop of the instance
|
5
|
+
# methods.
|
6
|
+
module Mixover
|
7
|
+
module InstanceMethods #:nodoc:
|
8
|
+
def dup
|
9
|
+
obj = super
|
10
|
+
included_modules = (class << self; included_modules; end) - (class << obj; included_modules; end)
|
11
|
+
included_modules.reverse.each { |mod| obj.extend(mod) }
|
12
|
+
obj
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Replaces include with a lazy version.
|
17
|
+
def include(*mod)
|
18
|
+
(@included_modules ||= []).push(*mod)
|
19
|
+
end
|
20
|
+
|
21
|
+
def new(*args, &block) #:nodoc:
|
22
|
+
obj = allocate
|
23
|
+
obj.extend(InstanceMethods)
|
24
|
+
(@included_modules ||= []).each { |mod| obj.extend(mod) }
|
25
|
+
obj.send(:initialize, *args, &block)
|
26
|
+
obj
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a new class without an included module.
|
30
|
+
def new_without_module(mod, *args, &block)
|
31
|
+
old_included_modules = (@included_modules ||= []).dup
|
32
|
+
@included_modules.delete(mod)
|
33
|
+
new(*args, &block)
|
34
|
+
ensure
|
35
|
+
@included_modules = old_included_modules
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new class temporarily with a module.
|
39
|
+
def new_with_module(mod, *args, &block)
|
40
|
+
old_included_modules = (@included_modules ||= []).dup
|
41
|
+
include(mod)
|
42
|
+
new(*args, &block)
|
43
|
+
ensure
|
44
|
+
@included_modules = old_included_modules
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
begin
|
2
|
+
require 'nested_multimap'
|
3
|
+
rescue LoadError
|
4
|
+
$: << File.expand_path(File.join(File.dirname(__FILE__), 'vendor/multimap'))
|
5
|
+
require 'nested_multimap'
|
6
|
+
end
|
7
|
+
|
8
|
+
module Rack::Mount
|
9
|
+
class Multimap < NestedMultimap #:nodoc:
|
10
|
+
def self.[](*args)
|
11
|
+
map = super
|
12
|
+
map.instance_variable_set('@fuzz', {})
|
13
|
+
map
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(default = [])
|
17
|
+
@fuzz = {}
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize_copy(original)
|
22
|
+
@fuzz = original.instance_variable_get('@fuzz').dup
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def store(*args)
|
27
|
+
keys = args.dup
|
28
|
+
value = keys.pop
|
29
|
+
key = keys.shift
|
30
|
+
|
31
|
+
raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value
|
32
|
+
|
33
|
+
unless key.respond_to?(:=~)
|
34
|
+
raise ArgumentError, "unsupported key: #{args.first.inspect}"
|
35
|
+
end
|
36
|
+
|
37
|
+
if key.is_a?(Regexp)
|
38
|
+
@fuzz[value] = key
|
39
|
+
if keys.empty?
|
40
|
+
hash_each_pair { |k, l| l << value if k =~ key }
|
41
|
+
self.default << value
|
42
|
+
else
|
43
|
+
hash_each_pair { |k, _|
|
44
|
+
if k =~ key
|
45
|
+
args[0] = k
|
46
|
+
super(*args)
|
47
|
+
end
|
48
|
+
}
|
49
|
+
|
50
|
+
self.default = self.class.new(default) unless default.is_a?(self.class)
|
51
|
+
default[*keys.dup] = value
|
52
|
+
end
|
53
|
+
else
|
54
|
+
super(*args)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
alias_method :[]=, :store
|
58
|
+
|
59
|
+
def freeze
|
60
|
+
@fuzz.clear
|
61
|
+
@fuzz = nil
|
62
|
+
super
|
63
|
+
end
|
64
|
+
|
65
|
+
undef :index, :invert
|
66
|
+
|
67
|
+
def height
|
68
|
+
containers_with_default.max { |a, b| a.length <=> b.length }.length
|
69
|
+
end
|
70
|
+
|
71
|
+
def average_height
|
72
|
+
lengths = containers_with_default.map { |e| e.length }
|
73
|
+
lengths.inject(0) { |sum, len| sum += len }.to_f / lengths.size
|
74
|
+
end
|
75
|
+
|
76
|
+
protected
|
77
|
+
def update_container(key) #:nodoc:
|
78
|
+
super do |container|
|
79
|
+
if container.is_a?(self.class)
|
80
|
+
container.each_container_with_default do |c|
|
81
|
+
c.delete_if do |value|
|
82
|
+
(requirement = @fuzz[value]) && key !~ requirement
|
83
|
+
end
|
84
|
+
end
|
85
|
+
else
|
86
|
+
container.delete_if do |value|
|
87
|
+
(requirement = @fuzz[value]) && key !~ requirement
|
88
|
+
end
|
89
|
+
end
|
90
|
+
yield container
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
class Prefix #:nodoc:
|
5
|
+
KEY = 'rack.mount.prefix'.freeze
|
6
|
+
|
7
|
+
def initialize(app, prefix = nil)
|
8
|
+
@app, @prefix = app, prefix.freeze
|
9
|
+
freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
if prefix = env[KEY] || @prefix
|
14
|
+
old_path_info = env[Const::PATH_INFO].dup
|
15
|
+
old_script_name = env[Const::SCRIPT_NAME].dup
|
16
|
+
|
17
|
+
begin
|
18
|
+
env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO].sub(prefix, Const::EMPTY_STRING))
|
19
|
+
env[Const::PATH_INFO] = Const::EMPTY_STRING if env[Const::PATH_INFO] == Const::SLASH
|
20
|
+
env[Const::SCRIPT_NAME] = Utils.normalize_path(env[Const::SCRIPT_NAME].to_s + prefix)
|
21
|
+
@app.call(env)
|
22
|
+
ensure
|
23
|
+
env[Const::PATH_INFO] = old_path_info
|
24
|
+
env[Const::SCRIPT_NAME] = old_script_name
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@app.call(env)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'rack/mount/meta_method'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
module Recognition
|
5
|
+
module CodeGeneration #:nodoc:
|
6
|
+
def _expired_call(env) #:nodoc:
|
7
|
+
raise 'route set not finalized'
|
8
|
+
end
|
9
|
+
|
10
|
+
def rehash
|
11
|
+
super
|
12
|
+
optimize_call!
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def expire!
|
17
|
+
class << self
|
18
|
+
alias_method :call, :_expired_call
|
19
|
+
end
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def optimize_container_iterator(container)
|
25
|
+
m = MetaMethod.new(:optimized_each, :req)
|
26
|
+
m << 'env = req.env'
|
27
|
+
|
28
|
+
container.each_with_index { |route, i|
|
29
|
+
path_info_unanchored = route.conditions[:path_info] &&
|
30
|
+
!Utils.regexp_anchored?(route.conditions[:path_info])
|
31
|
+
m << "route = self[#{i}]"
|
32
|
+
m << 'routing_args = route.defaults.dup'
|
33
|
+
|
34
|
+
m << matchers = MetaMethod::Condition.new do |body|
|
35
|
+
body << "env[#{@parameters_key.inspect}] = routing_args"
|
36
|
+
body << "response = route.app.call(env)"
|
37
|
+
body << "return response unless response[0].to_i == 417"
|
38
|
+
end
|
39
|
+
|
40
|
+
route.conditions.each do |method, condition|
|
41
|
+
matchers << MetaMethod::Block.new do |matcher|
|
42
|
+
matcher << c = MetaMethod::Condition.new("m = req.#{method}.match(#{condition.inspect})") do |b|
|
43
|
+
b << "matches = m.captures" if route.named_captures[method].any?
|
44
|
+
route.named_captures[method].each do |k, i|
|
45
|
+
b << MetaMethod::Condition.new("p = matches[#{i}]") do |c2|
|
46
|
+
c2 << "routing_args[#{k.inspect}] = Utils.unescape_uri(p)"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
if method == :path_info && !Utils.regexp_anchored?(condition)
|
50
|
+
b << "env[Prefix::KEY] = m.to_s"
|
51
|
+
end
|
52
|
+
b << "true"
|
53
|
+
end
|
54
|
+
c.else = MetaMethod::Block.new("false")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
}
|
58
|
+
|
59
|
+
m << 'nil'
|
60
|
+
# puts "\n#{m.inspect}"
|
61
|
+
container.instance_eval(m, __FILE__, __LINE__)
|
62
|
+
end
|
63
|
+
|
64
|
+
def optimize_call!
|
65
|
+
method = MetaMethod.new(:call, :env)
|
66
|
+
|
67
|
+
if @routes.empty?
|
68
|
+
method << 'env[Const::EXPECT] != Const::CONTINUE ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE'
|
69
|
+
else
|
70
|
+
method << 'begin'
|
71
|
+
method << 'set_expectation = env[Const::EXPECT] != Const::CONTINUE'
|
72
|
+
method << 'env[Const::EXPECT] = Const::CONTINUE if set_expectation'
|
73
|
+
|
74
|
+
method << 'env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO])'
|
75
|
+
method << "req = #{@request_class.name}.new(env)"
|
76
|
+
cache = false
|
77
|
+
keys = @recognition_keys.map { |key|
|
78
|
+
if key.is_a?(Array)
|
79
|
+
cache = true
|
80
|
+
key.call_source(:cache, :req)
|
81
|
+
else
|
82
|
+
"req.#{key}"
|
83
|
+
end
|
84
|
+
}.join(', ')
|
85
|
+
method << 'cache = {}' if cache
|
86
|
+
method << "container = @recognition_graph[#{keys}]"
|
87
|
+
method << "optimize_container_iterator(container) unless container.respond_to?(:optimized_each)"
|
88
|
+
method << "container.optimized_each(req) || (set_expectation ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE)"
|
89
|
+
method << 'ensure'
|
90
|
+
method << 'env.delete(Const::EXPECT) if set_expectation'
|
91
|
+
method << 'end'
|
92
|
+
end
|
93
|
+
|
94
|
+
# puts "\n#{method.inspect}"
|
95
|
+
instance_eval(method, __FILE__, __LINE__)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rack/mount/prefix'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
module Recognition
|
5
|
+
module Route #:nodoc:
|
6
|
+
attr_reader :named_captures
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
|
11
|
+
# TODO: Don't explict check for :path_info condition
|
12
|
+
if @conditions.has_key?(:path_info) &&
|
13
|
+
!Utils.regexp_anchored?(@conditions[:path_info])
|
14
|
+
@app = Prefix.new(@app)
|
15
|
+
end
|
16
|
+
|
17
|
+
@named_captures = {}
|
18
|
+
@conditions.map { |method, condition|
|
19
|
+
@named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)|
|
20
|
+
named_captures[k.to_sym] = v.last - 1
|
21
|
+
named_captures
|
22
|
+
}.freeze
|
23
|
+
}
|
24
|
+
@named_captures.freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(req)
|
28
|
+
env = req.env
|
29
|
+
|
30
|
+
routing_args = @defaults.dup
|
31
|
+
if @conditions.all? { |method, condition|
|
32
|
+
value = req.send(method)
|
33
|
+
if m = value.match(condition)
|
34
|
+
matches = m.captures
|
35
|
+
@named_captures[method].each { |k, i|
|
36
|
+
if v = matches[i]
|
37
|
+
# TODO: We only want to unescape params from
|
38
|
+
# uri related methods
|
39
|
+
routing_args[k] = Utils.unescape_uri(v)
|
40
|
+
end
|
41
|
+
}
|
42
|
+
# TODO: Don't explict check for :path_info condition
|
43
|
+
if method == :path_info && !Utils.regexp_anchored?(condition)
|
44
|
+
env[Prefix::KEY] = m.to_s
|
45
|
+
end
|
46
|
+
true
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
}
|
51
|
+
env[@set.parameters_key] = routing_args
|
52
|
+
@app.call(env)
|
53
|
+
else
|
54
|
+
Const::EXPECTATION_FAILED_RESPONSE
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
module Recognition
|
5
|
+
module RouteSet
|
6
|
+
attr_reader :parameters_key
|
7
|
+
|
8
|
+
# Adds recognition related concerns to RouteSet.new.
|
9
|
+
def initialize(options = {})
|
10
|
+
@parameters_key = options.delete(:parameters_key) || Const::RACK_ROUTING_ARGS
|
11
|
+
@parameters_key.freeze
|
12
|
+
@recognition_key_analyzer = Analysis::Frequency.new_with_module(Analysis::Splitting)
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds recognition aspects to RouteSet#add_route.
|
18
|
+
def add_route(*args)
|
19
|
+
route = super
|
20
|
+
@recognition_key_analyzer << route.conditions
|
21
|
+
route
|
22
|
+
end
|
23
|
+
|
24
|
+
# Rack compatible recognition and dispatching method. Routes are
|
25
|
+
# tried until one returns a non-catch status code. If no routes
|
26
|
+
# match, the catch status code is returned.
|
27
|
+
#
|
28
|
+
# This method can only be invoked after the RouteSet has been
|
29
|
+
# finalized.
|
30
|
+
def call(env)
|
31
|
+
raise 'route set not finalized' unless @recognition_graph
|
32
|
+
|
33
|
+
set_expectation = env[Const::EXPECT] != Const::CONTINUE
|
34
|
+
env[Const::EXPECT] = Const::CONTINUE if set_expectation
|
35
|
+
|
36
|
+
env[Const::PATH_INFO] = Utils.normalize_path(env[Const::PATH_INFO])
|
37
|
+
|
38
|
+
cache = {}
|
39
|
+
req = @request_class.new(env)
|
40
|
+
keys = @recognition_keys.map { |key|
|
41
|
+
if key.is_a?(Array)
|
42
|
+
key.call(cache, req)
|
43
|
+
else
|
44
|
+
req.send(key)
|
45
|
+
end
|
46
|
+
}
|
47
|
+
@recognition_graph[*keys].each do |route|
|
48
|
+
result = route.call(req)
|
49
|
+
return result unless result[0].to_i == 417
|
50
|
+
end
|
51
|
+
set_expectation ? Const::NOT_FOUND_RESPONSE : Const::EXPECTATION_FAILED_RESPONSE
|
52
|
+
ensure
|
53
|
+
env.delete(Const::EXPECT) if set_expectation
|
54
|
+
end
|
55
|
+
|
56
|
+
def rehash #:nodoc:
|
57
|
+
@recognition_keys = build_recognition_keys
|
58
|
+
@recognition_graph = build_recognition_graph
|
59
|
+
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
def valid_conditions #:nodoc:
|
64
|
+
@valid_conditions ||= begin
|
65
|
+
conditions = @request_class.instance_methods(false)
|
66
|
+
conditions.map! { |m| m.to_sym }
|
67
|
+
conditions.freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def expire!
|
73
|
+
@recognition_keys = @recognition_graph = nil
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def build_recognition_graph
|
78
|
+
build_nested_route_set(@recognition_keys) { |k, i|
|
79
|
+
@recognition_key_analyzer.possible_keys[i][k]
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_recognition_keys
|
84
|
+
@recognition_key_analyzer.report
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|