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,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require 'mustermann/ast/node'
|
|
3
3
|
require 'mustermann/error'
|
|
4
|
-
require 'ruby2_keywords'
|
|
5
4
|
require 'delegate'
|
|
6
5
|
|
|
7
6
|
module Mustermann
|
|
@@ -39,11 +38,15 @@ module Mustermann
|
|
|
39
38
|
attr_reader :translator
|
|
40
39
|
|
|
41
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.
|
|
42
44
|
# @!visibility private
|
|
43
|
-
|
|
45
|
+
def t(*args, &block)
|
|
44
46
|
return translator unless args.any?
|
|
45
47
|
translator.translate(*args, &block)
|
|
46
48
|
end
|
|
49
|
+
ruby2_keywords :t
|
|
47
50
|
|
|
48
51
|
# @!visibility private
|
|
49
52
|
alias_method :node, :__getobj__
|
|
@@ -105,19 +108,25 @@ module Mustermann
|
|
|
105
108
|
# @return decorator encapsulating translation
|
|
106
109
|
#
|
|
107
110
|
# @!visibility private
|
|
111
|
+
def self.factory_for(node_class)
|
|
112
|
+
@factory_for ||= {}
|
|
113
|
+
@factory_for[node_class] ||= node_class.ancestors.lazy.filter_map { dispatch_table[_1.name] }.first
|
|
114
|
+
end
|
|
115
|
+
|
|
108
116
|
def decorator_for(node)
|
|
109
|
-
factory =
|
|
110
|
-
|
|
117
|
+
factory = self.class.factory_for(node.class) or
|
|
118
|
+
raise error_class, "#{self.class}: Cannot translate #{node.class}"
|
|
111
119
|
factory.new(node, self)
|
|
112
120
|
end
|
|
113
121
|
|
|
114
122
|
# Start the translation dance for a (sub)tree.
|
|
115
123
|
# @!visibility private
|
|
116
|
-
|
|
124
|
+
def translate(node, *args, &block)
|
|
117
125
|
result = decorator_for(node).translate(*args, &block)
|
|
118
126
|
result = result.node while result.is_a? NodeTranslator
|
|
119
127
|
result
|
|
120
128
|
end
|
|
129
|
+
ruby2_keywords :translate
|
|
121
130
|
|
|
122
131
|
# @return [String] escaped character
|
|
123
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
|
@@ -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 ||= Mustermann.dedup(patterns.flat_map { |p| p.respond_to?(:names) ? p.names : EMPTY_ARRAY }.uniq)
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
# @see Mustermann::Pattern#==
|
|
33
38
|
def ==(pattern)
|
|
34
39
|
patterns == patterns_from(pattern)
|
|
@@ -66,7 +71,7 @@ module Mustermann
|
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
# (see Mustermann::Pattern#expand)
|
|
69
|
-
def expand(behavior = nil, values =
|
|
74
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
70
75
|
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
|
|
71
76
|
@expander ||= Mustermann::Expander.new(*patterns)
|
|
72
77
|
@expander.expand(behavior, values)
|
|
@@ -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
|
|
@@ -96,7 +107,7 @@ module Mustermann
|
|
|
96
107
|
# (see Mustermann::Pattern#to_templates)
|
|
97
108
|
def to_templates
|
|
98
109
|
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
|
|
99
|
-
@to_templates ||= patterns.inject([
|
|
110
|
+
@to_templates ||= patterns.inject([EMPTY_STRING]) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq
|
|
100
111
|
end
|
|
101
112
|
|
|
102
113
|
# @!visibility private
|
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,20 +13,21 @@ 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
|
|
28
29
|
@additional_values = additional_values
|
|
29
|
-
@options = options
|
|
30
|
+
@options = Mustermann.dedup(options)
|
|
30
31
|
@caster = Caster.new
|
|
31
32
|
add(*patterns, &block)
|
|
32
33
|
end
|
|
@@ -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))
|
|
@@ -142,7 +147,7 @@ module Mustermann
|
|
|
142
147
|
# @return [String] expanded string
|
|
143
148
|
# @raise [NotImplementedError] raised if expand is not supported.
|
|
144
149
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
|
145
|
-
def expand(behavior = nil, values =
|
|
150
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
146
151
|
behavior, values = nil, behavior if behavior.is_a? Hash
|
|
147
152
|
values = map_values(values)
|
|
148
153
|
|
|
@@ -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
|
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
|
|
@@ -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 || 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
|
+
@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
|
|
|
@@ -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
|
|
@@ -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 = EMPTY_ARRAY
|
|
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: EMPTY_HASH, 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
|
#
|
|
@@ -237,7 +217,7 @@ module Mustermann
|
|
|
237
217
|
# @raise [NotImplementedError] raised if expand is not supported.
|
|
238
218
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
|
239
219
|
# @see Mustermann::Expander
|
|
240
|
-
def expand(behavior = nil, values =
|
|
220
|
+
def expand(behavior = nil, values = EMPTY_HASH)
|
|
241
221
|
raise NotImplementedError, "expanding not supported by #{self.class}"
|
|
242
222
|
end
|
|
243
223
|
|
|
@@ -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
|