rack-mount 0.0.1

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