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
data/lib/mustermann/composite.rb
CHANGED
|
@@ -29,6 +29,11 @@ module Mustermann
|
|
|
29
29
|
@patterns = patterns.flat_map { |p| patterns_from(p, **options) }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# @see Mustermann::Pattern#names
|
|
33
|
+
def names
|
|
34
|
+
@names ||= patterns.flat_map { |p| p.respond_to?(:names) ? p.names : [] }.uniq
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
# @see Mustermann::Pattern#==
|
|
33
38
|
def ==(pattern)
|
|
34
39
|
patterns == patterns_from(pattern)
|
|
@@ -79,19 +84,33 @@ module Mustermann
|
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
# @return [String] the string representation of the pattern
|
|
82
|
-
def to_s
|
|
83
|
-
simple_inspect
|
|
84
|
-
end
|
|
87
|
+
def to_s = inspect
|
|
85
88
|
|
|
86
89
|
# @!visibility private
|
|
87
90
|
def inspect
|
|
88
|
-
"
|
|
91
|
+
"(#{simple_inspect})"
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
# @!visibility private
|
|
92
95
|
def simple_inspect
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
patterns.map { |p| p.is_a?(Composite) ? p.inspect : p.simple_inspect }.join(" #{operator} ")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @!visibility private
|
|
100
|
+
def pretty_print(q)
|
|
101
|
+
q.group(1, "(", ")") do
|
|
102
|
+
patterns.each_with_index do |pattern, index|
|
|
103
|
+
unless index == 0
|
|
104
|
+
q.text " #{operator}"
|
|
105
|
+
q.breakable " "
|
|
106
|
+
end
|
|
107
|
+
if pattern.is_a?(Composite)
|
|
108
|
+
q.pp pattern
|
|
109
|
+
else
|
|
110
|
+
q.text pattern.simple_inspect
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
95
114
|
end
|
|
96
115
|
|
|
97
116
|
# @!visibility private
|
data/lib/mustermann/concat.rb
CHANGED
|
@@ -14,8 +14,8 @@ module Mustermann
|
|
|
14
14
|
concat = (self + patterns.inject(:+))
|
|
15
15
|
concat + other.patterns.slice(patterns.length..-1).inject(:+)
|
|
16
16
|
else
|
|
17
|
-
|
|
18
|
-
self.class.new(native, **options)
|
|
17
|
+
native, opts = native_concat(other)
|
|
18
|
+
native ? self.class.new(native, **options, **opts.to_h) : super
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -75,10 +75,21 @@ module Mustermann
|
|
|
75
75
|
|
|
76
76
|
# @see Mustermann::Pattern#peek_match
|
|
77
77
|
def peek_match(string)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
post_match = string
|
|
79
|
+
params = {}
|
|
80
|
+
captures = []
|
|
81
|
+
named_captures = {}
|
|
82
|
+
|
|
83
|
+
patterns.each do |pattern|
|
|
84
|
+
return unless part = pattern.peek_match(post_match)
|
|
85
|
+
params.merge!(part.params)
|
|
86
|
+
named_captures.merge!(part.named_captures)
|
|
87
|
+
captures.concat(part.captures)
|
|
88
|
+
post_match = post_match[part.to_s.size..-1]
|
|
81
89
|
end
|
|
90
|
+
|
|
91
|
+
matched = string[0, string.size - post_match.size]
|
|
92
|
+
Match.new(self, string, matched:, params:, captures:, named_captures:, post_match:)
|
|
82
93
|
end
|
|
83
94
|
|
|
84
95
|
# @see Mustermann::Pattern#peek_params
|
data/lib/mustermann/error.rb
CHANGED
|
@@ -5,5 +5,6 @@ module Mustermann
|
|
|
5
5
|
CompileError = Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}.
|
|
6
6
|
ParseError = Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}.
|
|
7
7
|
ExpandError = Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}.
|
|
8
|
+
TrieError = Class.new(CompileError) # Raised if anything goes wrong while compiling a {Trie}.
|
|
8
9
|
end
|
|
9
10
|
end
|
data/lib/mustermann/expander.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann'
|
|
2
3
|
require 'mustermann/ast/expander'
|
|
3
4
|
require 'mustermann/caster'
|
|
4
|
-
require 'mustermann'
|
|
5
5
|
|
|
6
6
|
module Mustermann
|
|
7
7
|
# Allows fine-grained control over pattern expansion.
|
|
@@ -13,15 +13,16 @@ module Mustermann
|
|
|
13
13
|
#
|
|
14
14
|
# expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5"
|
|
15
15
|
class Expander
|
|
16
|
+
# @!visibility private
|
|
17
|
+
ADDITIONAL_VALUES = %i[raise ignore append].freeze
|
|
18
|
+
|
|
16
19
|
attr_reader :patterns, :additional_values, :caster
|
|
17
20
|
|
|
18
21
|
# @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}.
|
|
19
22
|
# @param [Symbol] additional_values behavior when encountering additional values, see {#expand}.
|
|
20
23
|
# @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}.
|
|
21
24
|
def initialize(*patterns, additional_values: :raise, **options, &block)
|
|
22
|
-
|
|
23
|
-
raise ArgumentError, "Illegal value %p for additional_values" % additional_values
|
|
24
|
-
end
|
|
25
|
+
raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless ADDITIONAL_VALUES.include? additional_values
|
|
25
26
|
|
|
26
27
|
@patterns = []
|
|
27
28
|
@api_expander = AST::Expander.new
|
|
@@ -42,6 +43,10 @@ module Mustermann
|
|
|
42
43
|
# @return [Mustermann::Expander] the expander
|
|
43
44
|
def add(*patterns)
|
|
44
45
|
patterns.each do |pattern|
|
|
46
|
+
if pattern.is_a? Expander
|
|
47
|
+
add(*pattern.patterns)
|
|
48
|
+
next
|
|
49
|
+
end
|
|
45
50
|
pattern = Mustermann.new(pattern, **@options)
|
|
46
51
|
if block_given?
|
|
47
52
|
@api_expander.add(yield(pattern))
|
|
@@ -154,6 +159,23 @@ module Mustermann
|
|
|
154
159
|
end
|
|
155
160
|
end
|
|
156
161
|
|
|
162
|
+
# @!visibility private
|
|
163
|
+
def inspect
|
|
164
|
+
return "#<#{self.class.name}>" if @patterns.empty?
|
|
165
|
+
"#<#{self.class.name}: #{@patterns.map { |p| p.to_s.inspect }.join(", ")}>"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @!visibility private
|
|
169
|
+
def pretty_print(q)
|
|
170
|
+
q.text "#<#{self.class.name}"
|
|
171
|
+
q.group(1, "", ">") do
|
|
172
|
+
@patterns.each_with_index do |pattern, index|
|
|
173
|
+
q.breakable(index == 0 ? " " : ", ")
|
|
174
|
+
q.pp pattern.to_s
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
157
179
|
# @see Object#==
|
|
158
180
|
def ==(other)
|
|
159
181
|
return false unless other.class == self.class
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'mustermann/sinatra'
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
# Hybrid pattern type that bridges {Mustermann::Sinatra} and Rails pattern syntax.
|
|
5
|
+
#
|
|
6
|
+
# It supports all syntax elements of {Mustermann::Sinatra}, plus URI template-style
|
|
7
|
+
# placeholders, and changes the semantics of parenthesized groups to match Rails:
|
|
8
|
+
#
|
|
9
|
+
# - A group *without* a pipe operator is **implicitly optional**, even without a
|
|
10
|
+
# trailing `?`. So `/foo(/bar)` matches both `/foo/bar` and `/foo`.
|
|
11
|
+
#
|
|
12
|
+
# - A group *with* a pipe operator is **not** implicitly optional, to avoid the
|
|
13
|
+
# ambiguity of `/scope/(a|b)` also matching `/scope/`. Add a trailing `?` to make
|
|
14
|
+
# such a group optional explicitly: `/scope/(a|b)?`.
|
|
15
|
+
#
|
|
16
|
+
# @example Implicit optional group (no pipe)
|
|
17
|
+
# require 'mustermann'
|
|
18
|
+
# pattern = Mustermann.new('/foo(/bar)', type: :hybrid)
|
|
19
|
+
# pattern === '/foo' # => true
|
|
20
|
+
# pattern === '/foo/bar' # => true
|
|
21
|
+
#
|
|
22
|
+
# @example Non-optional group with pipe
|
|
23
|
+
# pattern = Mustermann.new('/scope/(a|b)', type: :hybrid)
|
|
24
|
+
# pattern === '/scope/a' # => true
|
|
25
|
+
# pattern === '/scope/' # => false
|
|
26
|
+
#
|
|
27
|
+
# @example Explicitly optional group with pipe
|
|
28
|
+
# pattern = Mustermann.new('/scope/(a|b)?', type: :hybrid)
|
|
29
|
+
# pattern === '/scope/' # => true
|
|
30
|
+
#
|
|
31
|
+
# @example Nested implicit optional groups (Rails-style resource routing)
|
|
32
|
+
# pattern = Mustermann.new('/:controller(/:action(/:id))', type: :hybrid)
|
|
33
|
+
# pattern.params('/posts') # => { "controller" => "posts" }
|
|
34
|
+
# pattern.params('/posts/show') # => { "controller" => "posts", "action" => "show" }
|
|
35
|
+
# pattern.params('/posts/show/1') # => { "controller" => "posts", "action" => "show", "id" => "1" }
|
|
36
|
+
#
|
|
37
|
+
# @see Mustermann::Sinatra
|
|
38
|
+
class Hybrid < Sinatra
|
|
39
|
+
register :hybrid
|
|
40
|
+
|
|
41
|
+
# Parses a parenthesized group. Groups without a pipe operator are wrapped in an
|
|
42
|
+
# optional node (implicitly optional, Rails style). Groups that do contain a pipe
|
|
43
|
+
# operator are left as plain groups; append `?` to make them optional explicitly.
|
|
44
|
+
on("(") do |c|
|
|
45
|
+
n = node(:group) { read unless scan(?)) }
|
|
46
|
+
has_or = n.payload.any? { |e| e.is_a?(:or) }
|
|
47
|
+
has_or && !scan("?") ? n : node(:optional, n)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mustermann
|
|
4
|
+
# The return value of {Mustermann::Pattern#match}, {Mustermann::Pattern#peek_match}, {Mustermann::Set#match}, and similar methods.
|
|
5
|
+
# Mimics large parts of the MatchData API, but also provides access to the pattern and params hash.
|
|
6
|
+
class Match
|
|
7
|
+
# @return [Mustermann::Pattern] the pattern that produced the match
|
|
8
|
+
attr_reader :pattern
|
|
9
|
+
|
|
10
|
+
# @return [String] the string that was matched
|
|
11
|
+
attr_reader :string
|
|
12
|
+
|
|
13
|
+
# @return [Hash] the params hash
|
|
14
|
+
attr_reader :params
|
|
15
|
+
|
|
16
|
+
# @return [Array] the captures array
|
|
17
|
+
attr_reader :captures
|
|
18
|
+
|
|
19
|
+
# @return [Hash] the named captures hash, usually identical to {#params}
|
|
20
|
+
attr_reader :named_captures
|
|
21
|
+
|
|
22
|
+
# @return [String] the post match string
|
|
23
|
+
attr_reader :post_match
|
|
24
|
+
|
|
25
|
+
# @return [String] the pre match string
|
|
26
|
+
attr_reader :pre_match
|
|
27
|
+
|
|
28
|
+
# @return [Regexp, nil] the regular expression that produced the match, if available
|
|
29
|
+
attr_reader :regexp
|
|
30
|
+
|
|
31
|
+
# @overload initialize(pattern, string, **options)
|
|
32
|
+
# @param pattern [Mustermann::Pattern] the pattern that produced the match
|
|
33
|
+
# @param string [String] the string that was matched
|
|
34
|
+
#
|
|
35
|
+
# @overload initialize(match, **options)
|
|
36
|
+
# @param match [Mustermann::Match] the match to copy pattern and string from
|
|
37
|
+
#
|
|
38
|
+
# @overload initialize(pattern, match, **options)
|
|
39
|
+
# @param match [Mustermann::Match, MatchData] the match to copy string from
|
|
40
|
+
#
|
|
41
|
+
# @option options [Array] :captures the captures array
|
|
42
|
+
# @option options [Hash] :named_captures the named captures hash
|
|
43
|
+
# @option options [String] :matched the matched substring (defaults to string for full matches)
|
|
44
|
+
# @option options [Hash] :params the params hash
|
|
45
|
+
# @option options [Regexp] :regexp the regular expression that produced the match
|
|
46
|
+
# @option options [String] :post_match the post match string
|
|
47
|
+
# @option options [String] :pre_match the pre match string
|
|
48
|
+
def initialize(pattern_or_match, string_or_match = nil, matched: nil, params: nil, post_match: nil, pre_match: nil, captures: nil, named_captures: nil, regexp: nil)
|
|
49
|
+
case pattern_or_match
|
|
50
|
+
when Mustermann::Match, MatchData then match = pattern_or_match
|
|
51
|
+
when Mustermann::Pattern then pattern = pattern_or_match
|
|
52
|
+
else raise ArgumentError, "first argument must be a Mustermann::Pattern or a MatchData, not #{pattern_or_match.class}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
case string_or_match
|
|
56
|
+
when Mustermann::Match, MatchData then match ||= string_or_match
|
|
57
|
+
when String then string = string_or_match
|
|
58
|
+
when nil # ignore
|
|
59
|
+
else raise ArgumentError, "second argument must be a String or a MatchData, not #{string_or_match.class}"
|
|
60
|
+
end
|
|
61
|
+
|
|
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 || ''
|
|
67
|
+
@captures = captures || match&.captures || @params.values
|
|
68
|
+
@named_captures = named_captures || match&.named_captures || @params
|
|
69
|
+
@matched = matched || match&.to_s || @string
|
|
70
|
+
|
|
71
|
+
unless @regexp = regexp
|
|
72
|
+
@regexp = match.regexp if match.respond_to?(:regexp)
|
|
73
|
+
@regexp ||= pattern.respond_to?(:regexp) ? pattern.regexp : nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Array<String>] the names of the named captures
|
|
78
|
+
def names = named_captures.keys
|
|
79
|
+
|
|
80
|
+
# @overload [](key)
|
|
81
|
+
# Access named captures by key.
|
|
82
|
+
# @param key [String, Symbol] the key to access
|
|
83
|
+
# @return the value of the named capture, or nil if not found
|
|
84
|
+
#
|
|
85
|
+
# @overload [](index)
|
|
86
|
+
# Access captures by index.
|
|
87
|
+
# @param index [Integer] the index to access
|
|
88
|
+
# @return the value of the capture, or nil if not found
|
|
89
|
+
#
|
|
90
|
+
# @overload [](start, length)
|
|
91
|
+
# Access multiple captures by index and length.
|
|
92
|
+
# @param start [Integer] the starting index to access
|
|
93
|
+
# @param length [Integer] the number of captures to access
|
|
94
|
+
# @return [Array] the values of the captures
|
|
95
|
+
#
|
|
96
|
+
# @overload [](range)
|
|
97
|
+
# Access multiple captures by range.
|
|
98
|
+
# @param range [Range] the range of indices to access
|
|
99
|
+
# @return [Array] the values of the captures
|
|
100
|
+
def [](key, length = nil)
|
|
101
|
+
case key
|
|
102
|
+
when String then named_captures[key]
|
|
103
|
+
when Symbol then named_captures[key.to_s]
|
|
104
|
+
when Integer then length ? captures[key, length] : captures[key]
|
|
105
|
+
when Range then captures[key]
|
|
106
|
+
else raise ArgumentError, "key must be a String, Symbol, Integer, or Range, not #{key.class}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Deconstructs the match into a hash of the given keys. Useful for pattern matching.
|
|
111
|
+
# @param keys [Array] the keys to deconstruct
|
|
112
|
+
# @return [Hash] a hash of the given keys and their corresponding values
|
|
113
|
+
# @see https://docs.ruby-lang.org/en/4.0/syntax/pattern_matching_rdoc.html
|
|
114
|
+
def deconstruct_keys(keys) = keys.to_h { |key| [key, self[key]] }
|
|
115
|
+
|
|
116
|
+
# @see Object#hash
|
|
117
|
+
def hash = pattern.hash ^ string.hash ^ params.hash
|
|
118
|
+
|
|
119
|
+
# @see Object#eql?
|
|
120
|
+
def eql?(other)
|
|
121
|
+
return false unless other.is_a? self.class
|
|
122
|
+
pattern == other.pattern && string == other.string && params == other.params
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns the values of the given keys as an array.
|
|
126
|
+
# @params keys [Array<Symbol, String>] the keys to access
|
|
127
|
+
# @return [Array] the values of the given keys
|
|
128
|
+
def values_at(*keys) = keys.map { |key| self[key] }
|
|
129
|
+
|
|
130
|
+
# @return [String] the matched substring (like MatchData#to_s)
|
|
131
|
+
def to_s = @matched
|
|
132
|
+
|
|
133
|
+
alias == eql?
|
|
134
|
+
alias to_h params
|
|
135
|
+
|
|
136
|
+
# @!visibility private
|
|
137
|
+
def inspect
|
|
138
|
+
params_str = params.map { |k, v| " #{k}:#{v.inspect}" }.join
|
|
139
|
+
"#<#{self.class.name}: #{@matched.inspect}#{params_str}>"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @!visibility private
|
|
143
|
+
def pretty_print(q)
|
|
144
|
+
q.group(1, "#<#{self.class.name}:", ">") do
|
|
145
|
+
q.breakable
|
|
146
|
+
q.pp @matched
|
|
147
|
+
params.each do |key, value|
|
|
148
|
+
q.breakable
|
|
149
|
+
q.text("#{key}:")
|
|
150
|
+
q.pp value
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/mustermann/pattern.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mustermann/error'
|
|
3
|
-
require 'mustermann/
|
|
3
|
+
require 'mustermann/match'
|
|
4
4
|
require 'mustermann/equality_map'
|
|
5
5
|
require 'uri'
|
|
6
6
|
|
|
@@ -84,14 +84,15 @@ module Mustermann
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# @param [String] string The string to match against
|
|
87
|
-
# @return [
|
|
88
|
-
# @see
|
|
89
|
-
# @see http://ruby-doc.org/core-2.0/MatchData.html MatchData
|
|
90
|
-
# @see Mustermann::SimpleMatch
|
|
87
|
+
# @return [Mustermann::Match, nil] the match object if the pattern matches.
|
|
88
|
+
# @see Mustermann::Match
|
|
91
89
|
def match(string)
|
|
92
|
-
|
|
90
|
+
Match.new(self, string) if self === string
|
|
93
91
|
end
|
|
94
92
|
|
|
93
|
+
# @return [Array<String>] list of named captures in the pattern
|
|
94
|
+
def names = []
|
|
95
|
+
|
|
95
96
|
# @param [String] string The string to match against
|
|
96
97
|
# @return [Integer, nil] nil if pattern does not match the string, zero if it does.
|
|
97
98
|
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
|
|
@@ -110,7 +111,7 @@ module Mustermann
|
|
|
110
111
|
# Used by Ruby internally for hashing.
|
|
111
112
|
# @return [Integer] same has value for patterns that are equal
|
|
112
113
|
def hash
|
|
113
|
-
self.class.hash
|
|
114
|
+
self.class.hash ^ @string.hash ^ options.hash
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
# Two patterns are considered equal if they are of the same type, have the same pattern string
|
|
@@ -163,11 +164,11 @@ module Mustermann
|
|
|
163
164
|
# pattern.peek("/Frank/Sinatra") # => #<MatchData "/Frank" name:"Frank">
|
|
164
165
|
#
|
|
165
166
|
# @param [String] string The string to match against
|
|
166
|
-
# @return [
|
|
167
|
+
# @return [Mustermann::Match, nil] MatchData or similar object if the pattern matches.
|
|
167
168
|
# @see #peek_params
|
|
168
169
|
def peek_match(string)
|
|
169
170
|
matched = peek(string)
|
|
170
|
-
|
|
171
|
+
Match.new(self, string, matched:, params: {}, post_match: string[matched.size..-1]) if matched
|
|
171
172
|
end
|
|
172
173
|
|
|
173
174
|
# Tries to match the pattern against the beginning of the string (as opposed to the full string).
|
|
@@ -184,33 +185,12 @@ module Mustermann
|
|
|
184
185
|
# @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
|
|
185
186
|
def peek_params(string)
|
|
186
187
|
match = peek_match(string)
|
|
187
|
-
[params
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# @return [Hash{String: Array<Integer>}] capture names mapped to capture index.
|
|
191
|
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures
|
|
192
|
-
def named_captures
|
|
193
|
-
{}
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# @return [Array<String>] capture names.
|
|
197
|
-
# @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names
|
|
198
|
-
def names
|
|
199
|
-
[]
|
|
188
|
+
match ? [match.params, match.to_s.size] : nil
|
|
200
189
|
end
|
|
201
190
|
|
|
202
191
|
# @param [String] string the string to match against
|
|
203
192
|
# @return [Hash{String: String, Array<String>}, nil] Sinatra style params if pattern matches.
|
|
204
|
-
def params(string = nil
|
|
205
|
-
return unless captures ||= match(string)
|
|
206
|
-
params = named_captures.map do |name, positions|
|
|
207
|
-
values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten
|
|
208
|
-
values = values.first if values.size < 2 and not always_array? name
|
|
209
|
-
[name, values]
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
Hash[params]
|
|
213
|
-
end
|
|
193
|
+
def params(string = nil) = match(string)&.params
|
|
214
194
|
|
|
215
195
|
# @note This method is only implemented by certain subclasses.
|
|
216
196
|
#
|
|
@@ -362,6 +342,13 @@ module Mustermann
|
|
|
362
342
|
method(method).owner != Mustermann::Pattern
|
|
363
343
|
end
|
|
364
344
|
|
|
345
|
+
# @!visibility private
|
|
346
|
+
def pretty_print(q)
|
|
347
|
+
q.text "#<%p:" % self.class
|
|
348
|
+
q.pp(@string)
|
|
349
|
+
q.text ">"
|
|
350
|
+
end
|
|
351
|
+
|
|
365
352
|
# @!visibility private
|
|
366
353
|
def inspect
|
|
367
354
|
"#<%p:%p>" % [self.class, @string]
|
|
@@ -381,6 +368,7 @@ module Mustermann
|
|
|
381
368
|
# @!visibility private
|
|
382
369
|
def unescape(string, decode = uri_decode)
|
|
383
370
|
return string unless decode and string
|
|
371
|
+
return string unless string.include?('%')
|
|
384
372
|
@@uri.unescape(string)
|
|
385
373
|
end
|
|
386
374
|
|
|
@@ -392,6 +380,13 @@ module Mustermann
|
|
|
392
380
|
ALWAYS_ARRAY.include? key
|
|
393
381
|
end
|
|
394
382
|
|
|
383
|
+
# @api private
|
|
384
|
+
# Returns true if params can be used as-is without calling map_param.
|
|
385
|
+
# Used by Set::Trie to skip building a redundant copy of the params hash.
|
|
386
|
+
def identity_params?(params)
|
|
387
|
+
!params.any? { |k, v| v.is_a?(Array) || always_array?(k) || (v.respond_to?(:include?) && v.include?('%')) }
|
|
388
|
+
end
|
|
389
|
+
|
|
395
390
|
private :unescape, :map_param, :respond_to_special?
|
|
396
391
|
private_constant :ALWAYS_ARRAY
|
|
397
392
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann'
|
|
3
|
+
require 'mustermann/ast/pattern'
|
|
4
|
+
require 'mustermann/ast/fast_pattern'
|
|
5
|
+
require 'mustermann/versions'
|
|
6
|
+
|
|
7
|
+
module Mustermann
|
|
8
|
+
# Rails style pattern implementation.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# Mustermann.new('/:foo', type: :rails) === '/bar' # => true
|
|
12
|
+
#
|
|
13
|
+
# @see Mustermann::Pattern
|
|
14
|
+
# @see file:README.md#rails Syntax description in the README
|
|
15
|
+
class Rails < AST::Pattern
|
|
16
|
+
include AST::FastPattern
|
|
17
|
+
extend Versions
|
|
18
|
+
register :rails
|
|
19
|
+
|
|
20
|
+
# first parser, no optional parts
|
|
21
|
+
version('2.3') do
|
|
22
|
+
on(nil) { |c| unexpected(c) }
|
|
23
|
+
on(?*) { |c| node(:named_splat) { scan(/\w+/) } }
|
|
24
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rack-mount
|
|
28
|
+
version('3.0', '3.1') do
|
|
29
|
+
on(?)) { |c| unexpected(c) }
|
|
30
|
+
on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) }
|
|
31
|
+
on(?\\) { |c| node(:char, expect(/./)) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# stand-alone journey
|
|
35
|
+
version('3.2') do
|
|
36
|
+
on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" }
|
|
37
|
+
on(?\\) { |c| node(:char, c) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# embedded journey, broken (ignored) escapes
|
|
41
|
+
version('4.0', '4.1') { on(?\\) { |c| read } }
|
|
42
|
+
|
|
43
|
+
# escapes got fixed in 4.2
|
|
44
|
+
version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } }
|
|
45
|
+
|
|
46
|
+
# Rails 5.0 fixes |
|
|
47
|
+
version('5', '6', '7', '8') { on(?|) { |c| node(:or) }}
|
|
48
|
+
|
|
49
|
+
# (see Mustermann::Pattern#|)
|
|
50
|
+
def |(other) = combine(other, :|) { super }
|
|
51
|
+
|
|
52
|
+
# (see Mustermann::Pattern#+)
|
|
53
|
+
def +(other) = combine(other, :+) { super }
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def combine(other, operator)
|
|
58
|
+
return yield unless hybrid = Mustermann[:hybrid].try_convert(self, **options)
|
|
59
|
+
native = hybrid.public_send(operator, other)
|
|
60
|
+
native.is_a?(Composite) ? yield : native
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -11,14 +11,37 @@ module Mustermann
|
|
|
11
11
|
attr_reader :regexp
|
|
12
12
|
alias_method :to_regexp, :regexp
|
|
13
13
|
|
|
14
|
+
# @api private
|
|
15
|
+
supported_options :cache
|
|
16
|
+
|
|
14
17
|
# @param (see Mustermann::Pattern#initialize)
|
|
15
18
|
# @return (see Mustermann::Pattern#initialize)
|
|
16
19
|
# @see (see Mustermann::Pattern#initialize)
|
|
17
20
|
def initialize(string, **options)
|
|
21
|
+
cache = options.delete(:cache) { true }
|
|
22
|
+
|
|
18
23
|
super
|
|
19
|
-
regexp
|
|
20
|
-
@peek_regexp
|
|
21
|
-
@regexp
|
|
24
|
+
regexp = compile(**options)
|
|
25
|
+
@peek_regexp = /\A#{regexp}/
|
|
26
|
+
@regexp = /\A#{regexp}\Z/
|
|
27
|
+
@simple_captures = @regexp.named_captures.none? { |name, positions| positions.size > 1 || always_array?(name) }
|
|
28
|
+
|
|
29
|
+
cache_class = ObjectSpace::WeakKeyMap if defined?(ObjectSpace::WeakKeyMap)
|
|
30
|
+
|
|
31
|
+
case cache
|
|
32
|
+
when true
|
|
33
|
+
@match_cache = cache_class&.new || false
|
|
34
|
+
@peek_cache = cache_class&.new || false
|
|
35
|
+
when false
|
|
36
|
+
@match_cache = false
|
|
37
|
+
@peek_cache = false
|
|
38
|
+
when Hash
|
|
39
|
+
@match_cache = cache[:match] || cache_class&.new || false
|
|
40
|
+
@peek_cache = cache[:peek] || cache_class&.new || false
|
|
41
|
+
else
|
|
42
|
+
@match_cache = cache.new
|
|
43
|
+
@peek_cache = cache.new
|
|
44
|
+
end
|
|
22
45
|
end
|
|
23
46
|
|
|
24
47
|
# @param (see Mustermann::Pattern#peek_size)
|
|
@@ -32,17 +55,55 @@ module Mustermann
|
|
|
32
55
|
# @param (see Mustermann::Pattern#peek_match)
|
|
33
56
|
# @return (see Mustermann::Pattern#peek_match)
|
|
34
57
|
# @see (see Mustermann::Pattern#peek_match)
|
|
35
|
-
def peek_match(string)
|
|
36
|
-
|
|
58
|
+
def peek_match(string) = cache_match(@peek_cache, @peek_regexp, string)
|
|
59
|
+
|
|
60
|
+
# @param (see Mustermann::Pattern#match)
|
|
61
|
+
# @return (see Mustermann::Pattern#match)
|
|
62
|
+
# @see (see Mustermann::Pattern#match)
|
|
63
|
+
def match(string) = cache_match(@match_cache, @regexp, string)
|
|
64
|
+
|
|
65
|
+
# Extracts params directly from the regexp without allocating a Match object or
|
|
66
|
+
# populating the match cache — significant GC savings when called in hot loops.
|
|
67
|
+
# @param (see Mustermann::Pattern#params)
|
|
68
|
+
# @return (see Mustermann::Pattern#params)
|
|
69
|
+
def params(string = nil)
|
|
70
|
+
return unless md = @regexp.match(string)
|
|
71
|
+
build_params(md)
|
|
37
72
|
end
|
|
38
73
|
|
|
39
74
|
extend Forwardable
|
|
40
|
-
def_delegators :regexp, :===, :=~, :
|
|
75
|
+
def_delegators :regexp, :===, :=~, :names
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def cache_match(cache, regexp, string)
|
|
80
|
+
if cache
|
|
81
|
+
return cache[string] if cache.key?(string)
|
|
82
|
+
cache[string] = build_match(regexp, string)
|
|
83
|
+
else
|
|
84
|
+
build_match(regexp, string)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_match(regexp, string)
|
|
89
|
+
return unless match = regexp.match(string)
|
|
90
|
+
Match.new(self, match, params: build_params(match))
|
|
91
|
+
end
|
|
41
92
|
|
|
42
|
-
def
|
|
43
|
-
|
|
93
|
+
def build_params(match)
|
|
94
|
+
if @simple_captures
|
|
95
|
+
params = match.named_captures
|
|
96
|
+
return params if params.empty? || identity_params?(params)
|
|
97
|
+
params.each_with_object({}) { |(k, v), h| h[k] = map_param(k, v) }
|
|
98
|
+
else
|
|
99
|
+
match.regexp.named_captures.to_h do |name, positions|
|
|
100
|
+
value = positions.size < 2 && !always_array?(name) ? map_param(name, match[name]) :
|
|
101
|
+
positions.flat_map { |pos| map_param(name, match[pos]) }
|
|
102
|
+
[name, value]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
44
105
|
end
|
|
45
106
|
|
|
46
|
-
|
|
107
|
+
def compile(**options) = raise NotImplementedError, 'subclass responsibility'
|
|
47
108
|
end
|
|
48
109
|
end
|