mustermann 4.0.0 → 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.
- checksums.yaml +4 -4
- data/lib/mustermann/ast/compiler.rb +19 -9
- data/lib/mustermann/ast/expander.rb +2 -2
- data/lib/mustermann/ast/fast_pattern.rb +3 -3
- data/lib/mustermann/ast/param_scanner.rb +2 -2
- data/lib/mustermann/ast/pattern.rb +8 -7
- data/lib/mustermann/ast/template_generator.rb +2 -2
- data/lib/mustermann/ast/translator.rb +9 -4
- data/lib/mustermann/ast/validation.rb +1 -1
- data/lib/mustermann/composite.rb +2 -2
- data/lib/mustermann/concat.rb +1 -1
- data/lib/mustermann/expander.rb +2 -2
- data/lib/mustermann/identity.rb +1 -1
- data/lib/mustermann/match.rb +4 -4
- data/lib/mustermann/pattern.rb +5 -5
- data/lib/mustermann/regexp_based.rb +2 -2
- data/lib/mustermann/regular.rb +1 -1
- data/lib/mustermann/router.rb +1 -1
- data/lib/mustermann/set/cache.rb +0 -1
- data/lib/mustermann/set/trie.rb +38 -26
- data/lib/mustermann/set.rb +4 -2
- data/lib/mustermann/sinatra/try_convert.rb +1 -1
- data/lib/mustermann/sinatra.rb +1 -1
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +42 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4960bdf923e4b4a21deb051cfaa831122b3db56d38c09441ffd7a52ca650aa81
|
|
4
|
+
data.tar.gz: 545a7945333b4f25d0525612b50090e0f6562e66d7ff5c56043962f6e720c0b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8900d2b779556e86bd35454418982580b22400229720ee84ada894bd29e6732ec59094214fea478ae00c670a4cb5127870ab8baefce7cf4d323e31961c89858
|
|
7
|
+
data.tar.gz: f86cf1e9a034c7c4ba117326c7c0b939c705c47a8b9ba1318e4f4b2247d51fcb5d47ee96a0be5f0953d6c1270375904e464503e32d15cf324896d0e1f8d9ba26
|
|
@@ -28,16 +28,19 @@ module Mustermann
|
|
|
28
28
|
# Strip `atomic:` from incoming options so a parent's value cannot bleed
|
|
29
29
|
# into siblings; each element's atomicity comes solely from its own context.
|
|
30
30
|
translate(Array) do |atomic: false, **options|
|
|
31
|
-
greedy
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
greedy = options.fetch(:greedy, true)
|
|
32
|
+
elements = node
|
|
33
|
+
result = +''
|
|
34
|
+
elements.each_with_index do |element, index|
|
|
35
|
+
next_sibling = elements[index + 1]
|
|
34
36
|
atomic = greedy &&
|
|
35
37
|
element.is_a?(:capture) &&
|
|
36
38
|
!element.is_a?(:splat) &&
|
|
37
39
|
next_sibling&.is_a?(:separator) &&
|
|
38
40
|
next_sibling.payload == '/'
|
|
39
|
-
t(element, **options, atomic: atomic)
|
|
40
|
-
end
|
|
41
|
+
result << t(element, **options, atomic: atomic)
|
|
42
|
+
end
|
|
43
|
+
result
|
|
41
44
|
end
|
|
42
45
|
|
|
43
46
|
translate(:node) { |**o| t(payload, **o) }
|
|
@@ -56,7 +59,7 @@ module Mustermann
|
|
|
56
59
|
|
|
57
60
|
translate :with_look_ahead do |atomic: false, **options|
|
|
58
61
|
greedy = options.fetch(:greedy, true)
|
|
59
|
-
lookahead = each_leaf.inject(
|
|
62
|
+
lookahead = each_leaf.inject(EMPTY_STRING) do |ahead, element|
|
|
60
63
|
ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
|
|
61
64
|
end
|
|
62
65
|
lookahead << (at_end ? '$' : '/')
|
|
@@ -227,9 +230,16 @@ module Mustermann
|
|
|
227
230
|
def encoded(char, uri_decode: true, space_matches_plus: true, **options)
|
|
228
231
|
return Regexp.escape(char) unless uri_decode
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
self.class.encoded_chars[[char, space_matches_plus]] ||= begin
|
|
234
|
+
representations = self.class.char_representations(char, uri_decode:, space_matches_plus:)
|
|
235
|
+
('(?:%s)' % representations.map { |c| Regexp.escape(c) }.join('|')).freeze
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# @return [Hash] regular expression source for each URI-encodable character
|
|
240
|
+
# @!visibility private
|
|
241
|
+
def self.encoded_chars
|
|
242
|
+
@encoded_chars ||= {}
|
|
233
243
|
end
|
|
234
244
|
|
|
235
245
|
# Compiles an AST to a regular expression.
|
|
@@ -131,14 +131,14 @@ module Mustermann
|
|
|
131
131
|
|
|
132
132
|
# Turns a sprintf pattern into our secret internal data structure.
|
|
133
133
|
# @!visibility private
|
|
134
|
-
def pattern(string =
|
|
134
|
+
def pattern(string = EMPTY_STRING, *keys, **filters)
|
|
135
135
|
[[keys, string, filters]]
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
# Creates the product of two of our secret internal data structures.
|
|
139
139
|
# @!visibility private
|
|
140
140
|
def add_to(list, result)
|
|
141
|
-
list << [[],
|
|
141
|
+
list << [[], EMPTY_STRING] if list.empty?
|
|
142
142
|
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } }
|
|
143
143
|
end
|
|
144
144
|
end
|
|
@@ -14,13 +14,13 @@ module Mustermann
|
|
|
14
14
|
|
|
15
15
|
# Regexp fragment for each printable ASCII char, matching the same output
|
|
16
16
|
# as Compiler#encoded with uri_decode: true.
|
|
17
|
-
ENCODED = (0..127).each_with_object({}) do |byte, h|
|
|
17
|
+
ENCODED = Mustermann.dedup((0..127).each_with_object({}) do |byte, h|
|
|
18
18
|
c = byte.chr
|
|
19
19
|
pct = '%%%02X' % byte
|
|
20
20
|
reps = [c, pct, pct.downcase].uniq
|
|
21
21
|
h[c] = reps.size == 1 ? Regexp.escape(reps.first) :
|
|
22
22
|
'(?:%s)' % reps.map { |r| Regexp.escape(r) }.join('|')
|
|
23
|
-
end
|
|
23
|
+
end)
|
|
24
24
|
|
|
25
25
|
SEGMENT_SCAN = %r{(/)|(:[a-zA-Z_]\w*)|([^/:]+)}
|
|
26
26
|
|
|
@@ -31,7 +31,7 @@ module Mustermann
|
|
|
31
31
|
def to_ast
|
|
32
32
|
return super unless simple_pattern?
|
|
33
33
|
ast = self.class.ast_cache.fetch(@string) { build_fast_ast }
|
|
34
|
-
@param_converters ||=
|
|
34
|
+
@param_converters ||= EMPTY_HASH
|
|
35
35
|
ast
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -16,7 +16,7 @@ module Mustermann
|
|
|
16
16
|
translate(:node) { |o| t(payload, o) }
|
|
17
17
|
translate(:with_look_ahead) { |o| t(head, o).merge(t(payload, o)) }
|
|
18
18
|
translate(Array) { |o| map { |e| t(e, o) }.inject(:merge) }
|
|
19
|
-
translate(Object) { |o|
|
|
19
|
+
translate(Object) { |o| EMPTY_HASH }
|
|
20
20
|
|
|
21
21
|
class Capture < NodeTranslator
|
|
22
22
|
register :capture
|
|
@@ -24,7 +24,7 @@ module Mustermann
|
|
|
24
24
|
def translate(options)
|
|
25
25
|
return { name => convert } if convert
|
|
26
26
|
_, converter = converter(options[:capture])
|
|
27
|
-
converter ? { name => converter } :
|
|
27
|
+
converter ? { name => converter } : EMPTY_HASH
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def converter(capture)
|
|
@@ -95,20 +95,21 @@ module Mustermann
|
|
|
95
95
|
# @!visibility private
|
|
96
96
|
def except_regexp
|
|
97
97
|
return unless except_str = options[:except]
|
|
98
|
-
@except_regexp ||=
|
|
98
|
+
@except_regexp ||= begin
|
|
99
|
+
source = compiler.new.translate(parse(except_str), no_captures: true, **options.except(:except))
|
|
100
|
+
Mustermann.dedup(Regexp.new("\\A#{source}\\z"))
|
|
101
|
+
end
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
# Internal AST representation of pattern.
|
|
102
105
|
# @!visibility private
|
|
103
106
|
def to_ast
|
|
104
|
-
|
|
107
|
+
self.class.ast_cache.fetch([@string, options]) do
|
|
105
108
|
ast = parse(@string, pattern: self)
|
|
106
109
|
ast &&= transform(ast)
|
|
107
110
|
ast &&= set_boundaries(ast, string: @string)
|
|
108
111
|
validate(ast)
|
|
109
112
|
end
|
|
110
|
-
@param_converters ||= scan_params(ast, options) if ast
|
|
111
|
-
ast
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
# All AST-based pattern implementations support expanding.
|
|
@@ -119,7 +120,7 @@ module Mustermann
|
|
|
119
120
|
# @raise (see Mustermann::Pattern#expand)
|
|
120
121
|
# @see Mustermann::Pattern#expand
|
|
121
122
|
# @see Mustermann::Expander
|
|
122
|
-
def expand(behavior = nil, values =
|
|
123
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
123
124
|
@expander ||= Mustermann::Expander.new(self)
|
|
124
125
|
@expander.expand(behavior, values)
|
|
125
126
|
end
|
|
@@ -131,7 +132,7 @@ module Mustermann
|
|
|
131
132
|
# @return (see Mustermann::Pattern#to_templates)
|
|
132
133
|
# @see Mustermann::Pattern#to_templates
|
|
133
134
|
def to_templates
|
|
134
|
-
@to_templates ||= generate_templates(to_ast)
|
|
135
|
+
@to_templates ||= Mustermann.dedup(generate_templates(to_ast))
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
# @!visibility private
|
|
@@ -144,7 +145,7 @@ module Mustermann
|
|
|
144
145
|
|
|
145
146
|
# @!visibility private
|
|
146
147
|
def param_converters
|
|
147
|
-
@param_converters ||= scan_params(to_ast, options)
|
|
148
|
+
@param_converters ||= Mustermann.dedup(scan_params(to_ast, options))
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
# @api private
|
|
@@ -13,10 +13,10 @@ module Mustermann
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# translate(:expression) is not needed, since template patterns simply call to_s
|
|
16
|
-
translate(:root, :group) { t(payload) || [
|
|
16
|
+
translate(:root, :group) { t(payload) || [EMPTY_STRING] }
|
|
17
17
|
translate(:separator, :char) { t.escape(payload) }
|
|
18
18
|
translate(:capture) { "{#{name}}" }
|
|
19
|
-
translate(:optional) { [t(payload),
|
|
19
|
+
translate(:optional) { [t(payload), EMPTY_STRING] }
|
|
20
20
|
translate(:named_splat, :splat) { "{+#{name}}" }
|
|
21
21
|
translate(:with_look_ahead) { t([head, payload]) }
|
|
22
22
|
translate(:union) { payload.flat_map { |e| t(e) } }
|
|
@@ -38,11 +38,15 @@ module Mustermann
|
|
|
38
38
|
attr_reader :translator
|
|
39
39
|
|
|
40
40
|
# shorthand for translating a nested object
|
|
41
|
+
#
|
|
42
|
+
# Keywords pass through without being collected into a new hash, as
|
|
43
|
+
# this runs for every node of every pattern.
|
|
41
44
|
# @!visibility private
|
|
42
|
-
def t(*args,
|
|
45
|
+
def t(*args, &block)
|
|
43
46
|
return translator unless args.any?
|
|
44
|
-
translator.translate(*args,
|
|
47
|
+
translator.translate(*args, &block)
|
|
45
48
|
end
|
|
49
|
+
ruby2_keywords :t
|
|
46
50
|
|
|
47
51
|
# @!visibility private
|
|
48
52
|
alias_method :node, :__getobj__
|
|
@@ -117,11 +121,12 @@ module Mustermann
|
|
|
117
121
|
|
|
118
122
|
# Start the translation dance for a (sub)tree.
|
|
119
123
|
# @!visibility private
|
|
120
|
-
def translate(node, *args,
|
|
121
|
-
result = decorator_for(node).translate(*args,
|
|
124
|
+
def translate(node, *args, &block)
|
|
125
|
+
result = decorator_for(node).translate(*args, &block)
|
|
122
126
|
result = result.node while result.is_a? NodeTranslator
|
|
123
127
|
result
|
|
124
128
|
end
|
|
129
|
+
ruby2_keywords :translate
|
|
125
130
|
|
|
126
131
|
# @return [String] escaped character
|
|
127
132
|
# @!visibility private
|
|
@@ -27,7 +27,7 @@ module Mustermann
|
|
|
27
27
|
|
|
28
28
|
# @raise [Mustermann::CompileError] if name is not acceptable
|
|
29
29
|
# @!visibility private
|
|
30
|
-
def check_name(name, forbidden:
|
|
30
|
+
def check_name(name, forbidden: EMPTY_ARRAY)
|
|
31
31
|
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
|
|
32
32
|
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
|
|
33
33
|
raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
|
data/lib/mustermann/composite.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Mustermann
|
|
|
31
31
|
|
|
32
32
|
# @see Mustermann::Pattern#names
|
|
33
33
|
def names
|
|
34
|
-
@names ||= patterns.flat_map { |p| p.respond_to?(:names) ? p.names :
|
|
34
|
+
@names ||= Mustermann.dedup(patterns.flat_map { |p| p.respond_to?(:names) ? p.names : EMPTY_ARRAY }.uniq)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# @see Mustermann::Pattern#==
|
|
@@ -71,7 +71,7 @@ module Mustermann
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# (see Mustermann::Pattern#expand)
|
|
74
|
-
def expand(behavior = nil, values =
|
|
74
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
75
75
|
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
|
|
76
76
|
@expander ||= Mustermann::Expander.new(*patterns)
|
|
77
77
|
@expander.expand(behavior, values)
|
data/lib/mustermann/concat.rb
CHANGED
|
@@ -107,7 +107,7 @@ module Mustermann
|
|
|
107
107
|
# (see Mustermann::Pattern#to_templates)
|
|
108
108
|
def to_templates
|
|
109
109
|
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
|
|
110
|
-
@to_templates ||= patterns.inject([
|
|
110
|
+
@to_templates ||= patterns.inject([EMPTY_STRING]) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# @!visibility private
|
data/lib/mustermann/expander.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Mustermann
|
|
|
27
27
|
@patterns = []
|
|
28
28
|
@api_expander = AST::Expander.new
|
|
29
29
|
@additional_values = additional_values
|
|
30
|
-
@options = options
|
|
30
|
+
@options = Mustermann.dedup(options)
|
|
31
31
|
@caster = Caster.new
|
|
32
32
|
add(*patterns, &block)
|
|
33
33
|
end
|
|
@@ -147,7 +147,7 @@ module Mustermann
|
|
|
147
147
|
# @return [String] expanded string
|
|
148
148
|
# @raise [NotImplementedError] raised if expand is not supported.
|
|
149
149
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
|
150
|
-
def expand(behavior = nil, values =
|
|
150
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
151
151
|
behavior, values = nil, behavior if behavior.is_a? Hash
|
|
152
152
|
values = map_values(values)
|
|
153
153
|
|
data/lib/mustermann/identity.rb
CHANGED
|
@@ -65,7 +65,7 @@ module Mustermann
|
|
|
65
65
|
# @raise (see Mustermann::Pattern#expand)
|
|
66
66
|
# @see Mustermann::Pattern#expand
|
|
67
67
|
# @see Mustermann::Expander
|
|
68
|
-
def expand(behavior = nil, values =
|
|
68
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
69
69
|
return to_s if values.empty? or behavior == :ignore
|
|
70
70
|
raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise
|
|
71
71
|
raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append
|
data/lib/mustermann/match.rb
CHANGED
|
@@ -60,10 +60,10 @@ module Mustermann
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
@pattern = pattern || match&.pattern
|
|
63
|
-
@string = string || match&.string ||
|
|
64
|
-
@params = params || match&.params ||
|
|
65
|
-
@post_match = post_match || match&.post_match ||
|
|
66
|
-
@pre_match = pre_match || match&.pre_match ||
|
|
63
|
+
@string = string || match&.string || EMPTY_STRING
|
|
64
|
+
@params = params || match&.params || EMPTY_HASH
|
|
65
|
+
@post_match = post_match || match&.post_match || EMPTY_STRING
|
|
66
|
+
@pre_match = pre_match || match&.pre_match || EMPTY_STRING
|
|
67
67
|
@captures = captures || match&.captures || @params.values
|
|
68
68
|
@named_captures = named_captures || match&.named_captures || @params
|
|
69
69
|
@matched = matched || match&.to_s || @string
|
data/lib/mustermann/pattern.rb
CHANGED
|
@@ -74,8 +74,8 @@ module Mustermann
|
|
|
74
74
|
# @see Mustermann.new
|
|
75
75
|
def initialize(string, uri_decode: true, **options)
|
|
76
76
|
@uri_decode = uri_decode
|
|
77
|
-
@string = string
|
|
78
|
-
@options = yield
|
|
77
|
+
@string = Mustermann.dedup(string)
|
|
78
|
+
@options = Mustermann.dedup(yield) if block_given?
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# @return [String] the string representation of the pattern
|
|
@@ -91,7 +91,7 @@ module Mustermann
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# @return [Array<String>] list of named captures in the pattern
|
|
94
|
-
def names =
|
|
94
|
+
def names = EMPTY_ARRAY
|
|
95
95
|
|
|
96
96
|
# @param [String] string The string to match against
|
|
97
97
|
# @return [Integer, nil] nil if pattern does not match the string, zero if it does.
|
|
@@ -168,7 +168,7 @@ module Mustermann
|
|
|
168
168
|
# @see #peek_params
|
|
169
169
|
def peek_match(string)
|
|
170
170
|
matched = peek(string)
|
|
171
|
-
Match.new(self, string, matched:, params:
|
|
171
|
+
Match.new(self, string, matched:, params: EMPTY_HASH, post_match: string[matched.size..-1]) if matched
|
|
172
172
|
end
|
|
173
173
|
|
|
174
174
|
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
|
@@ -217,7 +217,7 @@ module Mustermann
|
|
|
217
217
|
# @raise [NotImplementedError] raised if expand is not supported.
|
|
218
218
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
|
219
219
|
# @see Mustermann::Expander
|
|
220
|
-
def expand(behavior = nil, values =
|
|
220
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
221
221
|
raise NotImplementedError, "expanding not supported by #{self.class}"
|
|
222
222
|
end
|
|
223
223
|
|
|
@@ -22,8 +22,8 @@ module Mustermann
|
|
|
22
22
|
|
|
23
23
|
super
|
|
24
24
|
regexp = compile(**options)
|
|
25
|
-
@peek_regexp = /\A#{regexp}/
|
|
26
|
-
@regexp = /\A#{regexp}\Z/
|
|
25
|
+
@peek_regexp = Mustermann.dedup(/\A#{regexp}/)
|
|
26
|
+
@regexp = Mustermann.dedup(/\A#{regexp}\Z/)
|
|
27
27
|
@simple_captures = @regexp.named_captures.none? { |name, positions| positions.size > 1 || always_array?(name) }
|
|
28
28
|
|
|
29
29
|
cache_class = ObjectSpace::WeakKeyMap if defined?(ObjectSpace::WeakKeyMap)
|
data/lib/mustermann/regular.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Mustermann
|
|
|
21
21
|
# @see (see Mustermann::Pattern#initialize)
|
|
22
22
|
def initialize(string, check_anchors: true, **options)
|
|
23
23
|
string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/"
|
|
24
|
-
string = string.source.gsub!(/(?<!\\)(?:\s|#.*$)/,
|
|
24
|
+
string = string.source.gsub!(/(?<!\\)(?:\s|#.*$)/, EMPTY_STRING) if extended_regexp?(string)
|
|
25
25
|
@check_anchors = check_anchors
|
|
26
26
|
super(string, **options)
|
|
27
27
|
end
|
data/lib/mustermann/router.rb
CHANGED
|
@@ -92,7 +92,7 @@ module Mustermann
|
|
|
92
92
|
# @param app [#call] The Rack application or middleware for which to generate the path.
|
|
93
93
|
# @param (see Mustermann::Expander#expand)
|
|
94
94
|
# @return [String] The generated path.
|
|
95
|
-
def path_for(app, behavior = nil, params =
|
|
95
|
+
def path_for(app, behavior = nil, params = EMPTY_HASH)
|
|
96
96
|
set = @sets.values.find { |s| s.has_value?(app) } || @sets[VERBS.first]
|
|
97
97
|
set.expand(app, behavior, params)
|
|
98
98
|
end
|
data/lib/mustermann/set/cache.rb
CHANGED
data/lib/mustermann/set/trie.rb
CHANGED
|
@@ -12,8 +12,12 @@ module Mustermann
|
|
|
12
12
|
translate(:root) do |trie, **options|
|
|
13
13
|
leaves = t(payload, trie, **options)
|
|
14
14
|
if leaves.is_a? Array
|
|
15
|
-
leaves.each
|
|
15
|
+
leaves.each do |leaf|
|
|
16
|
+
leaf.patterns ||= []
|
|
17
|
+
leaf.patterns << t.pattern
|
|
18
|
+
end
|
|
16
19
|
else
|
|
20
|
+
leaves.patterns ||= []
|
|
17
21
|
leaves.patterns << t.pattern
|
|
18
22
|
end
|
|
19
23
|
leaves
|
|
@@ -64,24 +68,28 @@ module Mustermann
|
|
|
64
68
|
def compile(node, **options) = /\G#{@compiler.translate(node, **@options, **options)}/
|
|
65
69
|
end
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
attr_accessor :patterns, :set, :static, :dynamic
|
|
68
72
|
|
|
69
|
-
def initialize(set, patterns =
|
|
73
|
+
def initialize(set, patterns = nil)
|
|
70
74
|
@set = set
|
|
71
|
-
@patterns =
|
|
72
|
-
@dynamic =
|
|
73
|
-
@static =
|
|
75
|
+
@patterns = nil
|
|
76
|
+
@dynamic = nil
|
|
77
|
+
@static = nil
|
|
74
78
|
@stride = nil
|
|
75
79
|
@fast_static = nil
|
|
76
80
|
@byte_lookup = nil
|
|
77
81
|
@dynamic_entries = nil
|
|
78
|
-
patterns
|
|
82
|
+
patterns&.each { |pattern| add(pattern) }
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
def [](key)
|
|
82
|
-
case key
|
|
83
|
-
when String
|
|
84
|
-
|
|
86
|
+
case key = Mustermann.dedup(key)
|
|
87
|
+
when String
|
|
88
|
+
@static ||= {}
|
|
89
|
+
@static[key] ||= Trie.new(@set)
|
|
90
|
+
when Regexp
|
|
91
|
+
@dynamic ||= {}
|
|
92
|
+
@dynamic[key] ||= Trie.new(@set)
|
|
85
93
|
end
|
|
86
94
|
end
|
|
87
95
|
|
|
@@ -107,8 +115,7 @@ module Mustermann
|
|
|
107
115
|
end
|
|
108
116
|
end
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
anchored = nil
|
|
118
|
+
if @dynamic_entries&.any?
|
|
112
119
|
base_params = all ? params : nil
|
|
113
120
|
@dynamic_entries.each do |matcher, node, capture_names, fast_name|
|
|
114
121
|
if fast_name
|
|
@@ -127,8 +134,8 @@ module Mustermann
|
|
|
127
134
|
# Non-greedy patterns (e.g. splat .*?) can match 0 chars on non-empty input, making
|
|
128
135
|
# no progress. Retry with an end-of-string anchor so they consume the full remainder.
|
|
129
136
|
if regexp_match && regexp_match.end(0) == position
|
|
130
|
-
anchored ||= {}
|
|
131
|
-
anchored_matcher = anchored[matcher] ||= Regexp.new(matcher.source + '\z')
|
|
137
|
+
@@anchored ||= {}
|
|
138
|
+
anchored_matcher = @@anchored[matcher] ||= Mustermann.dedup(Regexp.new(matcher.source + '\z'))
|
|
132
139
|
regexp_match = anchored_matcher.match(string, position)
|
|
133
140
|
end
|
|
134
141
|
next unless regexp_match
|
|
@@ -148,7 +155,7 @@ module Mustermann
|
|
|
148
155
|
end
|
|
149
156
|
|
|
150
157
|
if peek
|
|
151
|
-
matches = build_matches(string, params, all:, matched_length: position, post_match: string[position..], pre_match:
|
|
158
|
+
matches = build_matches(string, params, all:, matched_length: position, post_match: string[position..], pre_match: EMPTY_STRING)
|
|
152
159
|
return matches unless all
|
|
153
160
|
result.concat(matches)
|
|
154
161
|
end
|
|
@@ -158,11 +165,11 @@ module Mustermann
|
|
|
158
165
|
|
|
159
166
|
NIL_VALUES = [nil].freeze
|
|
160
167
|
|
|
161
|
-
def build_matches(string, params, all: false, matched_length: string.size, post_match:
|
|
168
|
+
def build_matches(string, params, all: false, matched_length: string.size, post_match: EMPTY_STRING, pre_match: EMPTY_STRING)
|
|
162
169
|
result = [] if all
|
|
163
170
|
matched = string[0, matched_length]
|
|
164
171
|
|
|
165
|
-
@patterns
|
|
172
|
+
@patterns&.each do |pattern|
|
|
166
173
|
next if pattern.except_regexp&.match?(matched)
|
|
167
174
|
|
|
168
175
|
pattern_params = build_pattern_params(pattern, params)
|
|
@@ -213,7 +220,7 @@ module Mustermann
|
|
|
213
220
|
@byte_lookup = nil
|
|
214
221
|
@stride = depth
|
|
215
222
|
@fast_static.each_value(&:optimize!)
|
|
216
|
-
elsif
|
|
223
|
+
elsif !@static&.any?
|
|
217
224
|
@fast_static = nil
|
|
218
225
|
@byte_lookup = nil
|
|
219
226
|
@stride = 1
|
|
@@ -221,12 +228,13 @@ module Mustermann
|
|
|
221
228
|
else
|
|
222
229
|
@fast_static = nil
|
|
223
230
|
@byte_lookup = Array.new(256)
|
|
224
|
-
@static.each { |k, v| @byte_lookup[k.getbyte(0)] = v }
|
|
225
231
|
@stride = 1
|
|
226
|
-
@static.
|
|
232
|
+
@static&.each { |k, v| @byte_lookup[k.getbyte(0)] = v }
|
|
233
|
+
@static&.each_value(&:optimize!)
|
|
227
234
|
end
|
|
228
|
-
|
|
229
|
-
@
|
|
235
|
+
|
|
236
|
+
@dynamic&.each_value(&:optimize!)
|
|
237
|
+
@dynamic_entries = @dynamic&.map do |matcher, node|
|
|
230
238
|
names = matcher.names.each(&:freeze)
|
|
231
239
|
# Detect unconstrained single-segment captures: can use fast string.index instead of regex.
|
|
232
240
|
# Two conditions: (1) the edge is a bare capture (source starts with \G(?<name>), no leading
|
|
@@ -237,6 +245,10 @@ module Mustermann
|
|
|
237
245
|
end
|
|
238
246
|
[matcher, node, names, fast]
|
|
239
247
|
end
|
|
248
|
+
|
|
249
|
+
@fast_static = Mustermann.dedup(@fast_static)
|
|
250
|
+
@dynamic_entries = Mustermann.dedup(@dynamic_entries)
|
|
251
|
+
@byte_lookup = Mustermann.dedup(@byte_lookup)
|
|
240
252
|
end
|
|
241
253
|
|
|
242
254
|
protected
|
|
@@ -245,9 +257,9 @@ module Mustermann
|
|
|
245
257
|
# all possible paths, before encountering a dynamic edge, a terminal pattern,
|
|
246
258
|
# or an empty node. Branching is allowed; only the minimum depth matters.
|
|
247
259
|
def min_static_depth
|
|
248
|
-
return 0 if @dynamic
|
|
249
|
-
return 0 if @patterns
|
|
250
|
-
return 0 if @static
|
|
260
|
+
return 0 if @dynamic&.any?
|
|
261
|
+
return 0 if @patterns&.any?
|
|
262
|
+
return 0 if @static&.empty?
|
|
251
263
|
1 + @static.values.map { |node| node.min_static_depth }.min
|
|
252
264
|
end
|
|
253
265
|
|
|
@@ -256,7 +268,7 @@ module Mustermann
|
|
|
256
268
|
# Builds a hash whose keys are +stride+-character strings and whose values are
|
|
257
269
|
# the trie nodes reached after consuming exactly those characters.
|
|
258
270
|
def build_stride_hash(stride)
|
|
259
|
-
stride.times.reduce({
|
|
271
|
+
stride.times.reduce({ EMPTY_STRING => self }) do |frontier, _|
|
|
260
272
|
frontier.each_with_object({}) do |(prefix, node), nxt|
|
|
261
273
|
node.static.each { |char, child| nxt[prefix + char] = child }
|
|
262
274
|
end
|
data/lib/mustermann/set.rb
CHANGED
|
@@ -104,6 +104,8 @@ module Mustermann
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
@options = Mustermann.dedup(@options)
|
|
108
|
+
|
|
107
109
|
update(mapping)
|
|
108
110
|
|
|
109
111
|
block.arity == 0 ? update(yield) : yield(self) if block
|
|
@@ -328,7 +330,7 @@ module Mustermann
|
|
|
328
330
|
# @return [Mustermann::Expander]
|
|
329
331
|
def expander(value = self)
|
|
330
332
|
@expanders[value] ||= begin
|
|
331
|
-
patterns = value == self ? @mapping.keys : @reverse_mapping[value] ||
|
|
333
|
+
patterns = value == self ? @mapping.keys : @reverse_mapping[value] || EMPTY_ARRAY
|
|
332
334
|
Mustermann::Expander.new(patterns, additional_values: @additional_values, **options)
|
|
333
335
|
end
|
|
334
336
|
end
|
|
@@ -373,7 +375,7 @@ module Mustermann
|
|
|
373
375
|
values = value
|
|
374
376
|
value = self unless @reverse_mapping.key? values
|
|
375
377
|
end
|
|
376
|
-
expander(value).expand(behavior || @additional_values, values ||
|
|
378
|
+
expander(value).expand(behavior || @additional_values, values || EMPTY_HASH)
|
|
377
379
|
end
|
|
378
380
|
|
|
379
381
|
# @return [Boolean] whether the set contains any pattern associated with the given value
|
data/lib/mustermann/sinatra.rb
CHANGED
|
@@ -89,7 +89,7 @@ module Mustermann
|
|
|
89
89
|
def normalize_capture(pattern)
|
|
90
90
|
case capture = pattern.options[:capture]
|
|
91
91
|
when Hash then capture.slice(*pattern.names.map(&:to_sym))
|
|
92
|
-
when nil then
|
|
92
|
+
when nil then EMPTY_HASH
|
|
93
93
|
else pattern.names.to_h { |name| [name.to_sym, capture] }
|
|
94
94
|
end
|
|
95
95
|
end
|
data/lib/mustermann/version.rb
CHANGED
data/lib/mustermann.rb
CHANGED
|
@@ -113,6 +113,47 @@ module Mustermann
|
|
|
113
113
|
|
|
114
114
|
# @!visibility private
|
|
115
115
|
def self.normalized_type(type)
|
|
116
|
-
|
|
116
|
+
@normalized_types ||= {}
|
|
117
|
+
@normalized_types[type] ||= type.to_s.gsub('-', '_').downcase
|
|
117
118
|
end
|
|
119
|
+
|
|
120
|
+
# @!visibility private
|
|
121
|
+
def self.dedup(object)
|
|
122
|
+
return object.freeze unless @dedup
|
|
123
|
+
# need to check this first because JRuby raises on @dedup.key?(nil)
|
|
124
|
+
case object
|
|
125
|
+
when nil, true, false, Integer, Float, Symbol, Module
|
|
126
|
+
return object
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
canonical = @dedup.getkey(object)
|
|
130
|
+
return canonical if canonical
|
|
131
|
+
|
|
132
|
+
# Build deduplicated copies so callers' arrays and hashes, which may be
|
|
133
|
+
# frozen or still in use, are never modified.
|
|
134
|
+
case object
|
|
135
|
+
when Array
|
|
136
|
+
object = object.map { |o| dedup(o) }
|
|
137
|
+
@dedup[object.freeze] = true
|
|
138
|
+
when Hash
|
|
139
|
+
object = object.to_h { |k, v| [dedup(k), dedup(v)] }
|
|
140
|
+
@dedup[object.freeze] = true
|
|
141
|
+
when String
|
|
142
|
+
@dedup[-object] = true
|
|
143
|
+
else
|
|
144
|
+
@dedup[object.freeze] = true
|
|
145
|
+
end
|
|
146
|
+
object
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@dedup = defined?(ObjectSpace::WeakKeyMap) ? ObjectSpace::WeakKeyMap.new : false
|
|
150
|
+
|
|
151
|
+
# @!visibility private
|
|
152
|
+
EMPTY_STRING = dedup(-'')
|
|
153
|
+
|
|
154
|
+
# @!visibility private
|
|
155
|
+
EMPTY_ARRAY = dedup([])
|
|
156
|
+
|
|
157
|
+
# @!visibility private
|
|
158
|
+
EMPTY_HASH = dedup({})
|
|
118
159
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mustermann
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.
|
|
4
|
+
version: 4.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Haase
|
|
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
89
89
|
- !ruby/object:Gem::Version
|
|
90
90
|
version: '0'
|
|
91
91
|
requirements: []
|
|
92
|
-
rubygems_version: 4.0.
|
|
92
|
+
rubygems_version: 4.0.20
|
|
93
93
|
specification_version: 4
|
|
94
94
|
summary: Your personal string matching expert.
|
|
95
95
|
test_files: []
|