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.
@@ -0,0 +1,49 @@
1
+ require 'rack/mount/utils'
2
+
3
+ module Rack::Mount
4
+ unless Const::SUPPORTS_NAMED_CAPTURES
5
+ # A wrapper that adds shim named capture support to older
6
+ # versions of Ruby.
7
+ #
8
+ # Because the named capture syntax causes a parse error, an
9
+ # alternate syntax is used to indicate named captures.
10
+ #
11
+ # Ruby 1.9+ named capture syntax:
12
+ #
13
+ # /(?<foo>[a-z]+)/
14
+ #
15
+ # Ruby 1.8 shim syntax:
16
+ #
17
+ # /(?:<foo>[a-z]+)/
18
+ class RegexpWithNamedGroups < Regexp
19
+ def self.new(regexp) #:nodoc:
20
+ if regexp.is_a?(RegexpWithNamedGroups)
21
+ regexp
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ # Wraps Regexp with named capture support.
28
+ def initialize(regexp)
29
+ regexp, @names = Utils.extract_named_captures(regexp)
30
+ @names.freeze
31
+ super(regexp)
32
+ end
33
+
34
+ def names
35
+ @names.dup
36
+ end
37
+
38
+ def named_captures
39
+ named_captures = {}
40
+ names.each_with_index { |n, i|
41
+ named_captures[n] = [i+1] if n
42
+ }
43
+ named_captures
44
+ end
45
+ end
46
+ else
47
+ RegexpWithNamedGroups = Regexp
48
+ end
49
+ end
@@ -0,0 +1,69 @@
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
+ extend Mixover
13
+
14
+ # Include generation and recognition concerns
15
+ include Generation::Route, Recognition::Route
16
+
17
+ # Valid rack application to call if conditions are met
18
+ attr_reader :app
19
+
20
+ # A hash of conditions to match against. Conditions may be expressed
21
+ # as strings or regexps to match against.
22
+ attr_reader :conditions
23
+
24
+ # A hash of values that always gets merged into the parameters hash
25
+ attr_reader :defaults
26
+
27
+ # Symbol identifier for the route used with named route generations
28
+ attr_reader :name
29
+
30
+ def initialize(set, app, conditions, defaults, name)
31
+ @set = set
32
+
33
+ unless app.respond_to?(:call)
34
+ raise ArgumentError, 'app must be a valid rack application' \
35
+ ' and respond to call'
36
+ end
37
+ @app = app
38
+
39
+ @name = name.to_sym if name
40
+ @defaults = (defaults || {}).freeze
41
+
42
+ unless conditions.is_a?(Hash)
43
+ raise ArgumentError, 'conditions must be a Hash'
44
+ end
45
+ @conditions = {}
46
+
47
+ conditions.each do |method, pattern|
48
+ next unless method && pattern
49
+
50
+ unless @set.valid_conditions.include?(method)
51
+ raise ArgumentError, 'conditions may only include ' +
52
+ @set.valid_conditions.inspect
53
+ end
54
+
55
+ pattern = Regexp.compile("\\A#{Regexp.escape(pattern)}\\Z") if pattern.is_a?(String)
56
+ pattern = Utils.normalize_extended_expression(pattern)
57
+ pattern = RegexpWithNamedGroups.new(pattern)
58
+ pattern.extend(GeneratableRegexp::InstanceMethods)
59
+ @conditions[method] = pattern.freeze
60
+ end
61
+
62
+ @conditions.freeze
63
+ end
64
+
65
+ def inspect #:nodoc:
66
+ "#<#{self.class.name} @app=#{@app.inspect} @conditions=#{@conditions.inspect} @defaults=#{@defaults.inspect} @name=#{@name.inspect}>"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,109 @@
1
+ require 'rack/mount/multimap'
2
+ require 'rack/mount/route'
3
+ require 'rack/mount/utils'
4
+
5
+ module Rack::Mount
6
+ class RouteSet
7
+ extend Mixover
8
+
9
+ # Include generation and recognition concerns
10
+ include Generation::RouteSet, Recognition::RouteSet
11
+ include Recognition::CodeGeneration
12
+
13
+ # Initialize a new RouteSet without optimizations
14
+ def self.new_without_optimizations(*args, &block)
15
+ new_without_module(Recognition::CodeGeneration, *args, &block)
16
+ end
17
+
18
+ # Basic RouteSet initializer.
19
+ #
20
+ # If a block is given, the set is yielded and finalized.
21
+ #
22
+ # See other aspects for other valid options:
23
+ # - <tt>Generation::RouteSet.new</tt>
24
+ # - <tt>Recognition::RouteSet.new</tt>
25
+ def initialize(options = {}, &block)
26
+ @request_class = options.delete(:request_class) || Rack::Request
27
+ @routes = []
28
+ expire!
29
+
30
+ if block_given?
31
+ yield self
32
+ rehash
33
+ end
34
+ end
35
+
36
+ # Builder method to add a route to the set
37
+ #
38
+ # <tt>app</tt>:: A valid Rack app to call if the conditions are met.
39
+ # <tt>conditions</tt>:: A hash of conditions to match against.
40
+ # Conditions may be expressed as strings or
41
+ # regexps to match against.
42
+ # <tt>defaults</tt>:: A hash of values that always gets merged in
43
+ # <tt>name</tt>:: Symbol identifier for the route used with named
44
+ # route generations
45
+ def add_route(app, conditions = {}, defaults = {}, name = nil)
46
+ route = Route.new(self, app, conditions, defaults, name)
47
+ @routes << route
48
+ expire!
49
+ route
50
+ end
51
+
52
+ # See <tt>Recognition::RouteSet#call</tt>
53
+ def call(env)
54
+ raise NotImplementedError
55
+ end
56
+
57
+ # See <tt>Generation::RouteSet#url</tt>
58
+ def url(*args)
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # See <tt>Generation::RouteSet#generate</tt>
63
+ def generate(*args)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ # Number of routes in the set
68
+ def length
69
+ @routes.length
70
+ end
71
+
72
+ def rehash #:nodoc:
73
+ end
74
+
75
+ # Finalizes the set and builds optimized data structures. You *must*
76
+ # freeze the set before you can use <tt>call</tt> and <tt>url</tt>.
77
+ # So remember to call freeze after you are done adding routes.
78
+ def freeze
79
+ unless frozen?
80
+ rehash
81
+ @routes.each { |route| route.freeze }
82
+ @routes.freeze
83
+ end
84
+
85
+ super
86
+ end
87
+
88
+ private
89
+ def expire! #:nodoc:
90
+ end
91
+
92
+ # An internal helper method for constructing a nested set from
93
+ # the linear route set.
94
+ #
95
+ # build_nested_route_set([:request_method, :path_info]) { |route, method|
96
+ # route.send(method)
97
+ # }
98
+ def build_nested_route_set(keys, &block)
99
+ graph = Multimap.new
100
+ @routes.each_with_index do |route, index|
101
+ k = keys.map { |key| block.call(key, index) }
102
+ Utils.pop_trailing_nils!(k)
103
+ k.map! { |key| key || /.+/ }
104
+ graph[*k] = route
105
+ end
106
+ graph
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,93 @@
1
+ require 'strscan'
2
+
3
+ module Rack::Mount
4
+ class Strexp < Regexp
5
+ # Parses segmented string expression and converts it into a Regexp
6
+ #
7
+ # Strexp.compile('foo')
8
+ # # => %r{\Afoo\Z}
9
+ #
10
+ # Strexp.compile('foo/:bar', {}, ['/'])
11
+ # # => %r{\Afoo/(?<bar>[^/]+)\Z}
12
+ #
13
+ # Strexp.compile(':foo.example.com')
14
+ # # => %r{\A(?<foo>.+)\.example\.com\Z}
15
+ #
16
+ # Strexp.compile('foo/:bar', {:bar => /[a-z]+/}, ['/'])
17
+ # # => %r{\Afoo/(?<bar>[a-z]+)\Z}
18
+ #
19
+ # Strexp.compile('foo(.:extension)')
20
+ # # => %r{\Afoo(\.(?<extension>.+))?\Z}
21
+ #
22
+ # Strexp.compile('src/*files')
23
+ # # => %r{\Asrc/(?<files>.+)\Z}
24
+ def initialize(str, requirements = {}, separators = [])
25
+ return super(str) if str.is_a?(Regexp)
26
+
27
+ re = Regexp.escape(str)
28
+ requirements = requirements ? requirements.dup : {}
29
+
30
+ normalize_requirements!(requirements, separators)
31
+ parse_dynamic_segments!(re, requirements)
32
+ parse_optional_segments!(re)
33
+
34
+ super("\\A#{re}\\Z")
35
+ end
36
+
37
+ private
38
+ def normalize_requirements!(requirements, separators)
39
+ requirements.each do |key, value|
40
+ if value.is_a?(Regexp)
41
+ if regexp_has_modifiers?(value)
42
+ requirements[key] = value
43
+ else
44
+ requirements[key] = value.source
45
+ end
46
+ else
47
+ requirements[key] = Regexp.escape(value)
48
+ end
49
+ end
50
+ requirements.default ||= separators.any? ?
51
+ "[^#{separators.join}]+" : '.+'
52
+ requirements
53
+ end
54
+
55
+ def parse_dynamic_segments!(str, requirements)
56
+ re, pos, scanner = '', 0, StringScanner.new(str)
57
+ while scanner.scan_until(/(:|\\\*)([a-zA-Z_]\w*)/)
58
+ pre, pos = scanner.pre_match[pos..-1], scanner.pos
59
+ if pre =~ /(.*)\\\\\Z/
60
+ re << $1 + scanner.matched
61
+ else
62
+ name = scanner[2].to_sym
63
+ requirement = scanner[1] == ':' ?
64
+ requirements[name] : '.+'
65
+ re << pre + Const::REGEXP_NAMED_CAPTURE % [name, requirement]
66
+ end
67
+ end
68
+ re << scanner.rest
69
+ str.replace(re)
70
+ end
71
+
72
+ def parse_optional_segments!(str)
73
+ re, pos, scanner = '', 0, StringScanner.new(str)
74
+ while scanner.scan_until(/\\\(|\\\)/)
75
+ pre, pos = scanner.pre_match[pos..-1], scanner.pos
76
+ if pre =~ /(.*)\\\\\Z/
77
+ re << $1 + scanner.matched
78
+ elsif scanner.matched == '\\('
79
+ # re << pre + '(?:'
80
+ re << pre + '('
81
+ elsif scanner.matched == '\\)'
82
+ re << pre + ')?'
83
+ end
84
+ end
85
+ re << scanner.rest
86
+ str.replace(re)
87
+ end
88
+
89
+ def regexp_has_modifiers?(regexp)
90
+ regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,271 @@
1
+ require 'rack/mount/regexp_with_named_groups'
2
+ require 'strscan'
3
+ require 'uri'
4
+
5
+ module Rack::Mount
6
+ # Private utility methods used throughout Rack::Mount.
7
+ #--
8
+ # This module is a trash can. Try to move these functions into
9
+ # more appropriate contexts.
10
+ #++
11
+ module Utils
12
+ # Normalizes URI path.
13
+ #
14
+ # Strips off trailing slash and ensures there is a leading slash.
15
+ #
16
+ # normalize_path("/foo") # => "/foo"
17
+ # normalize_path("/foo/") # => "/foo"
18
+ # normalize_path("foo") # => "/foo"
19
+ # normalize_path("") # => "/"
20
+ def normalize_path(path)
21
+ path = "/#{path}"
22
+ path.squeeze!(Const::SLASH)
23
+ path.sub!(%r{/+\Z}, Const::EMPTY_STRING)
24
+ path = Const::SLASH if path == Const::EMPTY_STRING
25
+ path
26
+ end
27
+ module_function :normalize_path
28
+
29
+ # Removes trailing nils from array.
30
+ #
31
+ # pop_trailing_nils!([1, 2, 3]) # => [1, 2, 3]
32
+ # pop_trailing_nils!([1, 2, 3, nil, nil]) # => [1, 2, 3]
33
+ # pop_trailing_nils!([nil]) # => []
34
+ def pop_trailing_nils!(ary)
35
+ while ary.length > 0 && ary.last.nil?
36
+ ary.pop
37
+ end
38
+ ary
39
+ end
40
+ module_function :pop_trailing_nils!
41
+
42
+ RESERVED_PCHAR = ':@&=+$,;%'
43
+ SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
44
+ if RUBY_VERSION >= '1.9'
45
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
46
+ else
47
+ UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
48
+ end
49
+
50
+ def escape_uri(uri)
51
+ URI.escape(uri.to_s, UNSAFE_PCHAR)
52
+ end
53
+ module_function :escape_uri
54
+
55
+ if ''.respond_to?(:force_encoding)
56
+ def unescape_uri(uri)
57
+ URI.unescape(uri).force_encoding('utf-8')
58
+ end
59
+ else
60
+ def unescape_uri(uri)
61
+ URI.unescape(uri)
62
+ end
63
+ end
64
+ module_function :unescape_uri
65
+
66
+ # Taken from Rack 1.1.x to build nested query strings
67
+ def build_nested_query(value, prefix = nil) #:nodoc:
68
+ case value
69
+ when Array
70
+ value.map { |v|
71
+ build_nested_query(v, "#{prefix}[]")
72
+ }.join("&")
73
+ when Hash
74
+ value.map { |k, v|
75
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
76
+ }.join("&")
77
+ when String
78
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
79
+ "#{Rack::Utils.escape(prefix)}=#{Rack::Utils.escape(value)}"
80
+ when NilClass
81
+ Rack::Utils.escape(prefix)
82
+ else
83
+ if value.respond_to?(:to_param)
84
+ build_nested_query(value.to_param.to_s, prefix)
85
+ else
86
+ Rack::Utils.escape(prefix)
87
+ end
88
+ end
89
+ end
90
+ module_function :build_nested_query
91
+
92
+ def normalize_extended_expression(regexp)
93
+ return regexp unless regexp.options & Regexp::EXTENDED != 0
94
+ source = regexp.source
95
+ source.gsub!(/#.+$/, '')
96
+ source.gsub!(/\s+/, '')
97
+ source.gsub!(/\\\//, '/')
98
+ Regexp.compile(source)
99
+ end
100
+ module_function :normalize_extended_expression
101
+
102
+ # Determines whether the regexp must match the entire string.
103
+ #
104
+ # regexp_anchored?(/^foo$/) # => true
105
+ # regexp_anchored?(/foo/) # => false
106
+ # regexp_anchored?(/^foo/) # => false
107
+ # regexp_anchored?(/foo$/) # => false
108
+ def regexp_anchored?(regexp)
109
+ regexp.source =~ /\A(\\A|\^).*(\\Z|\$)\Z/ ? true : false
110
+ end
111
+ module_function :regexp_anchored?
112
+
113
+ # Returns static string source of Regexp if it only includes static
114
+ # characters and no metacharacters. Otherwise the original Regexp is
115
+ # returned.
116
+ #
117
+ # extract_static_regexp(/^foo$/) # => "foo"
118
+ # extract_static_regexp(/^foo\.bar$/) # => "foo.bar"
119
+ # extract_static_regexp(/^foo|bar$/) # => /^foo|bar$/
120
+ def extract_static_regexp(regexp, options = nil)
121
+ if regexp.is_a?(String)
122
+ regexp = Regexp.compile("\\A#{regexp}\\Z", options)
123
+ end
124
+
125
+ # Just return if regexp is case-insensitive
126
+ return regexp if regexp.casefold?
127
+
128
+ source = regexp.source
129
+ if regexp_anchored?(regexp)
130
+ source.sub!(/^(\\A|\^)(.*)(\\Z|\$)$/, '\2')
131
+ unescaped_source = source.gsub(/\\/, Const::EMPTY_STRING)
132
+ if source == Regexp.escape(unescaped_source) &&
133
+ Regexp.compile("\\A(#{source})\\Z") =~ unescaped_source
134
+ return unescaped_source
135
+ end
136
+ end
137
+ regexp
138
+ end
139
+ module_function :extract_static_regexp
140
+
141
+ if Const::SUPPORTS_NAMED_CAPTURES
142
+ NAMED_CAPTURE_REGEXP = /\?<([^>]+)>/
143
+ else
144
+ NAMED_CAPTURE_REGEXP = /\?:<([^>]+)>/
145
+ end
146
+
147
+ # Strips shim named capture syntax and returns a clean Regexp and
148
+ # an ordered array of the named captures.
149
+ #
150
+ # extract_named_captures(/[a-z]+/) # => /[a-z]+/, []
151
+ # extract_named_captures(/(?:<foo>[a-z]+)/) # => /([a-z]+)/, ['foo']
152
+ # extract_named_captures(/([a-z]+)(?:<foo>[a-z]+)/)
153
+ # # => /([a-z]+)([a-z]+)/, [nil, 'foo']
154
+ def extract_named_captures(regexp)
155
+ options = regexp.is_a?(Regexp) ? regexp.options : nil
156
+ source = Regexp.compile(regexp).source
157
+ names, scanner = [], StringScanner.new(source)
158
+
159
+ while scanner.skip_until(/\(/)
160
+ if scanner.scan(NAMED_CAPTURE_REGEXP)
161
+ names << scanner[1]
162
+ else
163
+ names << nil
164
+ end
165
+ end
166
+
167
+ names = [] unless names.any?
168
+ source.gsub!(NAMED_CAPTURE_REGEXP, Const::EMPTY_STRING)
169
+ return Regexp.compile(source, options), names
170
+ end
171
+ module_function :extract_named_captures
172
+
173
+ class Capture < Array #:nodoc:
174
+ attr_reader :name, :optional
175
+ alias_method :optional?, :optional
176
+
177
+ def initialize(*args)
178
+ options = args.last.is_a?(Hash) ? args.pop : {}
179
+
180
+ @name = options.delete(:name)
181
+ @name = @name.to_s if @name
182
+
183
+ @optional = options.delete(:optional) || false
184
+
185
+ super(args)
186
+ end
187
+
188
+ def ==(obj)
189
+ obj.is_a?(Capture) && @name == obj.name && @optional == obj.optional && super
190
+ end
191
+
192
+ def optionalize!
193
+ @optional = true
194
+ self
195
+ end
196
+
197
+ def named?
198
+ name && name != Const::EMPTY_STRING
199
+ end
200
+
201
+ def to_s
202
+ source = "(#{join})"
203
+ source << '?' if optional?
204
+ source
205
+ end
206
+
207
+ def first_part
208
+ first.is_a?(Capture) ? first.first_part : first
209
+ end
210
+
211
+ def last_part
212
+ last.is_a?(Capture) ? last.last_part : last
213
+ end
214
+ end
215
+
216
+ def extract_regexp_parts(regexp) #:nodoc:
217
+ unless regexp.is_a?(RegexpWithNamedGroups)
218
+ regexp = RegexpWithNamedGroups.new(regexp)
219
+ end
220
+
221
+ if regexp.source =~ /\?<([^>]+)>/
222
+ regexp, names = extract_named_captures(regexp)
223
+ else
224
+ names = regexp.names
225
+ end
226
+ source = regexp.source
227
+
228
+ source =~ /^(\\A|\^)/ ? source.gsub!(/^(\\A|\^)/, Const::EMPTY_STRING) :
229
+ raise(ArgumentError, "#{source} needs to match the start of the string")
230
+
231
+ scanner = StringScanner.new(source)
232
+ stack = [[]]
233
+
234
+ capture_index = 0
235
+ until scanner.eos?
236
+ char = scanner.getch
237
+ cur = stack.last
238
+
239
+ escaped = cur.last.is_a?(String) && cur.last[-1, 1] == '\\'
240
+
241
+ if char == '\\' && scanner.peek(1) == 'Z'
242
+ scanner.pos += 1
243
+ cur.push(Const::NULL)
244
+ elsif escaped
245
+ cur.push('') unless cur.last.is_a?(String)
246
+ cur.last << char
247
+ elsif char == '('
248
+ name = names[capture_index]
249
+ capture = Capture.new(:name => name)
250
+ capture_index += 1
251
+ cur.push(capture)
252
+ stack.push(capture)
253
+ elsif char == ')'
254
+ capture = stack.pop
255
+ if scanner.peek(1) == '?'
256
+ scanner.pos += 1
257
+ capture.optionalize!
258
+ end
259
+ elsif char == '$'
260
+ cur.push(Const::NULL)
261
+ else
262
+ cur.push('') unless cur.last.is_a?(String)
263
+ cur.last << char
264
+ end
265
+ end
266
+
267
+ stack.pop
268
+ end
269
+ module_function :extract_regexp_parts
270
+ end
271
+ end