mustermann 3.0.4 → 4.0.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/LICENSE +1 -2
  3. data/README.md +238 -261
  4. data/lib/mustermann/ast/compiler.rb +128 -30
  5. data/lib/mustermann/ast/converters.rb +41 -0
  6. data/lib/mustermann/ast/expander.rb +4 -5
  7. data/lib/mustermann/ast/fast_pattern.rb +122 -0
  8. data/lib/mustermann/ast/param_scanner.rb +38 -6
  9. data/lib/mustermann/ast/parser.rb +2 -3
  10. data/lib/mustermann/ast/pattern.rb +26 -4
  11. data/lib/mustermann/ast/transformer.rb +7 -0
  12. data/lib/mustermann/ast/translator.rb +11 -7
  13. data/lib/mustermann/composite.rb +25 -6
  14. data/lib/mustermann/concat.rb +16 -5
  15. data/lib/mustermann/error.rb +1 -0
  16. data/lib/mustermann/expander.rb +26 -4
  17. data/lib/mustermann/hybrid.rb +50 -0
  18. data/lib/mustermann/match.rb +155 -0
  19. data/lib/mustermann/pattern.rb +27 -32
  20. data/lib/mustermann/rails.rb +63 -0
  21. data/lib/mustermann/regexp_based.rb +70 -9
  22. data/lib/mustermann/router.rb +104 -0
  23. data/lib/mustermann/set/cache.rb +48 -0
  24. data/lib/mustermann/set/linear.rb +32 -0
  25. data/lib/mustermann/set/match.rb +23 -0
  26. data/lib/mustermann/set/strict_order.rb +29 -0
  27. data/lib/mustermann/set/trie.rb +270 -0
  28. data/lib/mustermann/set.rb +445 -0
  29. data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
  30. data/lib/mustermann/sinatra/try_convert.rb +49 -11
  31. data/lib/mustermann/sinatra.rb +35 -11
  32. data/lib/mustermann/version.rb +1 -1
  33. data/lib/mustermann/versions.rb +47 -0
  34. data/lib/mustermann.rb +0 -15
  35. metadata +31 -45
  36. data/bench/capturing.rb +0 -57
  37. data/bench/regexp.rb +0 -21
  38. data/bench/simple_vs_sinatra.rb +0 -23
  39. data/bench/template_vs_addressable.rb +0 -26
  40. data/bench/uri_parser_object.rb +0 -16
  41. data/lib/mustermann/extension.rb +0 -3
  42. data/lib/mustermann/mapper.rb +0 -91
  43. data/lib/mustermann/pattern_cache.rb +0 -50
  44. data/lib/mustermann/simple_match.rb +0 -49
  45. data/lib/mustermann/to_pattern.rb +0 -51
  46. data/mustermann.gemspec +0 -18
  47. data/spec/ast_spec.rb +0 -15
  48. data/spec/composite_spec.rb +0 -163
  49. data/spec/concat_spec.rb +0 -127
  50. data/spec/equality_map_spec.rb +0 -42
  51. data/spec/expander_spec.rb +0 -123
  52. data/spec/identity_spec.rb +0 -127
  53. data/spec/mapper_spec.rb +0 -77
  54. data/spec/mustermann_spec.rb +0 -81
  55. data/spec/pattern_spec.rb +0 -54
  56. data/spec/regexp_based_spec.rb +0 -9
  57. data/spec/regular_spec.rb +0 -119
  58. data/spec/simple_match_spec.rb +0 -11
  59. data/spec/sinatra_spec.rb +0 -836
  60. data/spec/to_pattern_spec.rb +0 -70
@@ -0,0 +1,445 @@
1
+ # frozen_string_literal: true
2
+ require 'mustermann'
3
+ require 'mustermann/expander'
4
+ require 'mustermann/set/cache'
5
+ require 'mustermann/set/linear'
6
+ require 'mustermann/set/strict_order'
7
+ require 'mustermann/set/trie'
8
+
9
+ module Mustermann
10
+ # A collection of patterns that can be matched against strings efficiently.
11
+ #
12
+ # Each pattern in the set may be associated with one or more arbitrary values,
13
+ # such as handler objects or route actions. A single {#match} call returns a
14
+ # {Set::Match} that provides both the captured parameters and the associated
15
+ # value for the matched pattern. When the set contains many patterns, an
16
+ # internal trie (prefix tree) is used to dispatch requests in sub-linear time.
17
+ #
18
+ # @example Building a routing table
19
+ # require 'mustermann/set'
20
+ #
21
+ # set = Mustermann::Set.new
22
+ # set.add('/users/:id', :users_show)
23
+ # set.add('/posts/:id', :posts_show)
24
+ #
25
+ # m = set.match('/users/42')
26
+ # m.value # => :users_show
27
+ # m.params['id'] # => '42'
28
+ #
29
+ # @example Constructor shorthand with a hash
30
+ # set = Mustermann::Set.new('/users/:id' => :users_show, '/posts/:id' => :posts_show)
31
+ #
32
+ # @example Block syntax
33
+ # set = Mustermann::Set.new do |s|
34
+ # s.add('/users/:id', :users_show)
35
+ # s.add('/posts/:id', :posts_show)
36
+ # end
37
+ #
38
+ # @note Adding patterns via {#add}, {#update}, or {#[]=} is not thread-safe, but matching and expanding is.
39
+ class Set
40
+ # Pattern options forwarded to {Mustermann.new} when patterns are created from strings.
41
+ # @return [Hash]
42
+ attr_reader :options
43
+
44
+ # Creates a new set, optionally pre-populated with patterns.
45
+ #
46
+ # Patterns can be supplied as a Hash (pattern → value), a plain String or
47
+ # Pattern, an Array of any of these, or an existing {Set}. The same forms
48
+ # are accepted by {#update} and {#add}.
49
+ #
50
+ # @example Empty set
51
+ # Mustermann::Set.new
52
+ #
53
+ # @example Pre-populated from a hash
54
+ # Mustermann::Set.new('/users/:id' => :users, '/posts/:id' => :posts)
55
+ #
56
+ # @example Imperative block
57
+ # Mustermann::Set.new do |s|
58
+ # s.add('/users/:id', :users)
59
+ # end
60
+ #
61
+ # @example Zero-argument block returning a mapping hash
62
+ # Mustermann::Set.new { { '/users/:id' => :users } }
63
+ #
64
+ # @param mapping [Array] initial patterns or mappings to add
65
+ #
66
+ # @param additional_values [:raise, :ignore, :append] behavior when extra keys are passed to {#expand}.
67
+ # Defaults to +:raise+
68
+ #
69
+ # @param use_trie [Boolean, Integer]
70
+ # whether to use a trie for matching
71
+ # If an Integer is given, it is the number of patterns at which to switch from linear to trie matching.
72
+ # Defaults to 50
73
+ #
74
+ # @param use_cache [Boolean]
75
+ # whether to cache matches not yet garbage collected. Defaults to +true+
76
+ #
77
+ # @param strict_order [Boolean]
78
+ # whether to match patterns in strict insertion order rather than trie order. Defaults to +false+.
79
+ # See {#use_strict_order?} for details
80
+ #
81
+ # @param options [Hash]
82
+ # pattern options forwarded to {Mustermann.new} (e.g. +type: :rails+)
83
+ #
84
+ # @raise [ArgumentError] if +additional_values+ is not a recognized behavior symbol
85
+ def initialize(*mapping, additional_values: :raise, use_trie: 50, use_cache: true, strict_order: false, **options, &block)
86
+ raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless Expander::ADDITIONAL_VALUES.include? additional_values
87
+ raise ArgumentError, "Illegal value %p for use_trie" % use_trie unless [true, false].include?(use_trie) or use_trie.is_a? Integer
88
+
89
+ @use_trie = use_trie
90
+ @use_cache = use_cache
91
+ @matcher = nil
92
+ @mapping = {}
93
+ @reverse_mapping = {}
94
+ @options = {}
95
+ @expanders = {}
96
+ @additional_values = additional_values
97
+ @strict_order = strict_order
98
+
99
+ options.each do |key, value|
100
+ if key.is_a? Symbol
101
+ @options[key] = value
102
+ else
103
+ mapping << { key => value }
104
+ end
105
+ end
106
+
107
+ update(mapping)
108
+
109
+ block.arity == 0 ? update(yield) : yield(self) if block
110
+
111
+ optimize!
112
+ end
113
+
114
+ # A set can match patterns and values in loose or strict insertion order.
115
+ #
116
+ # You have the following guarantees without strict ordering:
117
+ # - Patterns with dynamic segments in the same position and equal static parts will always match in the order they were added.
118
+ # - Multiple values for the same pattern will retain their insertion order in regards to that pattern.
119
+ #
120
+ # Trade-offs without strict ordering:
121
+ # - Static segments may be favored over dynamic segments. If you want to guarantee this behavior, enable trie-mode proactively.
122
+ # - When a pattern has multiple values, these will follow each other directly when using {#match_all} or {#peek_match_all}.
123
+ #
124
+ # Strict ordering comes with both a performance overhead and marginally increased memory usage.
125
+ # How big the performance overhead is depends on the number of patterns that overlap in the strings they successfully match against.
126
+ # It does use Ruby's built-in sorting, which on MRI is based on quicksort. The memory overhead grows linear with the number
127
+ # of pattern and value combinations, but is generally small compared to the memory used by the patterns and values themselves.
128
+ #
129
+ # With strict ordering enabled, patterns and values are guaranteed to occur in insertion order.
130
+ #
131
+ # @example Without strict ordering, not using a trie
132
+ # set = Mustermann::Set.new(use_trie: false)
133
+ #
134
+ # set.add("/:path", :first)
135
+ # set.add("/static", :second)
136
+ # set.add("/:path", :third)
137
+ #
138
+ # set.match("/static").value # => :first
139
+ # set.match_all("/static").map(&:value) # => [:first, :third, :second]
140
+ #
141
+ # @example Without strict ordering, using a trie
142
+ # set = Mustermann::Set.new(use_trie: true)
143
+ #
144
+ # set.add("/:path", :first)
145
+ # set.add("/static", :second)
146
+ # set.add("/:path", :third)
147
+ #
148
+ # set.match("/static").value # => :second
149
+ # set.match_all("/static").map(&:value) # => [:second, :first, :third]
150
+ #
151
+ # @example With strict ordering
152
+ # set = Mustermann::Set.new(strict_order: true)
153
+ #
154
+ # set.add("/:path", :first)
155
+ # set.add("/static", :second)
156
+ # set.add("/:path", :third)
157
+ #
158
+ # set.match("/static").value # => :first
159
+ # set.match_all("/static").map(&:value) # => [:first, :second, :third]
160
+ #
161
+ # @return [Boolean] whether matching happens in strict pattern/value insertion order
162
+ def strict_order? = @strict_order
163
+
164
+ # @return [Boolean] whether caching is enabled
165
+ def use_cache? = @use_cache
166
+
167
+ # @return [Boolean] whether trie optimization is enabled
168
+ def use_trie? = @use_trie == true
169
+
170
+ # Adds a pattern to the set, optionally associated with one or more values.
171
+ #
172
+ # If the pattern is given as a String it will be compiled via {Mustermann.new}
173
+ # using the set's own options. The pattern must be AST-based (Sinatra, Rails,
174
+ # and similar types). Plain regexp patterns are not supported.
175
+ #
176
+ # Calling +add+ more than once for the same pattern appends additional values
177
+ # without creating duplicates.
178
+ #
179
+ # @example
180
+ # set.add('/users/:id', :users)
181
+ # set.add('/users/:id', :admin) # same pattern, second value
182
+ #
183
+ # @param pattern [String, Pattern] the pattern to add
184
+ # @param values [Array] zero or more values to associate with the pattern
185
+ # @return [self]
186
+ # @raise [ArgumentError] if the pattern is not AST-based, or if a reserved symbol is used as a value
187
+ def add(pattern, *values)
188
+ if pattern.is_a? Composite and pattern.operator == :|
189
+ pattern.patterns.each { |p| add(p, *values) }
190
+ return self
191
+ end
192
+
193
+ pattern = Mustermann.new(pattern, **options)
194
+ raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast
195
+
196
+ if @mapping.key? pattern
197
+ current = @mapping[pattern]
198
+ else
199
+ add_pattern(pattern)
200
+ current = @mapping[pattern] = []
201
+ end
202
+
203
+ values = [nil] if values.empty?
204
+
205
+ values.each do |value|
206
+ raise ArgumentError, "%p may not be used as a value" % value if Expander::ADDITIONAL_VALUES.include? value
207
+ raise ArgumentError, "the set itself may not be used as value" if value == self
208
+ next if current.include? value
209
+ current << value
210
+ @reverse_mapping[value] ||= []
211
+ @reverse_mapping[value] << pattern unless @reverse_mapping[value].include? pattern
212
+ @expanders[value]&.add(pattern)
213
+ @matcher.track(pattern, value) if strict_order?
214
+ end
215
+
216
+ self
217
+ end
218
+
219
+ # Adds a pattern associated with a value using hash-assignment syntax.
220
+ # @see #add
221
+ alias []= add
222
+
223
+ # Looks up a value by string or retrieves the first value for a known pattern object.
224
+ #
225
+ # When given a String, it is matched against the set and the associated value of the
226
+ # first matching pattern is returned. When given a {Pattern}, the first value
227
+ # registered for that exact pattern is returned without matching.
228
+ #
229
+ # @example String lookup
230
+ # set['/users/42'] # => :users_show (or nil)
231
+ #
232
+ # @example Pattern lookup
233
+ # pattern = Mustermann.new('/users/:id')
234
+ # set[pattern] # => :users_show (or nil)
235
+ #
236
+ # @param pattern_or_string [String, Pattern]
237
+ # @return [Object, nil] the associated value, or +nil+ if not found
238
+ # @raise [ArgumentError] for unsupported argument types
239
+ def [](pattern_or_string)
240
+ case pattern_or_string
241
+ when String then match(pattern_or_string)&.value
242
+ when Pattern then values_for_pattern(pattern_or_string)&.first
243
+ else raise ArgumentError, "unsupported pattern type #{pattern_or_string.class}"
244
+ end
245
+ end
246
+
247
+ # Matches the string against all patterns in the set and returns the first match.
248
+ #
249
+ # @param string [String] the string to match
250
+ # @return [Set::Match, nil] the first match, or +nil+ if none of the patterns match
251
+ def match(string) = @matcher&.match(string)
252
+
253
+ # Matches the beginning of the string against all patterns and returns the
254
+ # first prefix match. The unmatched remainder of the string is available via
255
+ # {Set::Match#post_match}.
256
+ #
257
+ # @param string [String]
258
+ # @return [Set::Match, nil] the first prefix match, or +nil+
259
+ def peek_match(string) = @matcher&.match(string, peek: true)
260
+
261
+ # Matches the string against all patterns and returns every match, one per
262
+ # (pattern, value) pair, in insertion order.
263
+ #
264
+ # @param string [String]
265
+ # @return [Array<Set::Match>] all matches, or an empty array if none
266
+ def match_all(string) = @matcher&.match(string, all: true)
267
+
268
+ # Matches the beginning of the string against all patterns and returns every
269
+ # prefix match, one per (pattern, value) pair. The unmatched remainder is
270
+ # available as {Set::Match#post_match} on each result.
271
+ #
272
+ # @param string [String]
273
+ # @return [Array<Set::Match>] all prefix matches, or an empty array if none
274
+ def peek_match_all(string) = @matcher&.match(string, all: true, peek: true)
275
+
276
+ # Returns a new set that includes all patterns from the receiver plus those
277
+ # from +mapping+. The receiver is not modified.
278
+ #
279
+ # @param mapping [Hash, String, Pattern, Array, Set] patterns to merge in
280
+ # @return [Set] a new set
281
+ def merge(mapping) = dup.update(mapping)
282
+
283
+ # @!visibility private
284
+ def initialize_copy(other)
285
+ @mapping = other.mapping.transform_values(&:dup)
286
+ @reverse_mapping = @mapping.each_with_object({}) do |(pattern, values), h|
287
+ values.each { |value| (h[value] ||= []) << pattern }
288
+ end
289
+ @expanders = {}
290
+ @matcher = nil
291
+ @mapping.each_key { |pattern| add_pattern(pattern) }
292
+ end
293
+
294
+ # Adds all patterns from +mapping+ to the set in place and returns +self+.
295
+ # Aliased as +merge!+.
296
+ #
297
+ # Accepts the same argument forms as {#initialize}: a Hash, a String, a
298
+ # {Pattern}, an Array, or another {Set}.
299
+ #
300
+ # @param mapping [Hash, String, Pattern, Array, Set]
301
+ # @return [self]
302
+ # @raise [ArgumentError] for unsupported mapping types
303
+ def update(mapping)
304
+ case mapping
305
+ when Set then mapping.mapping.each { |pattern, values| add(pattern, *values) }
306
+ when Hash then mapping.each { |k, v| add(k, v) }
307
+ when String, Pattern then add(mapping)
308
+ when Array then mapping.each { |item| update(item) }
309
+ else raise ArgumentError, "unsupported mapping type #{mapping.class}"
310
+ end
311
+ self
312
+ end
313
+
314
+ alias merge! update
315
+
316
+ # Returns all patterns that have been added to the set, in insertion order.
317
+ # @return [Array<Pattern>]
318
+ def patterns = @mapping.keys
319
+
320
+ # Returns an {Expander} that can generate strings from parameter hashes.
321
+ #
322
+ # When called without arguments (or with the set itself as the value) the
323
+ # expander covers all patterns in the set. Pass a specific value to get an
324
+ # expander limited to the patterns associated with that value.
325
+ #
326
+ # @param value [Object] restricts the expander to patterns associated with
327
+ # this value; defaults to the set itself (all patterns)
328
+ # @return [Mustermann::Expander]
329
+ def expander(value = self)
330
+ @expanders[value] ||= begin
331
+ patterns = value == self ? @mapping.keys : @reverse_mapping[value] || []
332
+ Mustermann::Expander.new(patterns, additional_values: @additional_values, **options)
333
+ end
334
+ end
335
+
336
+ # Generates a string from a parameter hash using the patterns in the set.
337
+ #
338
+ # When called with just a parameter hash, the first pattern that can be fully
339
+ # expanded with those keys is used. Pass a value as the first argument to
340
+ # restrict expansion to the patterns associated with that value. You may also
341
+ # pass an +additional_values+ behavior symbol (+:raise+, +:ignore+, or
342
+ # +:append+) as the first argument to override the set's default behavior for
343
+ # that call.
344
+ #
345
+ # @example Expand using any pattern
346
+ # set.expand(id: '5')
347
+ #
348
+ # @example Expand patterns for a specific value
349
+ # set.expand(:users, id: '5')
350
+ #
351
+ # @example Override additional_values behavior for one call
352
+ # set.expand(:ignore, id: '5', extra: 'ignored')
353
+ #
354
+ # @param value [Object, :raise, :ignore, :append] the value whose patterns
355
+ # should be used, or an additional_values behavior symbol; defaults to all
356
+ # patterns
357
+ # @param behavior [:raise, :ignore, :append, nil] how to handle extra keys;
358
+ # defaults to the set's +additional_values+ setting
359
+ # @param values [Hash, nil] the parameters to expand
360
+ # @return [String]
361
+ # @raise [Mustermann::ExpandError] if no pattern can be expanded with the given keys
362
+ def expand(value = self, behavior = nil, values = nil)
363
+ if Expander::ADDITIONAL_VALUES.include? value
364
+ if behavior.is_a? Hash
365
+ values = values ? values.merge(behavior) : behavior
366
+ behavior = nil
367
+ elsif behavior and behavior != value
368
+ raise ArgumentError, "behavior specified multiple times" if behavior
369
+ end
370
+ behavior = value
371
+ value = self
372
+ elsif value.is_a? Hash and behavior.nil? and values.nil?
373
+ values = value
374
+ value = self unless @reverse_mapping.key? values
375
+ end
376
+ expander(value).expand(behavior || @additional_values, values || {})
377
+ end
378
+
379
+ # @return [Boolean] whether the set contains any pattern associated with the given value
380
+ def has_value?(value) = @reverse_mapping[value]&.any?
381
+
382
+ # @!visibility private
383
+ def values_for_pattern(pattern) = @mapping[pattern] # :nodoc:
384
+
385
+ # Runs trie optimizations pro-actively and explicitly rather than at match time.
386
+ def optimize! = @matcher&.optimize!
387
+
388
+ # @!visibility private
389
+ def inspect # :nodoc:
390
+ mapping = @mapping.map do |pattern, values|
391
+ if values.size > 1 or values.first.is_a? Array
392
+ "%p => %p" % [pattern.to_s, values]
393
+ elsif values.first != nil
394
+ "%p => %p" % [pattern.to_s, values.first]
395
+ else
396
+ "%p" % pattern.to_s
397
+ end
398
+ end
399
+ "#<#{self.class.name}: #{mapping.join(", ")}>"
400
+ end
401
+
402
+ # @!visibility private
403
+ def pretty_print(q) # :nodoc:
404
+ q.text "#<#{self.class.name}"
405
+ q.group(1, "", ">") do
406
+ @mapping.each_with_index do |(pattern, values), index|
407
+ q.breakable(index == 0 ? " " : ", ")
408
+ q.pp(pattern.to_s)
409
+ if values.size > 1 or values.first.is_a? Array
410
+ q.text " => "
411
+ q.pp(values)
412
+ elsif values.first != nil
413
+ q.text " => "
414
+ q.pp(values.first)
415
+ end
416
+ end
417
+ end
418
+ end
419
+
420
+ protected
421
+
422
+ attr_reader :mapping
423
+
424
+ private
425
+
426
+ def add_pattern(pattern)
427
+ if @use_trie.is_a? Integer and @mapping.size >= @use_trie
428
+ @use_trie = true
429
+ @matcher = build_matcher
430
+ end
431
+
432
+ @matcher ||= build_matcher
433
+ @matcher.add(pattern)
434
+ @expanders[self]&.add(pattern)
435
+ end
436
+
437
+ def build_matcher
438
+ factory = use_trie? ? Trie : Linear
439
+ matcher = factory.new(self, @mapping.keys)
440
+ matcher = StrictOrder.new(matcher) if strict_order?
441
+ matcher = Cache.new(matcher) if use_cache?
442
+ matcher
443
+ end
444
+ end
445
+ end
@@ -2,7 +2,7 @@
2
2
  module Mustermann
3
3
  class Sinatra < AST::Pattern
4
4
  # Generates a string that can safely be concatenated with other strings
5
- # without chaning its semantics
5
+ # without changing its semantics
6
6
  # @see #safe_string
7
7
  # @!visibility private
8
8
  SafeRenderer = AST::Translator.create do
@@ -6,24 +6,34 @@ module Mustermann
6
6
  class TryConvert < AST::Translator
7
7
  # @return [Mustermann::Sinatra, nil]
8
8
  # @!visibility private
9
- def self.convert(input, **options)
10
- new(options).translate(input)
9
+ def self.convert(type, input, **options)
10
+ new(type, **options).translate(input)
11
11
  end
12
12
 
13
+ # Reserved variable names.
14
+ # @!visibility private
15
+ attr_reader :names
16
+
13
17
  # Expected options for the resulting pattern.
14
18
  # @!visibility private
15
19
  attr_reader :options
16
20
 
21
+ # Expected pattern type for the resulting pattern.
22
+ # @!visibility private
23
+ attr_reader :type
24
+
17
25
  # @!visibility private
18
- def initialize(options)
26
+ def initialize(type, names: [], **options)
27
+ @names = names
19
28
  @options = options
29
+ @type = type
20
30
  end
21
31
 
22
32
  # @return [Mustermann::Sinatra]
23
33
  # @!visibility private
24
- def new(input, escape = false)
34
+ def new(input, escape: false, **opts)
25
35
  input = Mustermann::Sinatra.escape(input) if escape
26
- Mustermann::Sinatra.new(input, **options)
36
+ type.new(input, **opts, **options, ignore_unknown_options: true)
27
37
  end
28
38
 
29
39
  # @return [true, false] whether or not expected pattern should have uri_decode option set
@@ -32,15 +42,43 @@ module Mustermann
32
42
  options.fetch(:uri_decode, true)
33
43
  end
34
44
 
35
- translate(Object) { nil }
36
- translate(String) { t.new(self, true) }
45
+ # @return [true, false] whether or not the given options are compatible with the expected options
46
+ # @!visibility private
47
+ def compatible_options?(other_options)
48
+ other_options.all? do |key, value|
49
+ case key
50
+ when :capture then compatible_capture_option?(value)
51
+ else value == options[key]
52
+ end
53
+ end
54
+ end
37
55
 
38
- translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode }
39
- translate(Sinatra) { node if options == t.options }
56
+ # @return [true, false] whether or not the given capture option is compatible with the expected capture option
57
+ # @!visibility private
58
+ def compatible_capture_option?(capture)
59
+ return true if names.empty?
60
+ case capture
61
+ when Hash then capture.all? { |n, o| !names.include?(n.to_s) and compatible_capture_option?(o) }
62
+ when Array then capture.all? { |o| compatible_capture_option?(o) }
63
+ else true
64
+ end
65
+ end
66
+
67
+ translate(Object) { nil }
68
+ translate(String) { t.new(self, escape: true) }
69
+ translate(Identity) { t.new(self, escape: true) if uri_decode == t.uri_decode }
70
+
71
+ translate(Sinatra) do
72
+ if node.class == t.type and t.options == options
73
+ node
74
+ elsif t.compatible_options? options
75
+ t.new(to_s, **options)
76
+ end
77
+ end
40
78
 
41
79
  translate AST::Pattern do
42
- next unless options == t.options
43
- t.new(SafeRenderer.translate(to_ast)) rescue nil
80
+ next unless t.compatible_options? options
81
+ t.new(SafeRenderer.translate(to_ast), **options) rescue nil
44
82
  end
45
83
  end
46
84
 
@@ -2,6 +2,7 @@
2
2
  require 'mustermann'
3
3
  require 'mustermann/identity'
4
4
  require 'mustermann/ast/pattern'
5
+ require 'mustermann/ast/fast_pattern'
5
6
  require 'mustermann/sinatra/parser'
6
7
  require 'mustermann/sinatra/safe_renderer'
7
8
  require 'mustermann/sinatra/try_convert'
@@ -15,6 +16,7 @@ module Mustermann
15
16
  # @see Mustermann::Pattern
16
17
  # @see file:README.md#sinatra Syntax description in the README
17
18
  class Sinatra < AST::Pattern
19
+ include AST::FastPattern
18
20
  include Concat::Native
19
21
  register :sinatra
20
22
 
@@ -35,13 +37,13 @@ module Mustermann
35
37
  # @return [Mustermann::Sinatra, nil] the converted pattern, if possible
36
38
  # @!visibility private
37
39
  def self.try_convert(input, **options)
38
- TryConvert.convert(input, **options)
40
+ TryConvert.convert(self, input, **options)
39
41
  end
40
42
 
41
43
  # Creates a pattern that matches any string matching either one of the patterns.
42
44
  # If a string is supplied, it is treated as a fully escaped Sinatra pattern.
43
45
  #
44
- # If the other pattern is also a Sintara pattern, it might join the two to a third
46
+ # If the other pattern is also a Sinatra pattern, it might join the two to a third
45
47
  # sinatra pattern instead of generating a composite for efficiency reasons.
46
48
  #
47
49
  # This only happens if the sinatra pattern behaves exactly the same as a composite
@@ -57,12 +59,12 @@ module Mustermann
57
59
  # @return [Mustermann::Pattern] a composite pattern
58
60
  # @see Mustermann::Pattern#|
59
61
  def |(other)
60
- return super unless converted = self.class.try_convert(other, **options)
61
- return super unless converted.names.empty? or names.empty?
62
- self.class.new(safe_string + "|" + converted.safe_string, **options)
62
+ return super unless converted = try_convert(other)
63
+ return super if converted.names.any? { |name| names.include?(name) }
64
+ self.class.new(safe_string + "|" + converted.safe_string, **converted.options)
63
65
  end
64
66
 
65
- # Generates a string represenation of the pattern that can safely be used for def interpolation
67
+ # Generates a string representation of the pattern that can safely be used for def interpolation
66
68
  # without changing its semantics.
67
69
  #
68
70
  # @example
@@ -70,19 +72,41 @@ module Mustermann
70
72
  # unsafe = Mustermann.new("/:name")
71
73
  #
72
74
  # Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" }
73
- # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" }
75
+ # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "foo" }
74
76
  #
75
- # @return [String] string representatin of the pattern
77
+ # @return [String] string representations of the pattern
76
78
  def safe_string
77
79
  @safe_string ||= SafeRenderer.translate(to_ast)
78
80
  end
79
81
 
80
82
  # @!visibility private
81
83
  def native_concat(other)
82
- return unless converted = self.class.try_convert(other, **options)
83
- safe_string + converted.safe_string
84
+ return unless converted = try_convert(other)
85
+ [safe_string + converted.safe_string, converted.options]
84
86
  end
85
87
 
86
- private :native_concat
88
+ # @!visibility private
89
+ def normalize_capture(pattern)
90
+ case capture = pattern.options[:capture]
91
+ when Hash then capture.slice(*pattern.names.map(&:to_sym))
92
+ when nil then {}
93
+ else pattern.names.to_h { |name| [name.to_sym, capture] }
94
+ end
95
+ end
96
+
97
+ # @!visibility private
98
+ def try_convert(other)
99
+ options = self.options
100
+
101
+ if other.is_a? Pattern and other.names.any?
102
+ capture = normalize_capture(other).merge(normalize_capture(self))
103
+ capture = nil if capture.empty?
104
+ options = options.merge(capture:)
105
+ end
106
+
107
+ self.class.try_convert(other, names:, **options)
108
+ end
109
+
110
+ private :native_concat, :normalize_capture, :try_convert
87
111
  end
88
112
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Mustermann
3
- VERSION ||= '3.0.4'
3
+ VERSION ||= '4.0.0'
4
4
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module Mustermann
3
+ # Mixin that adds support for multiple versions of the same type.
4
+ # @see Mustermann::Rails
5
+ # @!visibility private
6
+ module Versions
7
+ # Checks if class has mulitple versions available and picks one that matches the version option.
8
+ # @!visibility private
9
+ def new(*args, version: nil, **options)
10
+ return super(*args, **options) unless versions.any?
11
+ self[version].new(*args, **options)
12
+ end
13
+
14
+ # @return [Hash] version to subclass mapping.
15
+ # @!visibility private
16
+ def versions
17
+ @versions ||= {}
18
+ end
19
+
20
+ # Defines a new version.
21
+ # @!visibility private
22
+ def version(*list, inherit_from: nil, &block)
23
+ superclass = self[inherit_from] || self
24
+ subclass = Class.new(superclass, &block)
25
+ list.each { |v| versions[v] = subclass }
26
+ end
27
+
28
+ # Resolve a subclass for a given version string.
29
+ # @!visibility private
30
+ def [](version)
31
+ return versions.values.last unless version
32
+ detected = versions.detect { |v,_| version.start_with?(v) }
33
+ raise ArgumentError, 'unsupported version %p' % version unless detected
34
+ detected.last
35
+ end
36
+
37
+ # @!visibility private
38
+ def name
39
+ super || superclass.name
40
+ end
41
+
42
+ # @!visibility private
43
+ def inspect
44
+ name
45
+ end
46
+ end
47
+ end