mustermann19 0.3.1 → 0.3.1.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.
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