mustermann 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +429 -672
  3. data/lib/mustermann.rb +95 -20
  4. data/lib/mustermann/ast/boundaries.rb +44 -0
  5. data/lib/mustermann/ast/compiler.rb +13 -7
  6. data/lib/mustermann/ast/expander.rb +22 -12
  7. data/lib/mustermann/ast/node.rb +69 -5
  8. data/lib/mustermann/ast/param_scanner.rb +20 -0
  9. data/lib/mustermann/ast/parser.rb +138 -19
  10. data/lib/mustermann/ast/pattern.rb +59 -7
  11. data/lib/mustermann/ast/template_generator.rb +28 -0
  12. data/lib/mustermann/ast/transformer.rb +2 -2
  13. data/lib/mustermann/ast/translator.rb +20 -0
  14. data/lib/mustermann/ast/validation.rb +4 -3
  15. data/lib/mustermann/composite.rb +101 -0
  16. data/lib/mustermann/expander.rb +2 -2
  17. data/lib/mustermann/identity.rb +56 -0
  18. data/lib/mustermann/pattern.rb +185 -10
  19. data/lib/mustermann/pattern_cache.rb +49 -0
  20. data/lib/mustermann/regexp.rb +1 -0
  21. data/lib/mustermann/regexp_based.rb +18 -1
  22. data/lib/mustermann/regular.rb +4 -1
  23. data/lib/mustermann/simple_match.rb +5 -0
  24. data/lib/mustermann/sinatra.rb +22 -5
  25. data/lib/mustermann/to_pattern.rb +11 -6
  26. data/lib/mustermann/version.rb +1 -1
  27. data/mustermann.gemspec +1 -14
  28. data/spec/ast_spec.rb +14 -0
  29. data/spec/composite_spec.rb +147 -0
  30. data/spec/expander_spec.rb +15 -0
  31. data/spec/identity_spec.rb +44 -0
  32. data/spec/mustermann_spec.rb +17 -2
  33. data/spec/pattern_spec.rb +7 -3
  34. data/spec/regular_spec.rb +25 -0
  35. data/spec/sinatra_spec.rb +184 -9
  36. data/spec/to_pattern_spec.rb +49 -0
  37. metadata +15 -180
  38. data/.gitignore +0 -18
  39. data/.rspec +0 -2
  40. data/.travis.yml +0 -4
  41. data/.yardopts +0 -1
  42. data/Gemfile +0 -2
  43. data/LICENSE +0 -22
  44. data/Rakefile +0 -6
  45. data/internals.md +0 -64
  46. data/lib/mustermann/ast/tree_renderer.rb +0 -29
  47. data/lib/mustermann/rails.rb +0 -17
  48. data/lib/mustermann/shell.rb +0 -29
  49. data/lib/mustermann/simple.rb +0 -35
  50. data/lib/mustermann/template.rb +0 -47
  51. data/spec/rails_spec.rb +0 -521
  52. data/spec/shell_spec.rb +0 -108
  53. data/spec/simple_spec.rb +0 -236
  54. data/spec/support.rb +0 -5
  55. data/spec/support/coverage.rb +0 -16
  56. data/spec/support/env.rb +0 -16
  57. data/spec/support/expand_matcher.rb +0 -27
  58. data/spec/support/match_matcher.rb +0 -39
  59. data/spec/support/pattern.rb +0 -39
  60. data/spec/template_spec.rb +0 -814
@@ -0,0 +1,101 @@
1
+ module Mustermann
2
+ # Class for pattern objects composed of multiple patterns using binary logic.
3
+ # @see Mustermann::Pattern#&
4
+ # @see Mustermann::Pattern#|
5
+ # @see Mustermann::Pattern#^
6
+ class Composite < Pattern
7
+ attr_reader :patterns, :operator
8
+ supported_options :operator, :type
9
+
10
+ # @see Mustermann::Pattern.supported?
11
+ def self.supported?(option, **options)
12
+ return true if super
13
+ options[:type] and Mustermann[options[:type]].supported?(option, **options)
14
+ end
15
+
16
+ # @return [Mustermann::Pattern] a new composite pattern
17
+ def self.new(*patterns, **options)
18
+ patterns = patterns.flatten
19
+ case patterns.size
20
+ when 0 then raise ArgumentError, 'cannot create empty composite pattern'
21
+ when 1 then patterns.first
22
+ else super(patterns, **options)
23
+ end
24
+ end
25
+
26
+ def initialize(patterns, operator: :|, **options)
27
+ @operator = operator.to_sym
28
+ @patterns = patterns.flat_map { |p| patterns_from(p, **options) }
29
+ end
30
+
31
+ # @see Mustermann::Pattern#==
32
+ def ==(pattern)
33
+ patterns == patterns_from(pattern)
34
+ end
35
+
36
+ # @see Mustermann::Pattern#===
37
+ def ===(string)
38
+ patterns.map { |p| p === string }.inject(operator)
39
+ end
40
+
41
+ # @see Mustermann::Pattern#params
42
+ def params(string)
43
+ with_matching(string, :params)
44
+ end
45
+
46
+ # @see Mustermann::Pattern#match
47
+ def match(string)
48
+ with_matching(string, :match)
49
+ end
50
+
51
+ # @!visibility private
52
+ def respond_to_special?(method)
53
+ return false unless operator == :|
54
+ patterns.all? { |p| p.respond_to?(method) }
55
+ end
56
+
57
+ # (see Mustermann::Pattern#expand)
58
+ def expand(behavior = nil, values = {})
59
+ raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
60
+ @expander ||= Mustermann::Expander.new(*patterns)
61
+ @expander.expand(behavior, values)
62
+ end
63
+
64
+ # (see Mustermann::Pattern#expand)
65
+ def to_templates
66
+ raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
67
+ patterns.flat_map(&:to_templates).uniq
68
+ end
69
+
70
+ # @return [String] the string representation of the pattern
71
+ def to_s
72
+ simple_inspect
73
+ end
74
+
75
+ # @!visibility private
76
+ def inspect
77
+ "#<%p:%s>" % [self.class, simple_inspect]
78
+ end
79
+
80
+ # @!visibility private
81
+ def simple_inspect
82
+ pattern_strings = patterns.map { |p| p.simple_inspect }
83
+ "(#{pattern_strings.join(" #{operator} ")})"
84
+ end
85
+
86
+ # @!visibility private
87
+ def with_matching(string, method)
88
+ return unless self === string
89
+ pattern = patterns.detect { |p| p === string }
90
+ pattern.public_send(method, string) if pattern
91
+ end
92
+
93
+ # @!visibility private
94
+ def patterns_from(pattern, options = nil)
95
+ return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator
96
+ [options ? Mustermann.new(pattern, **options) : pattern]
97
+ end
98
+
99
+ private :with_matching, :patterns_from
100
+ end
101
+ end
@@ -41,7 +41,7 @@ module Mustermann
41
41
  # @return [Mustermann::Expander] the expander
42
42
  def add(*patterns)
43
43
  patterns.each do |pattern|
44
- pattern = Mustermann.new(pattern.to_str, **@options) if pattern.respond_to? :to_str
44
+ pattern = Mustermann.new(pattern, **@options)
45
45
  raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast
46
46
  @api_expander.add(pattern.to_ast)
47
47
  @patterns << pattern
@@ -137,7 +137,7 @@ module Mustermann
137
137
  # @return [String] expanded string
138
138
  # @raise [NotImplementedError] raised if expand is not supported.
139
139
  # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
140
- def expand(behavior = nil, **values)
140
+ def expand(behavior = nil, values = {})
141
141
  behavior, values = nil, behavior if behavior.is_a? Hash
142
142
  values = map_values(values)
143
143
 
@@ -1,4 +1,6 @@
1
+ require 'mustermann'
1
2
  require 'mustermann/pattern'
3
+ require 'mustermann/ast/node'
2
4
 
3
5
  module Mustermann
4
6
  # Matches strings that are identical to the pattern.
@@ -9,11 +11,65 @@ module Mustermann
9
11
  # @see Mustermann::Pattern
10
12
  # @see file:README.md#identity Syntax description in the README
11
13
  class Identity < Pattern
14
+ register :identity
15
+
12
16
  # @param (see Mustermann::Pattern#===)
13
17
  # @return (see Mustermann::Pattern#===)
14
18
  # @see (see Mustermann::Pattern#===)
15
19
  def ===(string)
16
20
  unescape(string) == @string
17
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 unescape(string).start_with? @string
28
+ return @string.size if string.start_with? @string # optimization
29
+ @string.each_char.with_index.inject(0) do |count, (char, index)|
30
+ char_size = 1
31
+ escaped = @@uri.escape(char, /./)
32
+ char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase
33
+ count + char_size
34
+ end
35
+ end
36
+
37
+ # URI templates support generating templates (the logic is quite complex, though).
38
+ #
39
+ # @example (see Mustermann::Pattern#to_templates)
40
+ # @param (see Mustermann::Pattern#to_templates)
41
+ # @return (see Mustermann::Pattern#to_templates)
42
+ # @see Mustermann::Pattern#to_templates
43
+ def to_templates
44
+ [@@uri.escape(to_s)]
45
+ end
46
+
47
+ # Generates an AST so it's compatible with {Mustermann::AST::Pattern}.
48
+ # Not used internally by {Mustermann::Identity}.
49
+ # @!visibility private
50
+ def to_ast
51
+ payload = @string.each_char.with_index.map { |c, i| AST::Node[c == ?/ ? :separator : :char].new(c, start: i, stop: i+1) }
52
+ AST::Node[:root].new(payload, pattern: @string, start: 0, stop: @string.length)
53
+ end
54
+
55
+ # Identity patterns support expanding.
56
+ #
57
+ # This implementation does not use {Mustermann::Expander} internally to save memory and
58
+ # compilation time.
59
+ #
60
+ # @example (see Mustermann::Pattern#expand)
61
+ # @param (see Mustermann::Pattern#expand)
62
+ # @return (see Mustermann::Pattern#expand)
63
+ # @raise (see Mustermann::Pattern#expand)
64
+ # @see Mustermann::Pattern#expand
65
+ # @see Mustermann::Expander
66
+ def expand(behavior = nil, values = {})
67
+ return to_s if values.empty? or behavior == :ignore
68
+ raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
69
+ raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
70
+ params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w]/) }
71
+ separator = @string.include?(??) ? ?& : ??
72
+ @string + separator + params.join(?&)
73
+ end
18
74
  end
19
75
  end
@@ -8,6 +8,7 @@ module Mustermann
8
8
  # @abstract
9
9
  class Pattern
10
10
  include Mustermann
11
+ @@uri ||= URI::Parser.new
11
12
 
12
13
  # List of supported options.
13
14
  #
@@ -26,9 +27,16 @@ module Mustermann
26
27
  options
27
28
  end
28
29
 
30
+ # Registers the pattern with Mustermann.
31
+ # @see Mustermann.register
32
+ # @!visibility private
33
+ def self.register(*names)
34
+ names.each { |name| Mustermann.register(name, self) }
35
+ end
36
+
29
37
  # @param [Symbol] option The option to check.
30
38
  # @return [Boolean] Whether or not option is supported.
31
- def self.supported?(option)
39
+ def self.supported?(option, **options)
32
40
  supported_options.include? option
33
41
  end
34
42
 
@@ -40,7 +48,7 @@ module Mustermann
40
48
  # @see #initialize
41
49
  def self.new(string, ignore_unknown_options: false, **options)
42
50
  unless ignore_unknown_options
43
- unsupported = options.keys.detect { |key| not supported?(key) }
51
+ unsupported = options.keys.detect { |key| not supported?(key, **options) }
44
52
  raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported
45
53
  end
46
54
 
@@ -90,13 +98,73 @@ module Mustermann
90
98
  raise NotImplementedError, 'subclass responsibility'
91
99
  end
92
100
 
93
- # @return [Array<String>] capture names.
101
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
102
+ # Will return the count of the matching characters if it matches.
103
+ #
104
+ # @example
105
+ # pattern = Mustermann.new('/:name')
106
+ # pattern.size("/Frank/Sinatra") # => 6
107
+ #
108
+ # @param [String] string The string to match against
109
+ # @return [Integer, nil] the number of characters that match
110
+ def peek_size(string)
111
+ # this is a very naive, unperformant implementation
112
+ string.size.downto(0).detect { |s| self === string[0, s] }
113
+ end
114
+
115
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
116
+ # Will return the substring if it matches.
117
+ #
118
+ # @example
119
+ # pattern = Mustermann.new('/:name')
120
+ # pattern.peek("/Frank/Sinatra") # => "/Frank"
121
+ #
122
+ # @param [String] string The string to match against
123
+ # @return [String, nil] matched subsctring
124
+ def peek(string)
125
+ size = peek_size(string)
126
+ string[0, size] if size
127
+ end
128
+
129
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
130
+ # Will return a MatchData or similar instance for the matched substring.
131
+ #
132
+ # @example
133
+ # pattern = Mustermann.new('/:name')
134
+ # pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
135
+ #
136
+ # @param [String] string The string to match against
137
+ # @return [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches.
138
+ # @see #peek_params
139
+ def peek_match(string)
140
+ matched = peek(string)
141
+ match(matched) if matched
142
+ end
143
+
144
+ # Tries to match the pattern against the beginning of the string (as opposed to the full string).
145
+ # Will return a two element Array with the params parsed from the substring as first entry and the length of
146
+ # the substring as second.
147
+ #
148
+ # @example
149
+ # pattern = Mustermann.new('/:name')
150
+ # params, _ = pattern.peek_params("/Frank/Sinatra")
151
+ #
152
+ # puts "Hello, #{params['name']}!" # Hello, Frank!
153
+ #
154
+ # @param [String] string The string to match against
155
+ # @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
156
+ def peek_params(string)
157
+ match = peek_match(string)
158
+ [params(captures: match), match.to_s.size] if match
159
+ end
160
+
161
+ # @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
94
162
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
95
163
  def named_captures
96
164
  {}
97
165
  end
98
166
 
99
- # @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
167
+ # @return [Array<String>] capture names.
100
168
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
101
169
  def names
102
170
  []
@@ -129,20 +197,122 @@ module Mustermann
129
197
  # warn "does not support expanding"
130
198
  # end
131
199
  #
132
- # @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)
133
207
  # @return [String] expanded string
134
208
  # @raise [NotImplementedError] raised if expand is not supported.
135
209
  # @raise [Mustermann::ExpandError] raised if a value is missing or unknown
136
210
  # @see Mustermann::Expander
137
- def expand(**values)
211
+ def expand(behavior = nil, values = {})
138
212
  raise NotImplementedError, "expanding not supported by #{self.class}"
139
213
  end
140
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 almost all patterns (notable execptions are
238
+ # {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}).
239
+ # Union {Mustermann::Composite} patterns (with the | operator) support template generation
240
+ # if all patterns they are composed 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: __callee__, type: :identity)
288
+ end
289
+
290
+ alias_method :&, :|
291
+ alias_method :^, :|
292
+
293
+ # @example
294
+ # pattern = Mustermann.new('/:a/:b')
295
+ # strings = ["foo/bar", "/foo/bar", "/foo/bar/"]
296
+ # strings.detect(&pattern) # => "/foo/bar"
297
+ #
298
+ # @return [Proc] proc wrapping {#===}
299
+ def to_proc
300
+ @to_proc ||= method(:===).to_proc
301
+ end
302
+
141
303
  # @!visibility private
142
304
  # @return [Boolean]
143
305
  # @see Object#respond_to?
144
306
  def respond_to?(method, *args)
145
- method.to_s == 'expand' ? false : super
307
+ return super unless %i[expand to_templates].include? method
308
+ respond_to_special?(method)
309
+ end
310
+
311
+ # @!visibility private
312
+ # @return [Boolean]
313
+ # @see #respond_to?
314
+ def respond_to_special?(method)
315
+ method(method).owner != Mustermann::Pattern
146
316
  end
147
317
 
148
318
  # @!visibility private
@@ -150,6 +320,12 @@ module Mustermann
150
320
  "#<%p:%p>" % [self.class, @string]
151
321
  end
152
322
 
323
+ # @!visibility private
324
+ def simple_inspect
325
+ type = self.class.name[/[^:]+$/].downcase
326
+ "%s:%p" % [type, @string]
327
+ end
328
+
153
329
  # @!visibility private
154
330
  def map_param(key, value)
155
331
  unescape(value, true)
@@ -158,8 +334,7 @@ module Mustermann
158
334
  # @!visibility private
159
335
  def unescape(string, decode = @uri_decode)
160
336
  return string unless decode and string
161
- @uri ||= URI::Parser.new
162
- @uri.unescape(string)
337
+ @@uri.unescape(string)
163
338
  end
164
339
 
165
340
  # @!visibility private
@@ -170,7 +345,7 @@ module Mustermann
170
345
  ALWAYS_ARRAY.include? key
171
346
  end
172
347
 
173
- private :unescape, :map_param
348
+ private :unescape, :map_param, :respond_to_special?
174
349
  private_constant :ALWAYS_ARRAY
175
350
  end
176
351
  end