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.
- checksums.yaml +4 -4
- data/LICENSE +1 -2
- data/README.md +238 -261
- data/lib/mustermann/ast/compiler.rb +137 -29
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/expander.rb +6 -7
- 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 +29 -6
- data/lib/mustermann/ast/template_generator.rb +2 -2
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/ast/translator.rb +14 -5
- data/lib/mustermann/ast/validation.rb +1 -1
- data/lib/mustermann/composite.rb +26 -7
- data/lib/mustermann/concat.rb +17 -6
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +28 -6
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/identity.rb +1 -1
- data/lib/mustermann/match.rb +155 -0
- data/lib/mustermann/pattern.rb +30 -35
- data/lib/mustermann/rails.rb +63 -0
- data/lib/mustermann/regexp_based.rb +70 -9
- data/lib/mustermann/regular.rb +1 -1
- data/lib/mustermann/router.rb +104 -0
- data/lib/mustermann/set/cache.rb +47 -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 +282 -0
- data/lib/mustermann/set.rb +447 -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 +38 -12
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'mustermann/ast/translator'
|
|
4
|
+
require 'mustermann/ast/converters'
|
|
3
5
|
|
|
4
6
|
module Mustermann
|
|
5
7
|
# @see Mustermann::AST::Pattern
|
|
@@ -9,28 +11,62 @@ module Mustermann
|
|
|
9
11
|
class Compiler < Translator
|
|
10
12
|
raises CompileError
|
|
11
13
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
+
# Compile an array of AST nodes, detecting which captures can safely use
|
|
15
|
+
# atomic groups. A capture is safe to atomicize when its very next sibling
|
|
16
|
+
# is a *path separator* (payload '/'), because every Mustermann capture
|
|
17
|
+
# character class (Sinatra's [^\/\?#]+, Template's [\w\-\.~%]+, etc.)
|
|
18
|
+
# excludes '/', so the greedy match naturally stops before '/' and
|
|
19
|
+
# committing to it atomically never affects correctness.
|
|
20
|
+
#
|
|
21
|
+
# More permissive conditions (e.g. end-of-array) are intentionally avoided:
|
|
22
|
+
# template expressions nest captures inside inner arrays where end-of-array
|
|
23
|
+
# does NOT mean end-of-pattern, and non-'/' separators (e.g. '.' in
|
|
24
|
+
# {.a,b,c}) may appear inside the capture character class.
|
|
25
|
+
#
|
|
26
|
+
# Splats (.*?) and non-greedy captures are excluded — atomicizing them
|
|
27
|
+
# would commit to zero or one character and break match resolution.
|
|
28
|
+
# Strip `atomic:` from incoming options so a parent's value cannot bleed
|
|
29
|
+
# into siblings; each element's atomicity comes solely from its own context.
|
|
30
|
+
translate(Array) do |atomic: false, **options|
|
|
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]
|
|
36
|
+
atomic = greedy &&
|
|
37
|
+
element.is_a?(:capture) &&
|
|
38
|
+
!element.is_a?(:splat) &&
|
|
39
|
+
next_sibling&.is_a?(:separator) &&
|
|
40
|
+
next_sibling.payload == '/'
|
|
41
|
+
result << t(element, **options, atomic: atomic)
|
|
42
|
+
end
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
14
46
|
translate(:node) { |**o| t(payload, **o) }
|
|
15
47
|
translate(:separator) { |**o| Regexp.escape(payload) }
|
|
16
|
-
translate(:optional) { |**o|
|
|
48
|
+
translate(:optional) { |**o| '(?:%s)?' % t(payload, **o) }
|
|
17
49
|
translate(:char) { |**o| t.encoded(payload, **o) }
|
|
18
50
|
|
|
19
51
|
translate :union do |**options|
|
|
20
|
-
|
|
52
|
+
'(?:%s)' % payload.map { |e| '(?:%s)' % t(e, **options) }.join('|')
|
|
21
53
|
end
|
|
22
54
|
|
|
23
55
|
translate :expression do |greedy: true, **options|
|
|
24
56
|
t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
|
25
|
-
|
|
57
|
+
parametric: operator.parametric, separator: operator.separator, **options)
|
|
26
58
|
end
|
|
27
59
|
|
|
28
|
-
translate :with_look_ahead do
|
|
29
|
-
|
|
60
|
+
translate :with_look_ahead do |atomic: false, **options|
|
|
61
|
+
greedy = options.fetch(:greedy, true)
|
|
62
|
+
lookahead = each_leaf.inject(EMPTY_STRING) do |ahead, element|
|
|
30
63
|
ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s
|
|
31
64
|
end
|
|
32
65
|
lookahead << (at_end ? '$' : '/')
|
|
33
|
-
|
|
66
|
+
# The look-ahead already constrains what the head capture can match, so
|
|
67
|
+
# it is safe to make it atomic when greedy. Non-greedy captures rely on
|
|
68
|
+
# backtracking to extend their match and must not be committed atomically.
|
|
69
|
+
t(head, **options, lookahead: lookahead, atomic: greedy) + t(payload, **options)
|
|
34
70
|
end
|
|
35
71
|
|
|
36
72
|
# Capture compilation is complex. :(
|
|
@@ -39,9 +75,23 @@ module Mustermann
|
|
|
39
75
|
register :capture
|
|
40
76
|
|
|
41
77
|
# @!visibility private
|
|
42
|
-
|
|
78
|
+
# When +atomic: true+ is passed (set by the Array translator for captures
|
|
79
|
+
# that are followed only by a separator or end-of-pattern), the compiled
|
|
80
|
+
# content is wrapped in an atomic group <tt>(?>…)</tt>. This prevents
|
|
81
|
+
# Oniguruma from backtracking into characters the capture has already
|
|
82
|
+
# consumed, giving a measurable speedup on failing matches without
|
|
83
|
+
# changing the result for any valid input.
|
|
84
|
+
def translate(atomic: false, **options)
|
|
43
85
|
return pattern(**options) if options[:no_captures]
|
|
44
|
-
|
|
86
|
+
|
|
87
|
+
inner = translate(no_captures: true, **options)
|
|
88
|
+
# Atomic groups are only safe for pure character-class repetitions.
|
|
89
|
+
# Captures with an explicit array/hash/string option or a custom
|
|
90
|
+
# constraint produce alternations that need backtracking to resolve
|
|
91
|
+
# the correct alternative, so they must not be wrapped atomically.
|
|
92
|
+
apply_atomic = atomic && options[:capture].nil? && constraint.nil?
|
|
93
|
+
content = apply_atomic ? "(?>#{inner})" : inner
|
|
94
|
+
"(?<#{name}>#{content})"
|
|
45
95
|
end
|
|
46
96
|
|
|
47
97
|
# @return [String] regexp without the named capture
|
|
@@ -53,19 +103,51 @@ module Mustermann
|
|
|
53
103
|
when Hash then from_hash(capture, **options)
|
|
54
104
|
when String then from_string(capture, **options)
|
|
55
105
|
when nil then from_nil(**options)
|
|
56
|
-
|
|
106
|
+
when Regexp then capture
|
|
107
|
+
when Class then from_class(capture, **options)
|
|
108
|
+
else raise CompileError, "invalid capture constraint %p for %p" % [capture, name]
|
|
57
109
|
end
|
|
58
110
|
end
|
|
59
111
|
|
|
60
112
|
private
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
113
|
+
|
|
114
|
+
def qualified(string, greedy: true, **options)
|
|
115
|
+
"#{string}#{qualifier || "+#{'?' unless greedy}"}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def with_lookahead(string, lookahead: nil, **options)
|
|
119
|
+
lookahead ? "(?:(?!#{lookahead})#{string})" : string
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def from_hash(hash, **options)
|
|
123
|
+
pattern(capture: hash[name.to_sym], **options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def from_array(array, **options)
|
|
127
|
+
Regexp.union(*array.map { |e| pattern(capture: e, **options) })
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def from_symbol(symbol, **options)
|
|
131
|
+
capture, _ = CONVERTERS[symbol]
|
|
132
|
+
return pattern(capture:, **options) if capture
|
|
133
|
+
qualified(with_lookahead("[[:#{symbol}:]]", **options), **options)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def from_string(string, **options)
|
|
137
|
+
Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def from_nil(**options)
|
|
141
|
+
qualified(with_lookahead(default(**options), **options), **options)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def from_class(klass, **options)
|
|
145
|
+
capture, _ = CONVERTERS[klass.name]
|
|
146
|
+
raise CompileError, "no converter for class %p" % klass unless capture
|
|
147
|
+
pattern(capture:, **options)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def default(**options) = constraint || '[^/\\?#]'
|
|
69
151
|
end
|
|
70
152
|
|
|
71
153
|
# @!visibility private
|
|
@@ -74,7 +156,7 @@ module Mustermann
|
|
|
74
156
|
# splats are always non-greedy
|
|
75
157
|
# @!visibility private
|
|
76
158
|
def pattern(**options)
|
|
77
|
-
constraint ||
|
|
159
|
+
constraint || '.*?'
|
|
78
160
|
end
|
|
79
161
|
end
|
|
80
162
|
|
|
@@ -83,11 +165,17 @@ module Mustermann
|
|
|
83
165
|
register :variable
|
|
84
166
|
|
|
85
167
|
# @!visibility private
|
|
86
|
-
def translate(**options)
|
|
87
|
-
|
|
168
|
+
def translate(atomic: false, **options)
|
|
169
|
+
# Exploded variables expand to `pattern(?:sep pattern)*`. The engine
|
|
170
|
+
# must be able to backtrack through that repetition when a following
|
|
171
|
+
# capture (e.g. the 'b' in {/a*,b}) needs to claim the last segment.
|
|
172
|
+
# Strip `atomic:` so Capture#translate never wraps the repetition.
|
|
173
|
+
effective_atomic = atomic && !explode
|
|
174
|
+
return super(atomic: effective_atomic, **options) if explode or !options[:parametric]
|
|
175
|
+
|
|
88
176
|
# Remove this line after fixing broken compatibility between 2.1 and 2.2
|
|
89
177
|
options.delete(:parametric) if options.has_key?(:parametric)
|
|
90
|
-
parametric super(parametric: false, **options)
|
|
178
|
+
parametric super(atomic: effective_atomic, parametric: false, **options)
|
|
91
179
|
end
|
|
92
180
|
|
|
93
181
|
# @!visibility private
|
|
@@ -117,21 +205,41 @@ module Mustermann
|
|
|
117
205
|
# @!visibility private
|
|
118
206
|
def register_param(parametric: false, split_params: nil, separator: nil, **options)
|
|
119
207
|
return unless explode and split_params
|
|
208
|
+
|
|
120
209
|
split_params[name] = { separator: separator, parametric: parametric }
|
|
121
210
|
end
|
|
122
211
|
end
|
|
123
212
|
|
|
213
|
+
# @return [Array<String>] all raw string representations of the character (literal + URI-encoded variants)
|
|
214
|
+
# @!visibility private
|
|
215
|
+
def self.char_representations(char, uri_decode: true, space_matches_plus: true)
|
|
216
|
+
if char == ' ' and space_matches_plus
|
|
217
|
+
@space_and_plus ||= char_representations(' ', space_matches_plus: false) +
|
|
218
|
+
char_representations('+', space_matches_plus: false)
|
|
219
|
+
else
|
|
220
|
+
@char_representations ||= {}
|
|
221
|
+
@char_representations[char] ||= begin
|
|
222
|
+
escaped = URI_PARSER.escape(char, /./)
|
|
223
|
+
[char, escaped.upcase, escaped.downcase].uniq
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
124
228
|
# @return [String] Regular expression for matching the given character in all representations
|
|
125
229
|
# @!visibility private
|
|
126
230
|
def encoded(char, uri_decode: true, space_matches_plus: true, **options)
|
|
127
231
|
return Regexp.escape(char) unless uri_decode
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
list << " "
|
|
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
|
|
133
236
|
end
|
|
134
|
-
|
|
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 ||= {}
|
|
135
243
|
end
|
|
136
244
|
|
|
137
245
|
# Compiles an AST to a regular expression.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "rubygems/version"
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
module AST
|
|
7
|
+
CONVERTERS = {
|
|
8
|
+
"Integer" => [ /-?\d+/, :to_i ],
|
|
9
|
+
"Symbol" => [ /\w+/, :to_sym ],
|
|
10
|
+
"String" => [ nil, :to_s ],
|
|
11
|
+
"Float" => [ /-?\d+(?:\.\d+)?/, :to_f ],
|
|
12
|
+
|
|
13
|
+
"Date" => [
|
|
14
|
+
/\d{4}-\d{2}-\d{2}/,
|
|
15
|
+
->(string) { Date.parse(string) }
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
"Gem::Version" => [
|
|
19
|
+
Regexp.new(Gem::Version::VERSION_PATTERN),
|
|
20
|
+
->(string) { Gem::Version.new(string) }
|
|
21
|
+
],
|
|
22
|
+
|
|
23
|
+
locale: [ /(?:[A-Za-z]{2,3}|i)(-[A-Za-z0-9]{1,8})*/ ],
|
|
24
|
+
slug: [ /[a-z0-9]+(?:-[a-z0-9]+)*/ ],
|
|
25
|
+
uuid: [ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i ],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
CONVERTERS.merge!({
|
|
29
|
+
integer: CONVERTERS["Integer"],
|
|
30
|
+
symbol: CONVERTERS["Symbol"],
|
|
31
|
+
string: CONVERTERS["String"],
|
|
32
|
+
float: CONVERTERS["Float"],
|
|
33
|
+
date: CONVERTERS["Date"],
|
|
34
|
+
version: CONVERTERS["Gem::Version"],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
CONVERTERS.freeze
|
|
38
|
+
|
|
39
|
+
private_constant :CONVERTERS
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mustermann/ast/translator'
|
|
3
3
|
require 'mustermann/ast/compiler'
|
|
4
|
-
require 'ruby2_keywords'
|
|
5
4
|
|
|
6
5
|
module Mustermann
|
|
7
6
|
module AST
|
|
@@ -12,11 +11,11 @@ module Mustermann
|
|
|
12
11
|
class Expander < Translator
|
|
13
12
|
raises ExpandError
|
|
14
13
|
|
|
15
|
-
translate
|
|
14
|
+
translate Array do |*args, **options|
|
|
16
15
|
inject(t.pattern) do |pattern, element|
|
|
17
|
-
t.add_to(pattern, t(element, *args))
|
|
16
|
+
t.add_to(pattern, t(element, *args, **options))
|
|
18
17
|
end
|
|
19
|
-
end
|
|
18
|
+
end
|
|
20
19
|
|
|
21
20
|
translate :capture do |**options|
|
|
22
21
|
t.for_capture(node, **options)
|
|
@@ -123,7 +122,7 @@ module Mustermann
|
|
|
123
122
|
|
|
124
123
|
# @see Mustermann::AST::Translator#expand
|
|
125
124
|
# @!visibility private
|
|
126
|
-
|
|
125
|
+
def escape(string, *args, **options)
|
|
127
126
|
return super unless string.respond_to?(:=~)
|
|
128
127
|
|
|
129
128
|
# URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
|
|
@@ -132,14 +131,14 @@ module Mustermann
|
|
|
132
131
|
|
|
133
132
|
# Turns a sprintf pattern into our secret internal data structure.
|
|
134
133
|
# @!visibility private
|
|
135
|
-
def pattern(string =
|
|
134
|
+
def pattern(string = EMPTY_STRING, *keys, **filters)
|
|
136
135
|
[[keys, string, filters]]
|
|
137
136
|
end
|
|
138
137
|
|
|
139
138
|
# Creates the product of two of our secret internal data structures.
|
|
140
139
|
# @!visibility private
|
|
141
140
|
def add_to(list, result)
|
|
142
|
-
list << [[],
|
|
141
|
+
list << [[], EMPTY_STRING] if list.empty?
|
|
143
142
|
list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } }
|
|
144
143
|
end
|
|
145
144
|
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
module AST
|
|
5
|
+
# Mixin for AST::Pattern subclasses that accelerates compilation and AST
|
|
6
|
+
# construction for "simple" patterns: only static path segments and
|
|
7
|
+
# unconstrained full-segment captures (e.g. /foo/:bar/baz/:id).
|
|
8
|
+
# Patterns with optional groups, constraints, or non-default options fall
|
|
9
|
+
# through to the full AST pipeline.
|
|
10
|
+
module FastPattern
|
|
11
|
+
# Matches patterns that consist only of slashes, static segments, and
|
|
12
|
+
# simple :name captures — no optional groups, no constraints.
|
|
13
|
+
SIMPLE = /\A(?:\/(?:[a-zA-Z0-9\-_.~]+|:[a-zA-Z_]\w*))+\z/
|
|
14
|
+
|
|
15
|
+
# Regexp fragment for each printable ASCII char, matching the same output
|
|
16
|
+
# as Compiler#encoded with uri_decode: true.
|
|
17
|
+
ENCODED = Mustermann.dedup((0..127).each_with_object({}) do |byte, h|
|
|
18
|
+
c = byte.chr
|
|
19
|
+
pct = '%%%02X' % byte
|
|
20
|
+
reps = [c, pct, pct.downcase].uniq
|
|
21
|
+
h[c] = reps.size == 1 ? Regexp.escape(reps.first) :
|
|
22
|
+
'(?:%s)' % reps.map { |r| Regexp.escape(r) }.join('|')
|
|
23
|
+
end)
|
|
24
|
+
|
|
25
|
+
SEGMENT_SCAN = %r{(/)|(:[a-zA-Z_]\w*)|([^/:]+)}
|
|
26
|
+
|
|
27
|
+
private_constant :SIMPLE, :ENCODED, :SEGMENT_SCAN
|
|
28
|
+
|
|
29
|
+
# Public override: fast path for simple patterns, falls through to super otherwise.
|
|
30
|
+
# Must remain public to match AST::Pattern#to_ast visibility.
|
|
31
|
+
def to_ast
|
|
32
|
+
return super unless simple_pattern?
|
|
33
|
+
ast = self.class.ast_cache.fetch(@string) { build_fast_ast }
|
|
34
|
+
@param_converters ||= EMPTY_HASH
|
|
35
|
+
ast
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def params(string = nil)
|
|
39
|
+
return super unless @fast_match
|
|
40
|
+
return unless md = @regexp.match(string)
|
|
41
|
+
result = md.named_captures
|
|
42
|
+
result.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%')
|
|
43
|
+
result
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_match(regexp, string)
|
|
49
|
+
return super unless @fast_match
|
|
50
|
+
return unless match = regexp.match(string)
|
|
51
|
+
params = match.named_captures
|
|
52
|
+
params.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%')
|
|
53
|
+
Match.new(self, match, params: params)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def simple_pattern?
|
|
57
|
+
options[:capture].nil? &&
|
|
58
|
+
options[:except].nil? &&
|
|
59
|
+
options.fetch(:greedy, true) != false &&
|
|
60
|
+
uri_decode &&
|
|
61
|
+
@string.match?(SIMPLE)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def compile(**options)
|
|
65
|
+
return super unless simple_pattern?
|
|
66
|
+
result = fast_compile
|
|
67
|
+
@fast_match = true
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fast_compile
|
|
72
|
+
tokens = @string.scan(SEGMENT_SCAN)
|
|
73
|
+
src = String.new
|
|
74
|
+
tokens.each_with_index do |(sep, cap, chars), i|
|
|
75
|
+
if sep
|
|
76
|
+
src << '\\/'
|
|
77
|
+
elsif cap
|
|
78
|
+
# Mirror the compiler: wrap in atomic group when the next token is a separator.
|
|
79
|
+
if tokens[i + 1]&.first
|
|
80
|
+
src << "(?<#{cap[1..]}>(?>[^/\\?#]+))"
|
|
81
|
+
else
|
|
82
|
+
src << "(?<#{cap[1..]}>[^/\\?#]+)"
|
|
83
|
+
end
|
|
84
|
+
else
|
|
85
|
+
chars.each_char { |c| src << ENCODED[c] }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
Regexp.new(src)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_fast_ast
|
|
92
|
+
nodes = []
|
|
93
|
+
pos = 0
|
|
94
|
+
@string.scan(SEGMENT_SCAN) do |sep, cap, chars|
|
|
95
|
+
if sep
|
|
96
|
+
node = Node::Separator.new('/')
|
|
97
|
+
node.start, node.stop = pos, pos + 1
|
|
98
|
+
nodes << node
|
|
99
|
+
pos += 1
|
|
100
|
+
elsif cap
|
|
101
|
+
node = Node::Capture.new(cap[1..])
|
|
102
|
+
node.start, node.stop = pos, pos + cap.length
|
|
103
|
+
nodes << node
|
|
104
|
+
pos += cap.length
|
|
105
|
+
else
|
|
106
|
+
chars.each_char do |c|
|
|
107
|
+
node = Node::Char.new(c)
|
|
108
|
+
node.start, node.stop = pos, pos + 1
|
|
109
|
+
nodes << node
|
|
110
|
+
pos += 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
root = Node::Root.new
|
|
115
|
+
root.payload = nodes
|
|
116
|
+
root.pattern = @string
|
|
117
|
+
root.start, root.stop = 0, @string.length
|
|
118
|
+
root
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/ast/converters'
|
|
2
3
|
require 'mustermann/ast/translator'
|
|
3
4
|
|
|
4
5
|
module Mustermann
|
|
@@ -8,14 +9,45 @@ module Mustermann
|
|
|
8
9
|
# @see Mustermann::AST::Pattern#to_templates
|
|
9
10
|
class ParamScanner < Translator
|
|
10
11
|
# @!visibility private
|
|
11
|
-
def self.scan_params(ast)
|
|
12
|
-
new.translate(ast)
|
|
12
|
+
def self.scan_params(ast, options)
|
|
13
|
+
new.translate(ast, options)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
translate(:node)
|
|
16
|
-
translate(
|
|
17
|
-
translate(
|
|
18
|
-
translate(
|
|
16
|
+
translate(:node) { |o| t(payload, o) }
|
|
17
|
+
translate(:with_look_ahead) { |o| t(head, o).merge(t(payload, o)) }
|
|
18
|
+
translate(Array) { |o| map { |e| t(e, o) }.inject(:merge) }
|
|
19
|
+
translate(Object) { |o| EMPTY_HASH }
|
|
20
|
+
|
|
21
|
+
class Capture < NodeTranslator
|
|
22
|
+
register :capture
|
|
23
|
+
|
|
24
|
+
def translate(options)
|
|
25
|
+
return { name => convert } if convert
|
|
26
|
+
_, converter = converter(options[:capture])
|
|
27
|
+
converter ? { name => converter } : EMPTY_HASH
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def converter(capture)
|
|
31
|
+
case capture
|
|
32
|
+
when Hash then return converter(capture[name.to_sym])
|
|
33
|
+
when Class then regexp, converter = CONVERTERS[capture.name]
|
|
34
|
+
when Symbol then regexp, converter = CONVERTERS[capture]
|
|
35
|
+
when Array
|
|
36
|
+
entries = capture.map { |item| converter(item) }.compact
|
|
37
|
+
regexp = Regexp.union(entries.map(&:first))
|
|
38
|
+
|
|
39
|
+
entries.map! { |r, c| [/\A#{r}\Z/, c] }
|
|
40
|
+
|
|
41
|
+
converter = ->(string) do
|
|
42
|
+
_, c = entries.find { |r, _| r.match?(string) }
|
|
43
|
+
c&.call(string) || string
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
return unless converter
|
|
48
|
+
[regexp, converter.to_proc]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
19
51
|
end
|
|
20
52
|
end
|
|
21
53
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mustermann/ast/node'
|
|
3
3
|
require 'forwardable'
|
|
4
|
-
require 'ruby2_keywords'
|
|
5
4
|
require 'strscan'
|
|
6
5
|
|
|
7
6
|
module Mustermann
|
|
@@ -65,10 +64,10 @@ module Mustermann
|
|
|
65
64
|
# @param [Symbol] type node type
|
|
66
65
|
# @return [Mustermann::AST::Node]
|
|
67
66
|
# @!visibility private
|
|
68
|
-
|
|
67
|
+
def node(type, *args, **options, &block)
|
|
69
68
|
type = Node[type] unless type.respond_to? :new
|
|
70
69
|
start = pos
|
|
71
|
-
node = block ? type.parse(*args, &block) : type.new(*args)
|
|
70
|
+
node = block ? type.parse(*args, **options, &block) : type.new(*args, **options)
|
|
72
71
|
min_size(start, pos, node)
|
|
73
72
|
end
|
|
74
73
|
|
|
@@ -23,6 +23,11 @@ module Mustermann
|
|
|
23
23
|
instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class'
|
|
24
24
|
instance_delegate parse: :parser, transform: :transformer, validate: :validation,
|
|
25
25
|
generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries
|
|
26
|
+
|
|
27
|
+
# @api private
|
|
28
|
+
def self.ast_cache
|
|
29
|
+
@ast_cache ||= EqualityMap.new
|
|
30
|
+
end
|
|
26
31
|
|
|
27
32
|
# @api private
|
|
28
33
|
# @return [#parse] parser object for pattern
|
|
@@ -83,11 +88,23 @@ module Mustermann
|
|
|
83
88
|
raise error.class, "#{error.message}: #{@string.inspect}", error.backtrace
|
|
84
89
|
end
|
|
85
90
|
|
|
91
|
+
# Returns a regexp that matches strings excluded by the +except+ option,
|
|
92
|
+
# or +nil+ if no +except+ constraint was given. Used by the trie matcher
|
|
93
|
+
# to filter out excluded strings at leaf nodes.
|
|
94
|
+
# @return [Regexp, nil]
|
|
95
|
+
# @!visibility private
|
|
96
|
+
def except_regexp
|
|
97
|
+
return unless except_str = options[:except]
|
|
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
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
# Internal AST representation of pattern.
|
|
87
105
|
# @!visibility private
|
|
88
106
|
def to_ast
|
|
89
|
-
@
|
|
90
|
-
@ast_cache.fetch(@string) do
|
|
107
|
+
self.class.ast_cache.fetch([@string, options]) do
|
|
91
108
|
ast = parse(@string, pattern: self)
|
|
92
109
|
ast &&= transform(ast)
|
|
93
110
|
ast &&= set_boundaries(ast, string: @string)
|
|
@@ -103,7 +120,7 @@ module Mustermann
|
|
|
103
120
|
# @raise (see Mustermann::Pattern#expand)
|
|
104
121
|
# @see Mustermann::Pattern#expand
|
|
105
122
|
# @see Mustermann::Expander
|
|
106
|
-
def expand(behavior = nil, values =
|
|
123
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
107
124
|
@expander ||= Mustermann::Expander.new(self)
|
|
108
125
|
@expander.expand(behavior, values)
|
|
109
126
|
end
|
|
@@ -115,19 +132,25 @@ module Mustermann
|
|
|
115
132
|
# @return (see Mustermann::Pattern#to_templates)
|
|
116
133
|
# @see Mustermann::Pattern#to_templates
|
|
117
134
|
def to_templates
|
|
118
|
-
@to_templates ||= generate_templates(to_ast)
|
|
135
|
+
@to_templates ||= Mustermann.dedup(generate_templates(to_ast))
|
|
119
136
|
end
|
|
120
137
|
|
|
121
138
|
# @!visibility private
|
|
122
139
|
# @see Mustermann::Pattern#map_param
|
|
123
140
|
def map_param(key, value)
|
|
124
141
|
return super unless param_converters.include? key
|
|
125
|
-
|
|
142
|
+
converted = super
|
|
143
|
+
converted.nil? ? converted : param_converters[key][converted]
|
|
126
144
|
end
|
|
127
145
|
|
|
128
146
|
# @!visibility private
|
|
129
147
|
def param_converters
|
|
130
|
-
@param_converters ||= scan_params(to_ast)
|
|
148
|
+
@param_converters ||= Mustermann.dedup(scan_params(to_ast, options))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @api private
|
|
152
|
+
def identity_params?(params)
|
|
153
|
+
param_converters.empty? && super
|
|
131
154
|
end
|
|
132
155
|
|
|
133
156
|
private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
|
|
@@ -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) } }
|
|
@@ -22,6 +22,13 @@ module Mustermann
|
|
|
22
22
|
node
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# eliminate redundant optional nodes - this helps avoid regexp warnings
|
|
26
|
+
translate(:optional) do
|
|
27
|
+
return t(payload) if payload.is_a? Node[:optional]
|
|
28
|
+
node.payload = t(payload)
|
|
29
|
+
node
|
|
30
|
+
end
|
|
31
|
+
|
|
25
32
|
# ignore unknown objects on the tree
|
|
26
33
|
translate(Object) { node }
|
|
27
34
|
|