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.
- checksums.yaml +4 -4
- data/LICENSE +1 -2
- data/README.md +238 -261
- data/lib/mustermann/ast/compiler.rb +128 -30
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/expander.rb +4 -5
- data/lib/mustermann/ast/fast_pattern.rb +122 -0
- data/lib/mustermann/ast/param_scanner.rb +38 -6
- data/lib/mustermann/ast/parser.rb +2 -3
- data/lib/mustermann/ast/pattern.rb +26 -4
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/ast/translator.rb +11 -7
- data/lib/mustermann/composite.rb +25 -6
- data/lib/mustermann/concat.rb +16 -5
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +26 -4
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/match.rb +155 -0
- data/lib/mustermann/pattern.rb +27 -32
- data/lib/mustermann/rails.rb +63 -0
- data/lib/mustermann/regexp_based.rb +70 -9
- data/lib/mustermann/router.rb +104 -0
- data/lib/mustermann/set/cache.rb +48 -0
- data/lib/mustermann/set/linear.rb +32 -0
- data/lib/mustermann/set/match.rb +23 -0
- data/lib/mustermann/set/strict_order.rb +29 -0
- data/lib/mustermann/set/trie.rb +270 -0
- data/lib/mustermann/set.rb +445 -0
- data/lib/mustermann/sinatra/safe_renderer.rb +1 -1
- data/lib/mustermann/sinatra/try_convert.rb +49 -11
- data/lib/mustermann/sinatra.rb +35 -11
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann/versions.rb +47 -0
- data/lib/mustermann.rb +0 -15
- metadata +31 -45
- data/bench/capturing.rb +0 -57
- data/bench/regexp.rb +0 -21
- data/bench/simple_vs_sinatra.rb +0 -23
- data/bench/template_vs_addressable.rb +0 -26
- data/bench/uri_parser_object.rb +0 -16
- data/lib/mustermann/extension.rb +0 -3
- data/lib/mustermann/mapper.rb +0 -91
- data/lib/mustermann/pattern_cache.rb +0 -50
- data/lib/mustermann/simple_match.rb +0 -49
- data/lib/mustermann/to_pattern.rb +0 -51
- data/mustermann.gemspec +0 -18
- data/spec/ast_spec.rb +0 -15
- data/spec/composite_spec.rb +0 -163
- data/spec/concat_spec.rb +0 -127
- data/spec/equality_map_spec.rb +0 -42
- data/spec/expander_spec.rb +0 -123
- data/spec/identity_spec.rb +0 -127
- data/spec/mapper_spec.rb +0 -77
- data/spec/mustermann_spec.rb +0 -81
- data/spec/pattern_spec.rb +0 -54
- data/spec/regexp_based_spec.rb +0 -9
- data/spec/regular_spec.rb +0 -119
- data/spec/simple_match_spec.rb +0 -11
- data/spec/sinatra_spec.rb +0 -836
- 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
|
|
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
|
|
34
|
+
def new(input, escape: false, **opts)
|
|
25
35
|
input = Mustermann::Sinatra.escape(input) if escape
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
data/lib/mustermann/sinatra.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
61
|
-
return super
|
|
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
|
|
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" => "
|
|
75
|
+
# Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "foo" }
|
|
74
76
|
#
|
|
75
|
-
# @return [String] string
|
|
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 =
|
|
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
|
|
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
|
data/lib/mustermann/version.rb
CHANGED
|
@@ -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
|