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,210 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
class GeneratableRegexp < Regexp #:nodoc:
|
5
|
+
class DynamicSegment #:nodoc:
|
6
|
+
attr_reader :name, :requirement
|
7
|
+
|
8
|
+
def initialize(name, requirement)
|
9
|
+
@name, @requirement = name.to_sym, requirement
|
10
|
+
freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(obj)
|
14
|
+
@name == obj.name && @requirement == obj.requirement
|
15
|
+
end
|
16
|
+
|
17
|
+
def =~(str)
|
18
|
+
@requirement =~ str
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
{ @name => @requirement }
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"/(?<#{@name}>#{@requirement.source})/"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
def self.extended(obj)
|
32
|
+
obj.segments
|
33
|
+
end
|
34
|
+
|
35
|
+
def defaults=(defaults)
|
36
|
+
@required_captures = nil
|
37
|
+
@required_params = nil
|
38
|
+
@required_defaults = nil
|
39
|
+
@defaults = defaults
|
40
|
+
end
|
41
|
+
|
42
|
+
def defaults
|
43
|
+
@defaults ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def generatable?
|
47
|
+
segments.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate(params = {}, recall = {}, options = {})
|
51
|
+
return nil unless generatable?
|
52
|
+
|
53
|
+
merged = recall.merge(params)
|
54
|
+
return nil unless required_params.all? { |p| merged.include?(p) }
|
55
|
+
return nil unless required_defaults.all? { |k, v| merged[k] == v }
|
56
|
+
|
57
|
+
generate_from_segments(segments, params, merged, options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def segments
|
61
|
+
@segments ||= begin
|
62
|
+
defaults
|
63
|
+
segments = []
|
64
|
+
catch(:halt) do
|
65
|
+
expression = Utils.parse_regexp(self)
|
66
|
+
segments = parse_segments(expression)
|
67
|
+
end
|
68
|
+
segments
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def captures
|
73
|
+
segments.flatten.find_all { |s| s.is_a?(DynamicSegment) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def required_captures
|
77
|
+
@required_captures ||= segments.find_all { |s|
|
78
|
+
s.is_a?(DynamicSegment) && !@defaults.include?(s.name)
|
79
|
+
}.freeze
|
80
|
+
end
|
81
|
+
|
82
|
+
def required_params
|
83
|
+
@required_params ||= required_captures.map { |s| s.name }.freeze
|
84
|
+
end
|
85
|
+
|
86
|
+
def required_defaults
|
87
|
+
@required_defaults ||= begin
|
88
|
+
required_defaults = @defaults.dup
|
89
|
+
captures.inject({}) { |h, s| h.merge!(s.to_hash) }.keys.each { |name|
|
90
|
+
required_defaults.delete(name)
|
91
|
+
}
|
92
|
+
required_defaults
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def freeze
|
97
|
+
segments
|
98
|
+
captures
|
99
|
+
required_captures
|
100
|
+
required_params
|
101
|
+
required_defaults
|
102
|
+
super
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
def parse_segments(segments)
|
107
|
+
s = []
|
108
|
+
segments.each_with_index do |part, index|
|
109
|
+
case part
|
110
|
+
when Regin::Anchor
|
111
|
+
# ignore
|
112
|
+
when Regin::Character
|
113
|
+
throw :halt unless part.literal?
|
114
|
+
|
115
|
+
if s.last.is_a?(String)
|
116
|
+
s.last << part.value.dup
|
117
|
+
else
|
118
|
+
s << part.value.dup
|
119
|
+
end
|
120
|
+
when Regin::Group
|
121
|
+
if part.name
|
122
|
+
s << DynamicSegment.new(part.name, part.expression.to_regexp(true))
|
123
|
+
else
|
124
|
+
s << parse_segments(part.expression)
|
125
|
+
end
|
126
|
+
when Regin::Expression
|
127
|
+
return parse_segments(part)
|
128
|
+
else
|
129
|
+
throw :halt
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
s
|
134
|
+
end
|
135
|
+
|
136
|
+
EMPTY_STRING = ''.freeze
|
137
|
+
|
138
|
+
def generate_from_segments(segments, params, merged, options, optional = false)
|
139
|
+
if optional
|
140
|
+
return EMPTY_STRING if segments.all? { |s| s.is_a?(String) }
|
141
|
+
return EMPTY_STRING unless segments.flatten.any? { |s|
|
142
|
+
params.has_key?(s.name) if s.is_a?(DynamicSegment)
|
143
|
+
}
|
144
|
+
return EMPTY_STRING if segments.any? { |segment|
|
145
|
+
if segment.is_a?(DynamicSegment)
|
146
|
+
value = merged[segment.name] || @defaults[segment.name]
|
147
|
+
value = parameterize(segment.name, value, options)
|
148
|
+
|
149
|
+
merged_value = parameterize(segment.name, merged[segment.name], options)
|
150
|
+
default_value = parameterize(segment.name, @defaults[segment.name], options)
|
151
|
+
|
152
|
+
if value.nil? || segment !~ value
|
153
|
+
true
|
154
|
+
elsif merged_value == default_value
|
155
|
+
# Nasty control flow
|
156
|
+
return :clear_remaining_segments
|
157
|
+
else
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
generated = segments.map do |segment|
|
165
|
+
case segment
|
166
|
+
when String
|
167
|
+
segment
|
168
|
+
when DynamicSegment
|
169
|
+
value = params[segment.name] || merged[segment.name] || @defaults[segment.name]
|
170
|
+
value = parameterize(segment.name, value, options)
|
171
|
+
if value && segment =~ value.to_s
|
172
|
+
value
|
173
|
+
else
|
174
|
+
return
|
175
|
+
end
|
176
|
+
when Array
|
177
|
+
value = generate_from_segments(segment, params, merged, options, true)
|
178
|
+
if value == :clear_remaining_segments
|
179
|
+
segment.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) }
|
180
|
+
EMPTY_STRING
|
181
|
+
elsif value.nil?
|
182
|
+
EMPTY_STRING
|
183
|
+
else
|
184
|
+
value
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Delete any used items from the params
|
190
|
+
segments.each { |s| params.delete(s.name) if s.is_a?(DynamicSegment) }
|
191
|
+
|
192
|
+
generated.join
|
193
|
+
end
|
194
|
+
|
195
|
+
def parameterize(name, value, options)
|
196
|
+
if block = options[:parameterize]
|
197
|
+
block.call(name, value)
|
198
|
+
else
|
199
|
+
value
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
include InstanceMethods
|
204
|
+
|
205
|
+
def initialize(regexp)
|
206
|
+
super
|
207
|
+
segments
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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 store(*args)
|
11
|
+
keys = args.dup
|
12
|
+
value = keys.pop
|
13
|
+
key = keys.shift
|
14
|
+
|
15
|
+
raise ArgumentError, 'wrong number of arguments (1 for 2)' unless value
|
16
|
+
|
17
|
+
unless key.respond_to?(:=~)
|
18
|
+
raise ArgumentError, "unsupported key: #{args.first.inspect}"
|
19
|
+
end
|
20
|
+
|
21
|
+
if key.is_a?(Regexp)
|
22
|
+
if keys.empty?
|
23
|
+
@hash.each_pair { |k, l| l << value if k =~ key }
|
24
|
+
self.default << value
|
25
|
+
else
|
26
|
+
@hash.each_pair { |k, _|
|
27
|
+
if k =~ key
|
28
|
+
args[0] = k
|
29
|
+
super(*args)
|
30
|
+
end
|
31
|
+
}
|
32
|
+
|
33
|
+
self.default = self.class.new(default) unless default.is_a?(self.class)
|
34
|
+
default[*keys.dup] = value
|
35
|
+
end
|
36
|
+
else
|
37
|
+
super(*args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias_method :[]=, :store
|
41
|
+
|
42
|
+
undef :index, :invert
|
43
|
+
|
44
|
+
def height
|
45
|
+
containers_with_default.max { |a, b| a.length <=> b.length }.length
|
46
|
+
end
|
47
|
+
|
48
|
+
def average_height
|
49
|
+
lengths = containers_with_default.map { |e| e.length }
|
50
|
+
lengths.inject(0) { |sum, len| sum += len }.to_f / lengths.size
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
class Prefix #:nodoc:
|
5
|
+
EMPTY_STRING = ''.freeze
|
6
|
+
PATH_INFO = 'PATH_INFO'.freeze
|
7
|
+
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
|
8
|
+
SLASH = '/'.freeze
|
9
|
+
|
10
|
+
KEY = 'rack.mount.prefix'.freeze
|
11
|
+
|
12
|
+
def initialize(app, prefix = nil)
|
13
|
+
@app, @prefix = app, prefix.freeze
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
if prefix = env[KEY] || @prefix
|
19
|
+
old_path_info = env[PATH_INFO].dup
|
20
|
+
old_script_name = env[SCRIPT_NAME].dup
|
21
|
+
|
22
|
+
begin
|
23
|
+
env[PATH_INFO] = env[PATH_INFO].sub(prefix, EMPTY_STRING)
|
24
|
+
env[PATH_INFO] = EMPTY_STRING if env[PATH_INFO] == SLASH
|
25
|
+
env[SCRIPT_NAME] = Utils.normalize_path(env[SCRIPT_NAME].to_s + prefix)
|
26
|
+
@app.call(env)
|
27
|
+
ensure
|
28
|
+
env[PATH_INFO] = old_path_info
|
29
|
+
env[SCRIPT_NAME] = old_script_name
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@app.call(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Rack::Mount
|
2
|
+
if Regin.regexp_supports_named_captures?
|
3
|
+
RegexpWithNamedGroups = Regexp
|
4
|
+
else
|
5
|
+
require 'strscan'
|
6
|
+
|
7
|
+
# A wrapper that adds shim named capture support to older
|
8
|
+
# versions of Ruby.
|
9
|
+
#
|
10
|
+
# Because the named capture syntax causes a parse error, an
|
11
|
+
# alternate syntax is used to indicate named captures.
|
12
|
+
#
|
13
|
+
# Ruby 1.9+ named capture syntax:
|
14
|
+
#
|
15
|
+
# /(?<foo>[a-z]+)/
|
16
|
+
#
|
17
|
+
# Ruby 1.8 shim syntax:
|
18
|
+
#
|
19
|
+
# /(?:<foo>[a-z]+)/
|
20
|
+
class RegexpWithNamedGroups < Regexp
|
21
|
+
def self.new(regexp) #:nodoc:
|
22
|
+
if regexp.is_a?(RegexpWithNamedGroups)
|
23
|
+
regexp
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Wraps Regexp with named capture support.
|
30
|
+
def initialize(regexp)
|
31
|
+
regexp = Regexp.compile(regexp) unless regexp.is_a?(Regexp)
|
32
|
+
source, options = regexp.source, regexp.options
|
33
|
+
@names, scanner = [], StringScanner.new(source)
|
34
|
+
|
35
|
+
while scanner.skip_until(/\(/)
|
36
|
+
if scanner.scan(/\?:<([^>]+)>/)
|
37
|
+
@names << scanner[1]
|
38
|
+
elsif scanner.scan(/\?(i?m?x?\-?i?m?x?)?:/)
|
39
|
+
# ignore noncapture
|
40
|
+
else
|
41
|
+
@names << nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
source.gsub!(/\?:<([^>]+)>/, '')
|
45
|
+
|
46
|
+
@names = [] unless @names.any?
|
47
|
+
@names.freeze
|
48
|
+
|
49
|
+
super(source, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def names
|
53
|
+
@names.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
def named_captures
|
57
|
+
named_captures = {}
|
58
|
+
names.each_with_index { |n, i|
|
59
|
+
named_captures[n] = [i+1] if n
|
60
|
+
}
|
61
|
+
named_captures
|
62
|
+
end
|
63
|
+
|
64
|
+
def eql?(other)
|
65
|
+
super && @names.eql?(other.names)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'rack/mount/generatable_regexp'
|
2
|
+
require 'rack/mount/regexp_with_named_groups'
|
3
|
+
require 'rack/mount/utils'
|
4
|
+
|
5
|
+
module Rack::Mount
|
6
|
+
# Route is an internal class used to wrap a single route attributes.
|
7
|
+
#
|
8
|
+
# Plugins should not depend on any method on this class or instantiate
|
9
|
+
# new Route objects. Instead use the factory method, RouteSet#add_route
|
10
|
+
# to create new routes and add them to the set.
|
11
|
+
class Route
|
12
|
+
# Valid rack application to call if conditions are met
|
13
|
+
attr_reader :app
|
14
|
+
|
15
|
+
# A hash of conditions to match against. Conditions may be expressed
|
16
|
+
# as strings or regexps to match against.
|
17
|
+
attr_reader :conditions
|
18
|
+
|
19
|
+
# A hash of values that always gets merged into the parameters hash
|
20
|
+
attr_reader :defaults
|
21
|
+
|
22
|
+
# Symbol identifier for the route used with named route generations
|
23
|
+
attr_reader :name
|
24
|
+
|
25
|
+
attr_reader :named_captures
|
26
|
+
|
27
|
+
def initialize(app, conditions, defaults, name)
|
28
|
+
unless app.respond_to?(:call)
|
29
|
+
raise ArgumentError, 'app must be a valid rack application' \
|
30
|
+
' and respond to call'
|
31
|
+
end
|
32
|
+
@app = app
|
33
|
+
|
34
|
+
@name = name ? name.to_sym : nil
|
35
|
+
@defaults = (defaults || {}).freeze
|
36
|
+
|
37
|
+
@conditions = {}
|
38
|
+
|
39
|
+
conditions.each do |method, pattern|
|
40
|
+
next unless method && pattern
|
41
|
+
|
42
|
+
pattern = Regexp.compile("\\A#{Regexp.escape(pattern)}\\Z") if pattern.is_a?(String)
|
43
|
+
|
44
|
+
if pattern.is_a?(Regexp)
|
45
|
+
pattern = Utils.normalize_extended_expression(pattern)
|
46
|
+
pattern = RegexpWithNamedGroups.new(pattern)
|
47
|
+
pattern.extend(GeneratableRegexp::InstanceMethods)
|
48
|
+
pattern.defaults = @defaults
|
49
|
+
end
|
50
|
+
|
51
|
+
@conditions[method] = pattern.freeze
|
52
|
+
end
|
53
|
+
|
54
|
+
@named_captures = {}
|
55
|
+
@conditions.map { |method, condition|
|
56
|
+
next unless condition.respond_to?(:named_captures)
|
57
|
+
@named_captures[method] = condition.named_captures.inject({}) { |named_captures, (k, v)|
|
58
|
+
named_captures[k.to_sym] = v.last - 1
|
59
|
+
named_captures
|
60
|
+
}.freeze
|
61
|
+
}
|
62
|
+
@named_captures.freeze
|
63
|
+
|
64
|
+
@has_significant_params = @conditions.any? { |method, condition|
|
65
|
+
(condition.respond_to?(:required_params) && condition.required_params.any?) ||
|
66
|
+
(condition.respond_to?(:required_defaults) && condition.required_defaults.any?)
|
67
|
+
}
|
68
|
+
|
69
|
+
if @conditions.has_key?(:path_info) &&
|
70
|
+
!Utils.regexp_anchored?(@conditions[:path_info])
|
71
|
+
@prefix = true
|
72
|
+
@app = Prefix.new(@app)
|
73
|
+
else
|
74
|
+
@prefix = false
|
75
|
+
end
|
76
|
+
|
77
|
+
@conditions.freeze
|
78
|
+
end
|
79
|
+
|
80
|
+
def prefix?
|
81
|
+
@prefix
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def generation_keys
|
86
|
+
@conditions.inject({}) { |keys, (method, condition)|
|
87
|
+
if condition.respond_to?(:required_defaults)
|
88
|
+
keys.merge!(condition.required_defaults)
|
89
|
+
else
|
90
|
+
keys
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def significant_params?
|
96
|
+
@has_significant_params
|
97
|
+
end
|
98
|
+
|
99
|
+
def generate(method, params = {}, recall = {}, options = {})
|
100
|
+
if method.nil?
|
101
|
+
result = @conditions.inject({}) { |h, (m, condition)|
|
102
|
+
if condition.respond_to?(:generate)
|
103
|
+
h[m] = condition.generate(params, recall, options)
|
104
|
+
end
|
105
|
+
h
|
106
|
+
}
|
107
|
+
return nil if result.values.compact.empty?
|
108
|
+
else
|
109
|
+
if condition = @conditions[method]
|
110
|
+
if condition.respond_to?(:generate)
|
111
|
+
result = condition.generate(params, recall, options)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if result
|
117
|
+
@defaults.each do |key, value|
|
118
|
+
params.delete(key) if params[key] == value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def inspect #:nodoc:
|
127
|
+
"#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|