mustermann 0.3.1 → 0.4.0

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 (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