mustermann19 0.3.1 → 0.3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +4 -3
  3. data/README.md +680 -376
  4. data/lib/mustermann/ast/compiler.rb +13 -7
  5. data/lib/mustermann/ast/expander.rb +11 -5
  6. data/lib/mustermann/ast/node.rb +27 -1
  7. data/lib/mustermann/ast/param_scanner.rb +20 -0
  8. data/lib/mustermann/ast/parser.rb +131 -12
  9. data/lib/mustermann/ast/pattern.rb +45 -6
  10. data/lib/mustermann/ast/template_generator.rb +28 -0
  11. data/lib/mustermann/ast/validation.rb +5 -3
  12. data/lib/mustermann/composite.rb +103 -0
  13. data/lib/mustermann/expander.rb +1 -1
  14. data/lib/mustermann/express.rb +34 -0
  15. data/lib/mustermann/flask.rb +204 -0
  16. data/lib/mustermann/identity.rb +54 -0
  17. data/lib/mustermann/pattern.rb +186 -12
  18. data/lib/mustermann/pattern_cache.rb +49 -0
  19. data/lib/mustermann/pyramid.rb +25 -0
  20. data/lib/mustermann/regexp_based.rb +18 -1
  21. data/lib/mustermann/regular.rb +1 -1
  22. data/lib/mustermann/shell.rb +8 -0
  23. data/lib/mustermann/simple.rb +1 -1
  24. data/lib/mustermann/simple_match.rb +5 -0
  25. data/lib/mustermann/sinatra.rb +19 -5
  26. data/lib/mustermann/string_scanner.rb +314 -0
  27. data/lib/mustermann/template.rb +10 -0
  28. data/lib/mustermann/to_pattern.rb +11 -6
  29. data/lib/mustermann/version.rb +1 -1
  30. data/lib/mustermann.rb +52 -3
  31. data/mustermann.gemspec +1 -1
  32. data/spec/composite_spec.rb +147 -0
  33. data/spec/expander_spec.rb +15 -0
  34. data/spec/express_spec.rb +209 -0
  35. data/spec/flask_spec.rb +361 -0
  36. data/spec/flask_subclass_spec.rb +368 -0
  37. data/spec/identity_spec.rb +44 -0
  38. data/spec/mustermann_spec.rb +14 -0
  39. data/spec/pattern_spec.rb +7 -3
  40. data/spec/pyramid_spec.rb +101 -0
  41. data/spec/rails_spec.rb +76 -2
  42. data/spec/regular_spec.rb +25 -0
  43. data/spec/shell_spec.rb +33 -0
  44. data/spec/simple_spec.rb +25 -0
  45. data/spec/sinatra_spec.rb +184 -9
  46. data/spec/string_scanner_spec.rb +271 -0
  47. data/spec/support/expand_matcher.rb +7 -5
  48. data/spec/support/generate_template_matcher.rb +27 -0
  49. data/spec/support/pattern.rb +3 -0
  50. data/spec/support/scan_matcher.rb +63 -0
  51. data/spec/support.rb +2 -1
  52. data/spec/template_spec.rb +22 -0
  53. data/spec/to_pattern_spec.rb +49 -0
  54. metadata +47 -61
  55. data/internals.md +0 -64
@@ -0,0 +1,204 @@
1
+ require 'mustermann/ast/pattern'
2
+
3
+ module Mustermann
4
+ # Flask style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/<foo>', type: :flask) === '/bar' # => true
8
+ #
9
+ # @see Mustermann::Pattern
10
+ # @see file:README.md#flask Syntax description in the README
11
+ class Flask < AST::Pattern
12
+ on(nil, ?>, ?:) { |c| unexpected(c) }
13
+
14
+ on(?<) do |char|
15
+ converter_name = expect(/\w+/, char: char)
16
+ args, opts = scan(?() ? read_args(?=, ?)) : [[], {}]
17
+
18
+ if scan(?:)
19
+ name = read_escaped(?>)
20
+ else
21
+ converter_name, name = 'default', converter_name
22
+ expect(?>)
23
+ end
24
+
25
+ converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) }
26
+ converter = converter.new(*args, opts) if converter.respond_to? :new
27
+ constraint = converter.constraint if converter.respond_to? :constraint
28
+ convert = converter.convert if converter.respond_to? :convert
29
+ qualifier = converter.qualifier if converter.respond_to? :qualifier
30
+ node_type = converter.node_type if converter.respond_to? :node_type
31
+ node_type ||= :capture
32
+
33
+ node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier)
34
+ end
35
+
36
+ # A class for easy creating of converters.
37
+ # @see Mustermann::Flask#register_converter
38
+ class Converter
39
+ # Constraint on the format used for the capture.
40
+ # Should be a regexp (or a string corresponding to a regexp)
41
+ # @see Mustermann::Flask#register_converter
42
+ attr_accessor :constraint
43
+
44
+ # Callback
45
+ # Should be a Proc.
46
+ # @see Mustermann::Flask#register_converter
47
+ attr_accessor :convert
48
+
49
+ # Constraint on the format used for the capture.
50
+ # Should be a regexp (or a string corresponding to a regexp)
51
+ # @see Mustermann::Flask#register_converter
52
+ # @!visibility private
53
+ attr_accessor :node_type
54
+
55
+ # Constraint on the format used for the capture.
56
+ # Should be a regexp (or a string corresponding to a regexp)
57
+ # @see Mustermann::Flask#register_converter
58
+ # @!visibility private
59
+ attr_accessor :qualifier
60
+
61
+ # @!visibility private
62
+ def self.create(&block)
63
+ Class.new(self) do
64
+ define_method(:initialize) { |*a| o = a.last.kind_of?(Hash) ? a.pop : {}; block[self, *a, o] }
65
+ end
66
+ end
67
+
68
+ # Makes sure a given value falls inbetween a min and a max.
69
+ # Uses the passed block to convert the value from a string to whatever
70
+ # format you'd expect.
71
+ #
72
+ # @example
73
+ # require 'mustermann/flask'
74
+ #
75
+ # class MyPattern < Mustermann::Flask
76
+ # register_converter(:x) { between(5, 15, &:to_i) }
77
+ # end
78
+ #
79
+ # pattern = MyPattern.new('<x:id>')
80
+ # pattern.params('/12') # => { 'id' => 12 }
81
+ # pattern.params('/16') # => { 'id' => 15 }
82
+ #
83
+ # @see Mustermann::Flask#register_converter
84
+ def between(min, max)
85
+ self.convert = proc do |input|
86
+ value = yield(input)
87
+ value = yield(min) if min and value < yield(min)
88
+ value = yield(max) if max and value > yield(max)
89
+ value
90
+ end
91
+ end
92
+ end
93
+
94
+ # Generally available converters.
95
+ # @!visibility private
96
+ def self.converters(inherited = true)
97
+ return @converters ||= {} unless inherited
98
+ defaults = superclass.respond_to?(:converters) ? superclass.converters : {}
99
+ defaults.merge(converters(false))
100
+ end
101
+
102
+ # Allows you to register your own converters.
103
+ #
104
+ # It is reommended to use this on a subclass, so to not influence other subsystems
105
+ # using flask templates.
106
+ #
107
+ # The object passed in as converter can implement #convert and/or #constraint.
108
+ #
109
+ # It can also instead implement #new, which will then return an object responding
110
+ # to some of these methods. Arguments from the flask pattern will be passed to #new.
111
+ #
112
+ # If passed a block, it will be yielded to with a {Mustermann::Flask::Converter}
113
+ # instance and any arguments in the flask pattern.
114
+ #
115
+ # @example with simple object
116
+ # require 'mustermann/flask'
117
+ #
118
+ # MyPattern = Class.new(Mustermann::Flask)
119
+ # up_converter = Struct.new(:convert).new(:upcase.to_proc)
120
+ # MyPattern.register_converter(:upper, up_converter)
121
+ #
122
+ # MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
123
+ #
124
+ # @example with block
125
+ # require 'mustermann/flask'
126
+ #
127
+ # MyPattern = Class.new(Mustermann::Flask)
128
+ # MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc }
129
+ #
130
+ # MyPattern.new("/<up:name>").params('/foo') # => { "name" => "FOO" }
131
+ #
132
+ # @example with converter class
133
+ # require 'mustermann/flasl'
134
+ #
135
+ # class MyPattern < Mustermann::Flask
136
+ # class Converter
137
+ # attr_reader :convert
138
+ # def initialize(send: :to_s)
139
+ # @convert = send.to_sym.to_proc
140
+ # end
141
+ # end
142
+ #
143
+ # register_converter(:t, Converter)
144
+ # end
145
+ #
146
+ # MyPattern.new("/<t(send=upcase):name>").params('/Foo') # => { "name" => "FOO" }
147
+ # MyPattern.new("/<t(send=downcase):name>").params('/Foo') # => { "name" => "foo" }
148
+ #
149
+ # @param [#to_s] name converter name
150
+ # @param [#new, #convert, #constraint, nil] converter
151
+ def self.register_converter(name, converter = nil, &block)
152
+ converter ||= Converter.create(&block)
153
+ converters(false)[name.to_s] = converter
154
+ end
155
+
156
+ register_converter(:string) do |converter, options = {}|
157
+ minlength = options.delete(:minlength)
158
+ maxlength = options.delete(:maxlength)
159
+ length = options.delete(:length)
160
+ converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength
161
+ converter.qualifier = "{%s}" % length if length
162
+ end
163
+
164
+ register_converter(:int) do |converter, options = {}|
165
+ min = options.delete(:min)
166
+ max = options.delete(:max)
167
+ fixed_digits = options.fetch(:fixed_digits, false)
168
+ options.delete(:fixed_digits)
169
+ converter.constraint = /\d/
170
+ converter.qualifier = "{#{fixed_digits}}" if fixed_digits
171
+ converter.between(min, max) { |string| Integer(string) }
172
+ end
173
+
174
+ register_converter(:float) do |converter, options = {}|
175
+ min = options.delete(:min)
176
+ max = options.delete(:max)
177
+ converter.constraint = /\d*\.?\d+/
178
+ converter.qualifier = ""
179
+ converter.between(min, max) { |string| Float(string) }
180
+ end
181
+
182
+ register_converter(:path) do |converter|
183
+ converter.node_type = :named_splat
184
+ end
185
+
186
+ register_converter(:any) do |converter, *strings|
187
+ strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact
188
+ converter.qualifier = ""
189
+ converter.constraint = Regexp.union(*strings)
190
+ end
191
+
192
+ register_converter(:default, converters['string'])
193
+
194
+ supported_options :converters
195
+ attr_reader :converters
196
+
197
+ def initialize(input, options = {})
198
+ converters = options[:converters] || {}
199
+ @converters = self.class.converters.dup
200
+ converters.each { |k,v| @converters[k.to_s] = v } if converters
201
+ super(input, options)
202
+ end
203
+ end
204
+ end
@@ -1,4 +1,5 @@
1
1
  require 'mustermann/pattern'
2
+ require 'mustermann/ast/node'
2
3
 
3
4
  module Mustermann
4
5
  # Matches strings that are identical to the pattern.
@@ -15,5 +16,58 @@ module Mustermann
15
16
  def ===(string)
16
17
  unescape(string) == @string
17
18
  end
19
+
20
+ # @param (see Mustermann::Pattern#peek_size)
21
+ # @return (see Mustermann::Pattern#peek_size)
22
+ # @see (see Mustermann::Pattern#peek_size)
23
+ def peek_size(string)
24
+ return unless unescape(string).start_with? @string
25
+ return @string.size if string.start_with? @string # optimization
26
+ @string.each_char.with_index.inject(0) do |count, (char, index)|
27
+ char_size = 1
28
+ escaped = @@uri.escape(char, /./)
29
+ char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase
30
+ count + char_size
31
+ end
32
+ end
33
+
34
+ # URI templates support generating templates (the logic is quite complex, though).
35
+ #
36
+ # @example (see Mustermann::Pattern#to_templates)
37
+ # @param (see Mustermann::Pattern#to_templates)
38
+ # @return (see Mustermann::Pattern#to_templates)
39
+ # @see Mustermann::Pattern#to_templates
40
+ def to_templates
41
+ [@@uri.escape(to_s)]
42
+ end
43
+
44
+ # Generates an AST so it's compatible with {Mustermann::AST::Pattern}.
45
+ # Not used internally by {Mustermann::Identity}.
46
+ # @!visibility private
47
+ def to_ast
48
+ payload = @string.each_char.map { |c| AST::Node[c == ?/ ? :separator : :char].new(c) }
49
+ AST::Node[:root].new(payload, pattern: @string)
50
+ end
51
+
52
+ # Identity patterns support expanding.
53
+ #
54
+ # This implementation does not use {Mustermann::Expander} internally to save memory and
55
+ # compilation time.
56
+ #
57
+ # @example (see Mustermann::Pattern#expand)
58
+ # @param (see Mustermann::Pattern#expand)
59
+ # @return (see Mustermann::Pattern#expand)
60
+ # @raise (see Mustermann::Pattern#expand)
61
+ # @see Mustermann::Pattern#expand
62
+ # @see Mustermann::Expander
63
+ def expand(behavior = nil, values = {})
64
+ values, behavior = behavior, nil if behavior.kind_of?(Hash)
65
+ return to_s if values.empty? or behavior == :ignore
66
+ raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
67
+ raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
68
+ params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w\d]/) }
69
+ separator = @string.include?(??) ? ?& : ??
70
+ @string + separator + params.join(?&)
71
+ end
18
72
  end
19
73
  end
@@ -8,7 +8,9 @@ module Mustermann
8
8
  # @abstract
9
9
  class Pattern
10
10
  include Mustermann
11
+ @@uri ||= URI::Parser.new
11
12
 
13
+ PATTERN_METHODS = %w[expand to_templates].map(&:to_sym)
12
14
  # List of supported options.
13
15
  #
14
16
  # @overload supported_options
@@ -28,7 +30,7 @@ module Mustermann
28
30
 
29
31
  # @param [Symbol] option The option to check.
30
32
  # @return [Boolean] Whether or not option is supported.
31
- def self.supported?(option)
33
+ def self.supported?(option, options = {})
32
34
  supported_options.include? option
33
35
  end
34
36
 
@@ -42,7 +44,7 @@ module Mustermann
42
44
  ignore_unknown_options = options.fetch(:ignore_unknown_options, false)
43
45
  options.delete(:ignore_unknown_options)
44
46
  unless ignore_unknown_options
45
- unsupported = options.keys.detect { |key| not supported?(key) }
47
+ unsupported = options.keys.detect { |key| not supported?(key, options) }
46
48
  raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
47
49
  end
48
50
 
@@ -93,13 +95,73 @@ module Mustermann
93
95
  raise NotImplementedError, 'subclass responsibility'
94
96
  end
95
97
 
96
- # @return [Array<String>] capture names.
98
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
99
+ # Will return the count of the matching characters if it matches.
100
+ #
101
+ # @example
102
+ # pattern = Mustermann.new('/:name')
103
+ # pattern.size("/Frank/Sinatra") # => 6
104
+ #
105
+ # @param [String] string The string to match against
106
+ # @return [Integer, nil] the number of characters that match
107
+ def peek_size(string)
108
+ # this is a very naive, unperformant implementation
109
+ string.size.downto(0).detect { |s| self === string[0, s] }
110
+ end
111
+
112
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
113
+ # Will return the substring if it matches.
114
+ #
115
+ # @example
116
+ # pattern = Mustermann.new('/:name')
117
+ # pattern.peek("/Frank/Sinatra") # => "/Frank"
118
+ #
119
+ # @param [String] string The string to match against
120
+ # @return [String, nil] matched subsctring
121
+ def peek(string)
122
+ size = peek_size(string)
123
+ string[0, size] if size
124
+ end
125
+
126
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
127
+ # Will return a MatchData or similar instance for the matched substring.
128
+ #
129
+ # @example
130
+ # pattern = Mustermann.new('/:name')
131
+ # pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
132
+ #
133
+ # @param [String] string The string to match against
134
+ # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
135
+ # @see #peek_params
136
+ def peek_match(string)
137
+ matched = peek(string)
138
+ match(matched) if matched
139
+ end
140
+
141
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
142
+ # Will return a two element Array with the params parsed from the substring as first entry and the length of
143
+ # the substring as second.
144
+ #
145
+ # @example
146
+ # pattern = Mustermann.new('/:name')
147
+ # params, _ = pattern.peek_params("/Frank/Sinatra")
148
+ #
149
+ # puts "Hello, #{params['name']}!" # Hello, Frank!
150
+ #
151
+ # @param [String] string The string to match against
152
+ # @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
153
+ def peek_params(string)
154
+ match = peek_match(string)
155
+ [params(nil, :captures => match), match.to_s.size] if match
156
+ end
157
+
158
+ # @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
97
159
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
98
160
  def named_captures
99
161
  {}
100
162
  end
101
163
 
102
- # @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
164
+ # @return [Array<String>] capture names.
103
165
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
104
166
  def names
105
167
  []
@@ -109,8 +171,8 @@ module Mustermann
109
171
  # @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
110
172
  def params(string = nil, options = {})
111
173
  options, string = string, nil if string.is_a?(Hash)
112
- captures = options[:captures]
113
- offset = options[:offset] || 0
174
+ captures = options.fetch(:captures, nil)
175
+ offset = options.fetch(:offset, 0)
114
176
  return unless captures ||= match(string)
115
177
  params = named_captures.map do |name, positions|
116
178
  values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
@@ -135,20 +197,127 @@ module Mustermann
135
197
  # warn "does not support expanding"
136
198
  # end
137
199
  #
138
- # @param [Hash{Symbol: #to_s, Array<#to_s>}] values values to use for expansion
200
+ # Expanding is supported by almost all patterns (notable execptions are {Mustermann::Shell},
201
+ # {Mustermann::Regular} and {Mustermann::Simple}).
202
+ #
203
+ # Union {Mustermann::Composite} patterns (with the | operator) support expanding if all
204
+ # patterns they are composed of also support it.
205
+ #
206
+ # @param (see Mustermann::Expander#expand)
139
207
  # @return [String] expanded string
140
208
  # @raise [NotImplementedError] raised if expand is not supported.
141
209
  # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
142
210
  # @see Mustermann::Expander
143
- def expand(values = {})
211
+ def expand(behavior = nil, values = {})
144
212
  raise NotImplementedError, "expanding not supported by #{self.class}"
145
213
  end
146
214
 
215
+ # @note This method is only implemented by certain subclasses.
216
+ #
217
+ # Generates a list of URI template strings representing the pattern.
218
+ #
219
+ # Note that this transformation is lossy and the strings matching these
220
+ # templates might not match the pattern (and vice versa).
221
+ #
222
+ # This comes in quite handy since URI templates are not made for pattern matching.
223
+ # That way you can easily use a more precise template syntax and have it automatically
224
+ # generate hypermedia links for you.
225
+ #
226
+ # @example generating templates
227
+ # Mustermann.new("/:name").to_templates # => ["/{name}"]
228
+ # Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"]
229
+ # Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"]
230
+ #
231
+ # @example generating templates from composite patterns
232
+ # pattern = Mustermann.new('/:name')
233
+ # pattern |= Mustermann.new('/{name}', type: :template)
234
+ # pattern |= Mustermann.new('/example/*nested')
235
+ # pattern.to_templates # => ["/{name}", "/example/{+nested}"]
236
+ #
237
+ # Template generation is supported by {Mustermann::Sinatra}, {Mustermann::Rails},
238
+ # {Mustermann::Template} and {Mustermann::Identity} patterns. Union {Mustermann::Composite}
239
+ # patterns (with the | operator) support template generation if all patterns they are composed
240
+ # of also support it.
241
+ #
242
+ # @example Checking if a pattern supports expanding
243
+ # if pattern.respond_to? :to_templates
244
+ # pattern.to_templates
245
+ # else
246
+ # warn "does not support template generation"
247
+ # end
248
+ #
249
+ # @return [Array<String>] list of URI templates
250
+ def to_templates
251
+ raise NotImplementedError, "template generation not supported by #{self.class}"
252
+ end
253
+
254
+ # @overload |(other)
255
+ # Creates a pattern that matches any string matching either one of the patterns.
256
+ # If a string is supplied, it is treated as an identity pattern.
257
+ #
258
+ # @example
259
+ # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second')
260
+ # pattern === '/foo/bar' # => true
261
+ # pattern === '/fox/bar' # => true
262
+ # pattern === '/foo' # => false
263
+ #
264
+ # @overload &(other)
265
+ # Creates a pattern that matches any string matching both of the patterns.
266
+ # If a string is supplied, it is treated as an identity pattern.
267
+ #
268
+ # @example
269
+ # pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second')
270
+ # pattern === '/foo/bar' # => true
271
+ # pattern === '/fox/bar' # => false
272
+ # pattern === '/foo' # => false
273
+ #
274
+ # @overload ^(other)
275
+ # Creates a pattern that matches any string matching exactly one of the patterns.
276
+ # If a string is supplied, it is treated as an identity pattern.
277
+ #
278
+ # @example
279
+ # pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second')
280
+ # pattern === '/foo/bar' # => false
281
+ # pattern === '/fox/bar' # => true
282
+ # pattern === '/foo' # => false
283
+ #
284
+ # @param [Mustermann::Pattern, String] other the other pattern
285
+ # @return [Mustermann::Pattern] a composite pattern
286
+ def |(other)
287
+ Mustermann.new(self, other, :operator => :|, :type => :identity)
288
+ end
289
+
290
+ def &(other)
291
+ Mustermann.new(self, other, :operator => :&, :type => :identity)
292
+ end
293
+
294
+ def ^(other)
295
+ Mustermann.new(self, other, :operator => :^, :type => :identity)
296
+ end
297
+
298
+ # @example
299
+ # pattern = Mustermann.new('/:a/:b')
300
+ # strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
301
+ # strings.detect(&pattern) # => "/foo/bar"
302
+ #
303
+ # @return [Proc] proc wrapping {#===}
304
+ def to_proc
305
+ @to_proc ||= method(:===).to_proc
306
+ end
307
+
147
308
  # @!visibility private
148
309
  # @return [Boolean]
149
310
  # @see Object#respond_to?
150
311
  def respond_to?(method, *args)
151
- method.to_s == 'expand' ? false : super
312
+ return super unless PATTERN_METHODS.include? method
313
+ respond_to_special?(method)
314
+ end
315
+
316
+ # @!visibility private
317
+ # @return [Boolean]
318
+ # @see #respond_to?
319
+ def respond_to_special?(method)
320
+ method(method).owner != Mustermann::Pattern
152
321
  end
153
322
 
154
323
  # @!visibility private
@@ -156,6 +325,12 @@ module Mustermann
156
325
  "#<%p:%p>" % [self.class, @string]
157
326
  end
158
327
 
328
+ # @!visibility private
329
+ def simple_inspect
330
+ type = self.class.name[/[^:]+$/].downcase
331
+ "%s:%p" % [type, @string]
332
+ end
333
+
159
334
  # @!visibility private
160
335
  def map_param(key, value)
161
336
  unescape(value, true)
@@ -164,8 +339,7 @@ module Mustermann
164
339
  # @!visibility private
165
340
  def unescape(string, decode = @uri_decode)
166
341
  return string unless decode and string
167
- @uri ||= URI::Parser.new
168
- @uri.unescape(string)
342
+ @@uri.unescape(string)
169
343
  end
170
344
 
171
345
  # @!visibility private
@@ -176,7 +350,7 @@ module Mustermann
176
350
  ALWAYS_ARRAY.include? key
177
351
  end
178
352
 
179
- private :unescape, :map_param
353
+ private :unescape, :map_param, :respond_to_special?
180
354
  #private_constant :ALWAYS_ARRAY
181
355
  end
182
356
  end
@@ -0,0 +1,49 @@
1
+ require 'set'
2
+ require 'thread'
3
+ require 'mustermann'
4
+
5
+ module Mustermann
6
+ # A simple, persistent cache for creating repositories.
7
+ #
8
+ # @example
9
+ # require 'mustermann/pattern_cache'
10
+ # cache = Mustermann::PatternCache.new
11
+ #
12
+ # # use this instead of Mustermann.new
13
+ # pattern = cache.create_pattern("/:name", type: :rails)
14
+ #
15
+ # @note
16
+ # {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have
17
+ # not yet been garbage collected. You only need an extra cache if you do not keep a reference to
18
+ # the patterns around.
19
+ #
20
+ # @api private
21
+ class PatternCache
22
+ # @param [Hash] pattern_options default options used for {#create_pattern}
23
+ def initialize(pattern_options = {})
24
+ @cached = Set.new
25
+ @mutex = Mutex.new
26
+ @pattern_options = pattern_options
27
+ end
28
+
29
+ # @param (see Mustermann.new)
30
+ # @return (see Mustermann.new)
31
+ # @raise (see Mustermann.new)
32
+ # @see Mustermann.new
33
+ def create_pattern(string, pattern_options = {})
34
+ pattern = Mustermann.new(string, @pattern_options.merge(pattern_options))
35
+ @mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern
36
+ pattern
37
+ end
38
+
39
+ # Removes all pattern instances from the cache.
40
+ def clear
41
+ @mutex.synchronize { @cached.clear }
42
+ end
43
+
44
+ # @return [Integer] number of currently cached patterns
45
+ def size
46
+ @mutex.synchronize { @cached.size }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ require 'mustermann/ast/pattern'
2
+
3
+ module Mustermann
4
+ # Pyramid style pattern implementation.
5
+ #
6
+ # @example
7
+ # Mustermann.new('/<foo>', type: :pryamid) === '/bar' # => true
8
+ #
9
+ # @see Mustermann::Pattern
10
+ # @see file:README.md#pryamid Syntax description in the README
11
+ class Pyramid < AST::Pattern
12
+ on(nil, ?}) { |c| unexpected(c) }
13
+
14
+ on(?{) do |char|
15
+ name = expect(/\w+/, char: char)
16
+ constraint = read_brackets(?{, ?}) if scan(?:)
17
+ expect(?}) unless constraint
18
+ node(:capture, name, constraint: constraint)
19
+ end
20
+
21
+ on(?*) do |char|
22
+ node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) })
23
+ end
24
+ end
25
+ end
@@ -15,7 +15,24 @@ module Mustermann
15
15
  # @see (see Mustermann::Pattern#initialize)
16
16
  def initialize(string, options = {})
17
17
  super
18
- @regexp = compile(options)
18
+ regexp = compile(options)
19
+ @peek_regexp = /\A(#{regexp})/
20
+ @regexp = /\A#{regexp}\Z/
21
+ end
22
+
23
+ # @param (see Mustermann::Pattern#peek_size)
24
+ # @return (see Mustermann::Pattern#peek_size)
25
+ # @see (see Mustermann::Pattern#peek_size)
26
+ def peek_size(string)
27
+ return unless match = peek_match(string)
28
+ match.to_s.size
29
+ end
30
+
31
+ # @param (see Mustermann::Pattern#peek_match)
32
+ # @return (see Mustermann::Pattern#peek_match)
33
+ # @see (see Mustermann::Pattern#peek_match)
34
+ def peek_match(string)
35
+ @peek_regexp.match(string)
19
36
  end
20
37
 
21
38
  extend Forwardable
@@ -18,7 +18,7 @@ module Mustermann
18
18
  end
19
19
 
20
20
  def compile(options = {})
21
- /\A#{@string}\Z/
21
+ /#{@string}/
22
22
  end
23
23
 
24
24
  private :compile
@@ -25,5 +25,13 @@ module Mustermann
25
25
  def ===(string)
26
26
  File.fnmatch? @string, unescape(string), @flags
27
27
  end
28
+
29
+ # @param (see Mustermann::Pattern#peek_size)
30
+ # @return (see Mustermann::Pattern#peek_size)
31
+ # @see (see Mustermann::Pattern#peek_size)
32
+ def peek_size(string)
33
+ @peek_string ||= @string + "{**,/**,/**/*}"
34
+ super if File.fnmatch? @peek_string, unescape(string), @flags
35
+ end
28
36
  end
29
37
  end
@@ -19,7 +19,7 @@ module Mustermann
19
19
  pattern.gsub!(/((:\w+)|\*)/) do |match|
20
20
  match == "*" ? "(?<splat>.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})"
21
21
  end
22
- /\A#{Regexp.new(pattern)}\Z/
22
+ Regexp.new(pattern)
23
23
  rescue SyntaxError, RegexpError => error
24
24
  type = error.message["invalid group name"] ? CompileError : ParseError
25
25
  raise type, error.message, error.backtrace
@@ -26,5 +26,10 @@ module Mustermann
26
26
  def [](*args)
27
27
  captures[*args]
28
28
  end
29
+
30
+ # @return [String] string representation
31
+ def inspect
32
+ "#<%p %p>" % [self.class, @string]
33
+ end
29
34
  end
30
35
  end