mustermann 3.0.4 → 4.0.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 (64) 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 +137 -29
  5. data/lib/mustermann/ast/converters.rb +41 -0
  6. data/lib/mustermann/ast/expander.rb +6 -7
  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 +29 -6
  11. data/lib/mustermann/ast/template_generator.rb +2 -2
  12. data/lib/mustermann/ast/transformer.rb +7 -0
  13. data/lib/mustermann/ast/translator.rb +14 -5
  14. data/lib/mustermann/ast/validation.rb +1 -1
  15. data/lib/mustermann/composite.rb +26 -7
  16. data/lib/mustermann/concat.rb +17 -6
  17. data/lib/mustermann/error.rb +1 -0
  18. data/lib/mustermann/expander.rb +28 -6
  19. data/lib/mustermann/hybrid.rb +50 -0
  20. data/lib/mustermann/identity.rb +1 -1
  21. data/lib/mustermann/match.rb +155 -0
  22. data/lib/mustermann/pattern.rb +30 -35
  23. data/lib/mustermann/rails.rb +63 -0
  24. data/lib/mustermann/regexp_based.rb +70 -9
  25. data/lib/mustermann/regular.rb +1 -1
  26. data/lib/mustermann/router.rb +104 -0
  27. data/lib/mustermann/set/cache.rb +47 -0
  28. data/lib/mustermann/set/linear.rb +32 -0
  29. data/lib/mustermann/set/match.rb +23 -0
  30. data/lib/mustermann/set/strict_order.rb +29 -0
  31. data/lib/mustermann/set/trie.rb +282 -0
  32. data/lib/mustermann/set.rb +447 -0
  33. data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
  34. data/lib/mustermann/sinatra/try_convert.rb +49 -11
  35. data/lib/mustermann/sinatra.rb +35 -11
  36. data/lib/mustermann/version.rb +1 -1
  37. data/lib/mustermann/versions.rb +47 -0
  38. data/lib/mustermann.rb +38 -12
  39. metadata +31 -45
  40. data/bench/capturing.rb +0 -57
  41. data/bench/regexp.rb +0 -21
  42. data/bench/simple_vs_sinatra.rb +0 -23
  43. data/bench/template_vs_addressable.rb +0 -26
  44. data/bench/uri_parser_object.rb +0 -16
  45. data/lib/mustermann/extension.rb +0 -3
  46. data/lib/mustermann/mapper.rb +0 -91
  47. data/lib/mustermann/pattern_cache.rb +0 -50
  48. data/lib/mustermann/simple_match.rb +0 -49
  49. data/lib/mustermann/to_pattern.rb +0 -51
  50. data/mustermann.gemspec +0 -18
  51. data/spec/ast_spec.rb +0 -15
  52. data/spec/composite_spec.rb +0 -163
  53. data/spec/concat_spec.rb +0 -127
  54. data/spec/equality_map_spec.rb +0 -42
  55. data/spec/expander_spec.rb +0 -123
  56. data/spec/identity_spec.rb +0 -127
  57. data/spec/mapper_spec.rb +0 -77
  58. data/spec/mustermann_spec.rb +0 -81
  59. data/spec/pattern_spec.rb +0 -54
  60. data/spec/regexp_based_spec.rb +0 -9
  61. data/spec/regular_spec.rb +0 -119
  62. data/spec/simple_match_spec.rb +0 -11
  63. data/spec/sinatra_spec.rb +0 -836
  64. data/spec/to_pattern_spec.rb +0 -70
@@ -0,0 +1,447 @@
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
+ @options = Mustermann.dedup(@options)
108
+
109
+ update(mapping)
110
+
111
+ block.arity == 0 ? update(yield) : yield(self) if block
112
+
113
+ optimize!
114
+ end
115
+
116
+ # A set can match patterns and values in loose or strict insertion order.
117
+ #
118
+ # You have the following guarantees without strict ordering:
119
+ # - Patterns with dynamic segments in the same position and equal static parts will always match in the order they were added.
120
+ # - Multiple values for the same pattern will retain their insertion order in regards to that pattern.
121
+ #
122
+ # Trade-offs without strict ordering:
123
+ # - Static segments may be favored over dynamic segments. If you want to guarantee this behavior, enable trie-mode proactively.
124
+ # - When a pattern has multiple values, these will follow each other directly when using {#match_all} or {#peek_match_all}.
125
+ #
126
+ # Strict ordering comes with both a performance overhead and marginally increased memory usage.
127
+ # How big the performance overhead is depends on the number of patterns that overlap in the strings they successfully match against.
128
+ # It does use Ruby's built-in sorting, which on MRI is based on quicksort. The memory overhead grows linear with the number
129
+ # of pattern and value combinations, but is generally small compared to the memory used by the patterns and values themselves.
130
+ #
131
+ # With strict ordering enabled, patterns and values are guaranteed to occur in insertion order.
132
+ #
133
+ # @example Without strict ordering, not using a trie
134
+ # set = Mustermann::Set.new(use_trie: false)
135
+ #
136
+ # set.add("/:path", :first)
137
+ # set.add("/static", :second)
138
+ # set.add("/:path", :third)
139
+ #
140
+ # set.match("/static").value # => :first
141
+ # set.match_all("/static").map(&:value) # => [:first, :third, :second]
142
+ #
143
+ # @example Without strict ordering, using a trie
144
+ # set = Mustermann::Set.new(use_trie: true)
145
+ #
146
+ # set.add("/:path", :first)
147
+ # set.add("/static", :second)
148
+ # set.add("/:path", :third)
149
+ #
150
+ # set.match("/static").value # => :second
151
+ # set.match_all("/static").map(&:value) # => [:second, :first, :third]
152
+ #
153
+ # @example With strict ordering
154
+ # set = Mustermann::Set.new(strict_order: true)
155
+ #
156
+ # set.add("/:path", :first)
157
+ # set.add("/static", :second)
158
+ # set.add("/:path", :third)
159
+ #
160
+ # set.match("/static").value # => :first
161
+ # set.match_all("/static").map(&:value) # => [:first, :second, :third]
162
+ #
163
+ # @return [Boolean] whether matching happens in strict pattern/value insertion order
164
+ def strict_order? = @strict_order
165
+
166
+ # @return [Boolean] whether caching is enabled
167
+ def use_cache? = @use_cache
168
+
169
+ # @return [Boolean] whether trie optimization is enabled
170
+ def use_trie? = @use_trie == true
171
+
172
+ # Adds a pattern to the set, optionally associated with one or more values.
173
+ #
174
+ # If the pattern is given as a String it will be compiled via {Mustermann.new}
175
+ # using the set's own options. The pattern must be AST-based (Sinatra, Rails,
176
+ # and similar types). Plain regexp patterns are not supported.
177
+ #
178
+ # Calling +add+ more than once for the same pattern appends additional values
179
+ # without creating duplicates.
180
+ #
181
+ # @example
182
+ # set.add('/users/:id', :users)
183
+ # set.add('/users/:id', :admin) # same pattern, second value
184
+ #
185
+ # @param pattern [String, Pattern] the pattern to add
186
+ # @param values [Array] zero or more values to associate with the pattern
187
+ # @return [self]
188
+ # @raise [ArgumentError] if the pattern is not AST-based, or if a reserved symbol is used as a value
189
+ def add(pattern, *values)
190
+ if pattern.is_a? Composite and pattern.operator == :|
191
+ pattern.patterns.each { |p| add(p, *values) }
192
+ return self
193
+ end
194
+
195
+ pattern = Mustermann.new(pattern, **options)
196
+ raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast
197
+
198
+ if @mapping.key? pattern
199
+ current = @mapping[pattern]
200
+ else
201
+ add_pattern(pattern)
202
+ current = @mapping[pattern] = []
203
+ end
204
+
205
+ values = [nil] if values.empty?
206
+
207
+ values.each do |value|
208
+ raise ArgumentError, "%p may not be used as a value" % value if Expander::ADDITIONAL_VALUES.include? value
209
+ raise ArgumentError, "the set itself may not be used as value" if value == self
210
+ next if current.include? value
211
+ current << value
212
+ @reverse_mapping[value] ||= []
213
+ @reverse_mapping[value] << pattern unless @reverse_mapping[value].include? pattern
214
+ @expanders[value]&.add(pattern)
215
+ @matcher.track(pattern, value) if strict_order?
216
+ end
217
+
218
+ self
219
+ end
220
+
221
+ # Adds a pattern associated with a value using hash-assignment syntax.
222
+ # @see #add
223
+ alias []= add
224
+
225
+ # Looks up a value by string or retrieves the first value for a known pattern object.
226
+ #
227
+ # When given a String, it is matched against the set and the associated value of the
228
+ # first matching pattern is returned. When given a {Pattern}, the first value
229
+ # registered for that exact pattern is returned without matching.
230
+ #
231
+ # @example String lookup
232
+ # set['/users/42'] # => :users_show (or nil)
233
+ #
234
+ # @example Pattern lookup
235
+ # pattern = Mustermann.new('/users/:id')
236
+ # set[pattern] # => :users_show (or nil)
237
+ #
238
+ # @param pattern_or_string [String, Pattern]
239
+ # @return [Object, nil] the associated value, or +nil+ if not found
240
+ # @raise [ArgumentError] for unsupported argument types
241
+ def [](pattern_or_string)
242
+ case pattern_or_string
243
+ when String then match(pattern_or_string)&.value
244
+ when Pattern then values_for_pattern(pattern_or_string)&.first
245
+ else raise ArgumentError, "unsupported pattern type #{pattern_or_string.class}"
246
+ end
247
+ end
248
+
249
+ # Matches the string against all patterns in the set and returns the first match.
250
+ #
251
+ # @param string [String] the string to match
252
+ # @return [Set::Match, nil] the first match, or +nil+ if none of the patterns match
253
+ def match(string) = @matcher&.match(string)
254
+
255
+ # Matches the beginning of the string against all patterns and returns the
256
+ # first prefix match. The unmatched remainder of the string is available via
257
+ # {Set::Match#post_match}.
258
+ #
259
+ # @param string [String]
260
+ # @return [Set::Match, nil] the first prefix match, or +nil+
261
+ def peek_match(string) = @matcher&.match(string, peek: true)
262
+
263
+ # Matches the string against all patterns and returns every match, one per
264
+ # (pattern, value) pair, in insertion order.
265
+ #
266
+ # @param string [String]
267
+ # @return [Array<Set::Match>] all matches, or an empty array if none
268
+ def match_all(string) = @matcher&.match(string, all: true)
269
+
270
+ # Matches the beginning of the string against all patterns and returns every
271
+ # prefix match, one per (pattern, value) pair. The unmatched remainder is
272
+ # available as {Set::Match#post_match} on each result.
273
+ #
274
+ # @param string [String]
275
+ # @return [Array<Set::Match>] all prefix matches, or an empty array if none
276
+ def peek_match_all(string) = @matcher&.match(string, all: true, peek: true)
277
+
278
+ # Returns a new set that includes all patterns from the receiver plus those
279
+ # from +mapping+. The receiver is not modified.
280
+ #
281
+ # @param mapping [Hash, String, Pattern, Array, Set] patterns to merge in
282
+ # @return [Set] a new set
283
+ def merge(mapping) = dup.update(mapping)
284
+
285
+ # @!visibility private
286
+ def initialize_copy(other)
287
+ @mapping = other.mapping.transform_values(&:dup)
288
+ @reverse_mapping = @mapping.each_with_object({}) do |(pattern, values), h|
289
+ values.each { |value| (h[value] ||= []) << pattern }
290
+ end
291
+ @expanders = {}
292
+ @matcher = nil
293
+ @mapping.each_key { |pattern| add_pattern(pattern) }
294
+ end
295
+
296
+ # Adds all patterns from +mapping+ to the set in place and returns +self+.
297
+ # Aliased as +merge!+.
298
+ #
299
+ # Accepts the same argument forms as {#initialize}: a Hash, a String, a
300
+ # {Pattern}, an Array, or another {Set}.
301
+ #
302
+ # @param mapping [Hash, String, Pattern, Array, Set]
303
+ # @return [self]
304
+ # @raise [ArgumentError] for unsupported mapping types
305
+ def update(mapping)
306
+ case mapping
307
+ when Set then mapping.mapping.each { |pattern, values| add(pattern, *values) }
308
+ when Hash then mapping.each { |k, v| add(k, v) }
309
+ when String, Pattern then add(mapping)
310
+ when Array then mapping.each { |item| update(item) }
311
+ else raise ArgumentError, "unsupported mapping type #{mapping.class}"
312
+ end
313
+ self
314
+ end
315
+
316
+ alias merge! update
317
+
318
+ # Returns all patterns that have been added to the set, in insertion order.
319
+ # @return [Array<Pattern>]
320
+ def patterns = @mapping.keys
321
+
322
+ # Returns an {Expander} that can generate strings from parameter hashes.
323
+ #
324
+ # When called without arguments (or with the set itself as the value) the
325
+ # expander covers all patterns in the set. Pass a specific value to get an
326
+ # expander limited to the patterns associated with that value.
327
+ #
328
+ # @param value [Object] restricts the expander to patterns associated with
329
+ # this value; defaults to the set itself (all patterns)
330
+ # @return [Mustermann::Expander]
331
+ def expander(value = self)
332
+ @expanders[value] ||= begin
333
+ patterns = value == self ? @mapping.keys : @reverse_mapping[value] || EMPTY_ARRAY
334
+ Mustermann::Expander.new(patterns, additional_values: @additional_values, **options)
335
+ end
336
+ end
337
+
338
+ # Generates a string from a parameter hash using the patterns in the set.
339
+ #
340
+ # When called with just a parameter hash, the first pattern that can be fully
341
+ # expanded with those keys is used. Pass a value as the first argument to
342
+ # restrict expansion to the patterns associated with that value. You may also
343
+ # pass an +additional_values+ behavior symbol (+:raise+, +:ignore+, or
344
+ # +:append+) as the first argument to override the set's default behavior for
345
+ # that call.
346
+ #
347
+ # @example Expand using any pattern
348
+ # set.expand(id: '5')
349
+ #
350
+ # @example Expand patterns for a specific value
351
+ # set.expand(:users, id: '5')
352
+ #
353
+ # @example Override additional_values behavior for one call
354
+ # set.expand(:ignore, id: '5', extra: 'ignored')
355
+ #
356
+ # @param value [Object, :raise, :ignore, :append] the value whose patterns
357
+ # should be used, or an additional_values behavior symbol; defaults to all
358
+ # patterns
359
+ # @param behavior [:raise, :ignore, :append, nil] how to handle extra keys;
360
+ # defaults to the set's +additional_values+ setting
361
+ # @param values [Hash, nil] the parameters to expand
362
+ # @return [String]
363
+ # @raise [Mustermann::ExpandError] if no pattern can be expanded with the given keys
364
+ def expand(value = self, behavior = nil, values = nil)
365
+ if Expander::ADDITIONAL_VALUES.include? value
366
+ if behavior.is_a? Hash
367
+ values = values ? values.merge(behavior) : behavior
368
+ behavior = nil
369
+ elsif behavior and behavior != value
370
+ raise ArgumentError, "behavior specified multiple times" if behavior
371
+ end
372
+ behavior = value
373
+ value = self
374
+ elsif value.is_a? Hash and behavior.nil? and values.nil?
375
+ values = value
376
+ value = self unless @reverse_mapping.key? values
377
+ end
378
+ expander(value).expand(behavior || @additional_values, values || EMPTY_HASH)
379
+ end
380
+
381
+ # @return [Boolean] whether the set contains any pattern associated with the given value
382
+ def has_value?(value) = @reverse_mapping[value]&.any?
383
+
384
+ # @!visibility private
385
+ def values_for_pattern(pattern) = @mapping[pattern] # :nodoc:
386
+
387
+ # Runs trie optimizations pro-actively and explicitly rather than at match time.
388
+ def optimize! = @matcher&.optimize!
389
+
390
+ # @!visibility private
391
+ def inspect # :nodoc:
392
+ mapping = @mapping.map do |pattern, values|
393
+ if values.size > 1 or values.first.is_a? Array
394
+ "%p => %p" % [pattern.to_s, values]
395
+ elsif values.first != nil
396
+ "%p => %p" % [pattern.to_s, values.first]
397
+ else
398
+ "%p" % pattern.to_s
399
+ end
400
+ end
401
+ "#<#{self.class.name}: #{mapping.join(", ")}>"
402
+ end
403
+
404
+ # @!visibility private
405
+ def pretty_print(q) # :nodoc:
406
+ q.text "#<#{self.class.name}"
407
+ q.group(1, "", ">") do
408
+ @mapping.each_with_index do |(pattern, values), index|
409
+ q.breakable(index == 0 ? " " : ", ")
410
+ q.pp(pattern.to_s)
411
+ if values.size > 1 or values.first.is_a? Array
412
+ q.text " => "
413
+ q.pp(values)
414
+ elsif values.first != nil
415
+ q.text " => "
416
+ q.pp(values.first)
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ protected
423
+
424
+ attr_reader :mapping
425
+
426
+ private
427
+
428
+ def add_pattern(pattern)
429
+ if @use_trie.is_a? Integer and @mapping.size >= @use_trie
430
+ @use_trie = true
431
+ @matcher = build_matcher
432
+ end
433
+
434
+ @matcher ||= build_matcher
435
+ @matcher.add(pattern)
436
+ @expanders[self]&.add(pattern)
437
+ end
438
+
439
+ def build_matcher
440
+ factory = use_trie? ? Trie : Linear
441
+ matcher = factory.new(self, @mapping.keys)
442
+ matcher = StrictOrder.new(matcher) if strict_order?
443
+ matcher = Cache.new(matcher) if use_cache?
444
+ matcher
445
+ end
446
+ end
447
+ 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: EMPTY_ARRAY, **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 EMPTY_HASH
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.1'
4
4
  end