mustermann 3.1.1 → 4.0.0.alpha
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 +95 -370
- data/lib/mustermann/ast/compiler.rb +124 -29
- data/lib/mustermann/ast/pattern.rb +10 -0
- data/lib/mustermann/ast/translator.rb +7 -2
- data/lib/mustermann/concat.rb +9 -3
- data/lib/mustermann/error.rb +1 -0
- data/lib/mustermann/expander.rb +8 -3
- data/lib/mustermann/hybrid.rb +50 -0
- data/lib/mustermann/match.rb +39 -0
- data/lib/mustermann/pattern.rb +8 -31
- data/lib/mustermann/rails.rb +1 -1
- data/lib/mustermann/regexp_based.rb +22 -7
- data/lib/mustermann/router.rb +99 -0
- data/lib/mustermann/set/cache.rb +30 -0
- data/lib/mustermann/set/linear.rb +30 -0
- data/lib/mustermann/set/match.rb +16 -0
- data/lib/mustermann/set/trie.rb +173 -0
- data/lib/mustermann/set.rb +327 -0
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +0 -6
- metadata +24 -9
- 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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/set/match'
|
|
3
|
+
|
|
4
|
+
module Mustermann
|
|
5
|
+
class Set
|
|
6
|
+
class Linear
|
|
7
|
+
def initialize(set, patterns = [])
|
|
8
|
+
@set = set
|
|
9
|
+
@patterns = patterns
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add(pattern)
|
|
13
|
+
@patterns << pattern
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def match(string, all: false, peek: false)
|
|
17
|
+
result = [] if all
|
|
18
|
+
@patterns.each do |pattern|
|
|
19
|
+
next unless match = peek ? pattern.peek_match(string) : pattern.match(string)
|
|
20
|
+
return Match.new(match:, value: @set.values_for_pattern(pattern)&.first) unless all
|
|
21
|
+
values = @set.values_for_pattern(pattern) || [nil]
|
|
22
|
+
values.each { |value| result << Match.new(match:, value:) }
|
|
23
|
+
end
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private_constant :Linear
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/match'
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
class Set
|
|
7
|
+
class Match < DelegateClass(Mustermann::Match)
|
|
8
|
+
attr_reader :value
|
|
9
|
+
|
|
10
|
+
def initialize(*args, value: nil, match: nil, **options)
|
|
11
|
+
@value = value
|
|
12
|
+
super(match || Mustermann::Match.new(*args, **options))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann/ast/translator'
|
|
3
|
+
require 'mustermann/set/match'
|
|
4
|
+
|
|
5
|
+
module Mustermann
|
|
6
|
+
class Set
|
|
7
|
+
class Trie
|
|
8
|
+
class Translator < AST::Translator
|
|
9
|
+
translate(:node) { |trie, **o| trie[t.compile(node)] }
|
|
10
|
+
translate(:separator) { |trie, **options| trie[payload] }
|
|
11
|
+
|
|
12
|
+
translate(:root) do |trie, **options|
|
|
13
|
+
leaves = t(payload, trie, **options)
|
|
14
|
+
if leaves.is_a? Array
|
|
15
|
+
leaves.each { |leaf| leaf.patterns << t.pattern }
|
|
16
|
+
else
|
|
17
|
+
leaves.patterns << t.pattern
|
|
18
|
+
end
|
|
19
|
+
leaves
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
translate(:char) do |trie, **options|
|
|
23
|
+
strings = t.possible_strings(payload)
|
|
24
|
+
return trie if strings.empty?
|
|
25
|
+
primary_node = trie[strings.first]
|
|
26
|
+
strings[1..-1].each { |s| trie.wire(s, primary_node) }
|
|
27
|
+
primary_node
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
translate(:optional) do |trie, **options|
|
|
31
|
+
[*t(payload, trie, **options), trie]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
translate(Array) do |trie, **options|
|
|
35
|
+
i = 0
|
|
36
|
+
while i < size
|
|
37
|
+
element = self[i]
|
|
38
|
+
if element.is_a? :char or element.is_a? :separator
|
|
39
|
+
trie = t(element, trie, **options)
|
|
40
|
+
i += 1
|
|
41
|
+
elsif element.is_a? :splat and self[i + 1]&.is_a? :separator
|
|
42
|
+
# Compile splat+separator together so the splat is bounded by the separator,
|
|
43
|
+
# then continue building the trie for the remaining elements.
|
|
44
|
+
trie = trie[t.compile(self[i..i + 1])]
|
|
45
|
+
i += 2
|
|
46
|
+
elsif element.is_a? :splat or !self[i + 1]&.is_a? :separator
|
|
47
|
+
return trie[t.compile(self[i..-1])]
|
|
48
|
+
else
|
|
49
|
+
trie = t(element, trie, **options)
|
|
50
|
+
return trie.flat_map { |node| t(self[i + 1..-1], node, **options) } if trie.is_a? Array
|
|
51
|
+
i += 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
trie
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
attr_reader :pattern
|
|
58
|
+
|
|
59
|
+
def initialize(pattern)
|
|
60
|
+
@pattern = pattern
|
|
61
|
+
@compiler = pattern.compiler.new
|
|
62
|
+
@options = pattern.options
|
|
63
|
+
super()
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def compile(node, **options) = /\A#{@compiler.translate(node, **@options, **options)}/
|
|
67
|
+
|
|
68
|
+
def possible_strings(char)
|
|
69
|
+
return [] if char.empty?
|
|
70
|
+
@compiler.class.char_representations(char, **@options.slice(:uri_decode, :space_matches_plus))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
attr_reader :patterns, :set, :static, :dynamic
|
|
75
|
+
|
|
76
|
+
def initialize(set, patterns = [])
|
|
77
|
+
@set = set
|
|
78
|
+
@patterns = []
|
|
79
|
+
@dynamic = {}
|
|
80
|
+
@static = {}
|
|
81
|
+
patterns.each { |pattern| add(pattern) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def [](key)
|
|
85
|
+
case key
|
|
86
|
+
when String then @static[key] ||= Trie.new(@set)
|
|
87
|
+
when Regexp then @dynamic[key] ||= Trie.new(@set)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def wire(string, target)
|
|
92
|
+
return if string.empty?
|
|
93
|
+
if string.size == 1
|
|
94
|
+
@static[string] ||= target
|
|
95
|
+
else
|
|
96
|
+
(@static[string[0]] ||= Trie.new(@set)).wire(string[1..-1], target)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def match(string, all: false, peek: false, position: 0, params: {})
|
|
101
|
+
return build_matches(string, params, all:) if position >= string.size
|
|
102
|
+
result = [] if all
|
|
103
|
+
|
|
104
|
+
if node = @static[string[position]]
|
|
105
|
+
if nested_result = node.match(string, all:, peek:, position: position + 1, params:)
|
|
106
|
+
return nested_result unless all
|
|
107
|
+
result.concat(nested_result)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
anchored = {}
|
|
112
|
+
@dynamic.each do |matcher, node|
|
|
113
|
+
remaining = string[position..-1]
|
|
114
|
+
regexp_match = matcher.match(remaining)
|
|
115
|
+
# Non-greedy patterns (e.g. splat .*?) can match 0 chars on non-empty input, making
|
|
116
|
+
# no progress. Retry with an end-of-string anchor so they consume the full remainder.
|
|
117
|
+
if regexp_match&.to_s&.empty? && !remaining.empty?
|
|
118
|
+
anchored_matcher = anchored[matcher] ||= Regexp.new(matcher.source + '\z')
|
|
119
|
+
regexp_match = anchored_matcher.match(remaining)
|
|
120
|
+
end
|
|
121
|
+
next unless regexp_match
|
|
122
|
+
|
|
123
|
+
regexp_match.named_captures.each do |name, value|
|
|
124
|
+
params = params.dup
|
|
125
|
+
params[name] = params[name]&.dup || []
|
|
126
|
+
params[name] << value
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
nested_result = node.match(string, all:, params:, peek:, position: position + regexp_match.to_s.size)
|
|
130
|
+
return nested_result unless all
|
|
131
|
+
result.concat(nested_result)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if peek
|
|
135
|
+
matches = build_matches(string[0, position], params, all:, post_match: string[position..])
|
|
136
|
+
return matches unless all
|
|
137
|
+
result.concat(matches)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
result
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_matches(string, params, all: false, **options)
|
|
144
|
+
result = [] if all
|
|
145
|
+
|
|
146
|
+
@patterns.each do |pattern|
|
|
147
|
+
next if pattern.except_regexp&.match?(string)
|
|
148
|
+
|
|
149
|
+
pattern_params = params.to_h do |key, value|
|
|
150
|
+
value = value.flat_map { |v| pattern.map_param(key, v) }
|
|
151
|
+
value = value.first if value.size < 2 and not pattern.always_array?(key)
|
|
152
|
+
[key, value]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
values = @set.values_for_pattern(pattern) || [nil]
|
|
156
|
+
values.each do |value|
|
|
157
|
+
match = Set::Match.new(pattern, string, pattern_params, value:, **options)
|
|
158
|
+
return match unless all
|
|
159
|
+
result << match
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
result
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def add(pattern)
|
|
167
|
+
Translator.new(pattern).translate(pattern.to_ast, self)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private_constant :Trie
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mustermann'
|
|
3
|
+
require 'mustermann/expander'
|
|
4
|
+
require 'mustermann/set/cache'
|
|
5
|
+
require 'mustermann/set/linear'
|
|
6
|
+
require 'mustermann/set/trie'
|
|
7
|
+
|
|
8
|
+
module Mustermann
|
|
9
|
+
# A collection of patterns that can be matched against strings efficiently.
|
|
10
|
+
#
|
|
11
|
+
# Each pattern in the set may be associated with one or more arbitrary values,
|
|
12
|
+
# such as handler objects or route actions. A single {#match} call returns a
|
|
13
|
+
# {Set::Match} that provides both the captured parameters and the associated
|
|
14
|
+
# value for the matched pattern. When the set contains many patterns, an
|
|
15
|
+
# internal trie (prefix tree) is used to dispatch requests in sub-linear time.
|
|
16
|
+
#
|
|
17
|
+
# @example Building a routing table
|
|
18
|
+
# require 'mustermann/set'
|
|
19
|
+
#
|
|
20
|
+
# set = Mustermann::Set.new
|
|
21
|
+
# set.add('/users/:id', :users_show)
|
|
22
|
+
# set.add('/posts/:id', :posts_show)
|
|
23
|
+
#
|
|
24
|
+
# m = set.match('/users/42')
|
|
25
|
+
# m.value # => :users_show
|
|
26
|
+
# m.params['id'] # => '42'
|
|
27
|
+
#
|
|
28
|
+
# @example Constructor shorthand with a hash
|
|
29
|
+
# set = Mustermann::Set.new('/users/:id' => :users_show, '/posts/:id' => :posts_show)
|
|
30
|
+
#
|
|
31
|
+
# @example Block syntax
|
|
32
|
+
# set = Mustermann::Set.new do |s|
|
|
33
|
+
# s.add('/users/:id', :users_show)
|
|
34
|
+
# s.add('/posts/:id', :posts_show)
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @note Adding patterns via {#add}, {#update}, or {#[]=} is not thread-safe, but matching and expanding is.
|
|
38
|
+
class Set
|
|
39
|
+
# Pattern options forwarded to {Mustermann.new} when patterns are created from strings.
|
|
40
|
+
# @return [Hash]
|
|
41
|
+
attr_reader :options
|
|
42
|
+
|
|
43
|
+
# Creates a new set, optionally pre-populated with patterns.
|
|
44
|
+
#
|
|
45
|
+
# Patterns can be supplied as a Hash (pattern → value), a plain String or
|
|
46
|
+
# Pattern, an Array of any of these, or an existing {Set}. The same forms
|
|
47
|
+
# are accepted by {#update} and {#add}.
|
|
48
|
+
#
|
|
49
|
+
# @example Empty set
|
|
50
|
+
# Mustermann::Set.new
|
|
51
|
+
#
|
|
52
|
+
# @example Pre-populated from a hash
|
|
53
|
+
# Mustermann::Set.new('/users/:id' => :users, '/posts/:id' => :posts)
|
|
54
|
+
#
|
|
55
|
+
# @example Imperative block
|
|
56
|
+
# Mustermann::Set.new do |s|
|
|
57
|
+
# s.add('/users/:id', :users)
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Zero-argument block returning a mapping hash
|
|
61
|
+
# Mustermann::Set.new { { '/users/:id' => :users } }
|
|
62
|
+
#
|
|
63
|
+
# @param mapping [Array] initial patterns or mappings to add
|
|
64
|
+
# @param additional_values [:raise, :ignore, :append] behavior when extra keys are passed to {#expand};
|
|
65
|
+
# defaults to +:raise+
|
|
66
|
+
# @param options [Hash] pattern options forwarded to {Mustermann.new} (e.g. +type: :rails+)
|
|
67
|
+
# @raise [ArgumentError] if +additional_values+ is not a recognized behavior symbol
|
|
68
|
+
def initialize(*mapping, additional_values: :raise, use_trie: 50, use_cache: true, **options, &block)
|
|
69
|
+
raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless Expander::ADDITIONAL_VALUES.include? additional_values
|
|
70
|
+
raise ArgumentError, "Illegal value %p for use_trie" % use_trie unless [true, false].include?(use_trie) or use_trie.is_a? Integer
|
|
71
|
+
|
|
72
|
+
@use_trie = use_trie
|
|
73
|
+
@use_cache = use_cache
|
|
74
|
+
@matcher = nil
|
|
75
|
+
@mapping = {}
|
|
76
|
+
@reverse_mapping = {}
|
|
77
|
+
@options = {}
|
|
78
|
+
@expanders = {}
|
|
79
|
+
@additional_values = additional_values
|
|
80
|
+
|
|
81
|
+
options.each do |key, value|
|
|
82
|
+
if key.is_a? Symbol
|
|
83
|
+
@options[key] = value
|
|
84
|
+
else
|
|
85
|
+
mapping << { key => value }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
update(mapping)
|
|
90
|
+
|
|
91
|
+
block.arity == 0 ? update(yield) : yield(self) if block
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Adds a pattern to the set, optionally associated with one or more values.
|
|
95
|
+
#
|
|
96
|
+
# If the pattern is given as a String it will be compiled via {Mustermann.new}
|
|
97
|
+
# using the set's own options. The pattern must be AST-based (Sinatra, Rails,
|
|
98
|
+
# and similar types). Plain regexp patterns are not supported.
|
|
99
|
+
#
|
|
100
|
+
# Calling +add+ more than once for the same pattern appends additional values
|
|
101
|
+
# without creating duplicates.
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# set.add('/users/:id', :users)
|
|
105
|
+
# set.add('/users/:id', :admin) # same pattern, second value
|
|
106
|
+
#
|
|
107
|
+
# @param pattern [String, Pattern] the pattern to add
|
|
108
|
+
# @param values [Array] zero or more values to associate with the pattern
|
|
109
|
+
# @return [self]
|
|
110
|
+
# @raise [ArgumentError] if the pattern is not AST-based, or if a reserved symbol is used as a value
|
|
111
|
+
def add(pattern, *values)
|
|
112
|
+
pattern = Mustermann.new(pattern, **options)
|
|
113
|
+
raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast
|
|
114
|
+
|
|
115
|
+
if @mapping.key? pattern
|
|
116
|
+
current = @mapping[pattern]
|
|
117
|
+
else
|
|
118
|
+
add_pattern(pattern)
|
|
119
|
+
current = @mapping[pattern] = []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
values = [nil] if values.empty?
|
|
123
|
+
|
|
124
|
+
values.each do |value|
|
|
125
|
+
raise ArgumentError, "%p may not be used as a value" % value if Expander::ADDITIONAL_VALUES.include? value
|
|
126
|
+
raise ArgumentError, "the set itself may not be used as value" if value == self
|
|
127
|
+
next if current.include? value
|
|
128
|
+
current << value
|
|
129
|
+
@reverse_mapping[value] ||= []
|
|
130
|
+
@reverse_mapping[value] << pattern unless @reverse_mapping[value].include? pattern
|
|
131
|
+
@expanders[value]&.add(pattern)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Adds a pattern associated with a value using hash-assignment syntax.
|
|
138
|
+
# @see #add
|
|
139
|
+
alias []= add
|
|
140
|
+
|
|
141
|
+
# Looks up a value by string or retrieves the first value for a known pattern object.
|
|
142
|
+
#
|
|
143
|
+
# When given a String, it is matched against the set and the associated value of the
|
|
144
|
+
# first matching pattern is returned. When given a {Pattern}, the first value
|
|
145
|
+
# registered for that exact pattern is returned without matching.
|
|
146
|
+
#
|
|
147
|
+
# @example String lookup
|
|
148
|
+
# set['/users/42'] # => :users_show (or nil)
|
|
149
|
+
#
|
|
150
|
+
# @example Pattern lookup
|
|
151
|
+
# pat = Mustermann.new('/users/:id')
|
|
152
|
+
# set[pat] # => :users_show (or nil)
|
|
153
|
+
#
|
|
154
|
+
# @param pattern_or_string [String, Pattern]
|
|
155
|
+
# @return [Object, nil] the associated value, or +nil+ if not found
|
|
156
|
+
# @raise [ArgumentError] for unsupported argument types
|
|
157
|
+
def [](pattern_or_string)
|
|
158
|
+
case pattern_or_string
|
|
159
|
+
when String then match(pattern_or_string)&.value
|
|
160
|
+
when Pattern then values_for_pattern(pattern_or_string)&.first
|
|
161
|
+
else raise ArgumentError, "unsupported pattern type #{pattern_or_string.class}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Matches the string against all patterns in the set and returns the first match.
|
|
166
|
+
#
|
|
167
|
+
# @param string [String] the string to match
|
|
168
|
+
# @return [Set::Match, nil] the first match, or +nil+ if none of the patterns match
|
|
169
|
+
def match(string) = @matcher&.match(string)
|
|
170
|
+
|
|
171
|
+
# Matches the beginning of the string against all patterns and returns the
|
|
172
|
+
# first prefix match. The unmatched remainder of the string is available via
|
|
173
|
+
# {Set::Match#post_match}.
|
|
174
|
+
#
|
|
175
|
+
# @param string [String]
|
|
176
|
+
# @return [Set::Match, nil] the first prefix match, or +nil+
|
|
177
|
+
def peek_match(string) = @matcher&.match(string, peek: true)
|
|
178
|
+
|
|
179
|
+
# Matches the string against all patterns and returns every match, one per
|
|
180
|
+
# (pattern, value) pair, in insertion order.
|
|
181
|
+
#
|
|
182
|
+
# @param string [String]
|
|
183
|
+
# @return [Array<Set::Match>] all matches, or an empty array if none
|
|
184
|
+
def match_all(string) = @matcher&.match(string, all: true)
|
|
185
|
+
|
|
186
|
+
# Matches the beginning of the string against all patterns and returns every
|
|
187
|
+
# prefix match, one per (pattern, value) pair. The unmatched remainder is
|
|
188
|
+
# available as {Set::Match#post_match} on each result.
|
|
189
|
+
#
|
|
190
|
+
# @param string [String]
|
|
191
|
+
# @return [Array<Set::Match>] all prefix matches, or an empty array if none
|
|
192
|
+
def peek_match_all(string) = @matcher&.match(string, all: true, peek: true)
|
|
193
|
+
|
|
194
|
+
# Returns a new set that includes all patterns from the receiver plus those
|
|
195
|
+
# from +mapping+. The receiver is not modified.
|
|
196
|
+
#
|
|
197
|
+
# @param mapping [Hash, String, Pattern, Array, Set] patterns to merge in
|
|
198
|
+
# @return [Set] a new set
|
|
199
|
+
def merge(mapping) = dup.update(mapping)
|
|
200
|
+
|
|
201
|
+
# @!visibility private
|
|
202
|
+
def initialize_copy(other)
|
|
203
|
+
@mapping = other.mapping.transform_values(&:dup)
|
|
204
|
+
@reverse_mapping = @mapping.each_with_object({}) do |(pattern, values), h|
|
|
205
|
+
values.each { |value| (h[value] ||= []) << pattern }
|
|
206
|
+
end
|
|
207
|
+
@expanders = {}
|
|
208
|
+
@matcher = nil
|
|
209
|
+
@mapping.each_key { |pattern| add_pattern(pattern) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Adds all patterns from +mapping+ to the set in place and returns +self+.
|
|
213
|
+
# Aliased as +merge!+.
|
|
214
|
+
#
|
|
215
|
+
# Accepts the same argument forms as {#initialize}: a Hash, a String, a
|
|
216
|
+
# {Pattern}, an Array, or another {Set}.
|
|
217
|
+
#
|
|
218
|
+
# @param mapping [Hash, String, Pattern, Array, Set]
|
|
219
|
+
# @return [self]
|
|
220
|
+
# @raise [ArgumentError] for unsupported mapping types
|
|
221
|
+
def update(mapping)
|
|
222
|
+
case mapping
|
|
223
|
+
when Set then mapping.mapping.each { |pattern, values| add(pattern, *values) }
|
|
224
|
+
when Hash then mapping.each { |k, v| add(k, v) }
|
|
225
|
+
when String, Pattern then add(mapping)
|
|
226
|
+
when Array then mapping.each { |item| update(item) }
|
|
227
|
+
else raise ArgumentError, "unsupported mapping type #{mapping.class}"
|
|
228
|
+
end
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
alias merge! update
|
|
233
|
+
|
|
234
|
+
# Returns all patterns that have been added to the set, in insertion order.
|
|
235
|
+
# @return [Array<Pattern>]
|
|
236
|
+
def patterns = @mapping.keys
|
|
237
|
+
|
|
238
|
+
# Returns an {Expander} that can generate strings from parameter hashes.
|
|
239
|
+
#
|
|
240
|
+
# When called without arguments (or with the set itself as the value) the
|
|
241
|
+
# expander covers all patterns in the set. Pass a specific value to get an
|
|
242
|
+
# expander limited to the patterns associated with that value.
|
|
243
|
+
#
|
|
244
|
+
# @param value [Object] restricts the expander to patterns associated with
|
|
245
|
+
# this value; defaults to the set itself (all patterns)
|
|
246
|
+
# @return [Mustermann::Expander]
|
|
247
|
+
def expander(value = self)
|
|
248
|
+
@expanders[value] ||= begin
|
|
249
|
+
patterns = value == self ? @mapping.keys : @reverse_mapping[value] || []
|
|
250
|
+
Mustermann::Expander.new(patterns, additional_values: @additional_values, **options)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Generates a string from a parameter hash using the patterns in the set.
|
|
255
|
+
#
|
|
256
|
+
# When called with just a parameter hash, the first pattern that can be fully
|
|
257
|
+
# expanded with those keys is used. Pass a value as the first argument to
|
|
258
|
+
# restrict expansion to the patterns associated with that value. You may also
|
|
259
|
+
# pass an +additional_values+ behavior symbol (+:raise+, +:ignore+, or
|
|
260
|
+
# +:append+) as the first argument to override the set's default behavior for
|
|
261
|
+
# that call.
|
|
262
|
+
#
|
|
263
|
+
# @example Expand using any pattern
|
|
264
|
+
# set.expand(id: '5')
|
|
265
|
+
#
|
|
266
|
+
# @example Expand patterns for a specific value
|
|
267
|
+
# set.expand(:users, id: '5')
|
|
268
|
+
#
|
|
269
|
+
# @example Override additional_values behavior for one call
|
|
270
|
+
# set.expand(:ignore, id: '5', extra: 'ignored')
|
|
271
|
+
#
|
|
272
|
+
# @param value [Object, :raise, :ignore, :append] the value whose patterns
|
|
273
|
+
# should be used, or an additional_values behavior symbol; defaults to all
|
|
274
|
+
# patterns
|
|
275
|
+
# @param behavior [:raise, :ignore, :append, nil] how to handle extra keys;
|
|
276
|
+
# defaults to the set's +additional_values+ setting
|
|
277
|
+
# @param values [Hash, nil] the parameters to expand
|
|
278
|
+
# @return [String]
|
|
279
|
+
# @raise [Mustermann::ExpandError] if no pattern can be expanded with the given keys
|
|
280
|
+
def expand(value = self, behavior = nil, values = nil)
|
|
281
|
+
if Expander::ADDITIONAL_VALUES.include? value
|
|
282
|
+
if behavior.is_a? Hash
|
|
283
|
+
values = values ? values.merge(behavior) : behavior
|
|
284
|
+
behavior = nil
|
|
285
|
+
elsif behavior and behavior != value
|
|
286
|
+
raise ArgumentError, "behavior specified multiple times" if behavior
|
|
287
|
+
end
|
|
288
|
+
behavior = value
|
|
289
|
+
value = self
|
|
290
|
+
elsif value.is_a? Hash and behavior.nil? and values.nil?
|
|
291
|
+
values = value
|
|
292
|
+
value = self unless @reverse_mapping.key? values
|
|
293
|
+
end
|
|
294
|
+
expander(value).expand(behavior || @additional_values, values || {})
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# @return [Boolean] whether the set contains any pattern associated with the given value
|
|
298
|
+
def has_value?(value) = @reverse_mapping[value]&.any?
|
|
299
|
+
|
|
300
|
+
# @!visibility private
|
|
301
|
+
def values_for_pattern(pattern) = @mapping[pattern] # :nodoc:
|
|
302
|
+
|
|
303
|
+
protected
|
|
304
|
+
|
|
305
|
+
attr_reader :mapping
|
|
306
|
+
|
|
307
|
+
private
|
|
308
|
+
|
|
309
|
+
def add_pattern(pattern)
|
|
310
|
+
case @use_trie
|
|
311
|
+
when true
|
|
312
|
+
@matcher ||= Trie.new(self, @mapping.keys)
|
|
313
|
+
when Integer
|
|
314
|
+
if @mapping.size >= @use_trie
|
|
315
|
+
@matcher = Trie.new(self, @mapping.keys)
|
|
316
|
+
@use_trie = true
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
@matcher ||= Linear.new(self, @mapping.keys)
|
|
321
|
+
@matcher = Cache.new(@matcher) if @use_cache and not @matcher.is_a? Cache
|
|
322
|
+
@matcher.add(pattern)
|
|
323
|
+
|
|
324
|
+
@expanders[self]&.add(pattern)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
data/lib/mustermann/version.rb
CHANGED
data/lib/mustermann.rb
CHANGED
|
@@ -115,10 +115,4 @@ module Mustermann
|
|
|
115
115
|
def self.normalized_type(type)
|
|
116
116
|
type.to_s.gsub('-', '_').downcase
|
|
117
117
|
end
|
|
118
|
-
|
|
119
|
-
# @!visibility private
|
|
120
|
-
def self.extend_object(object)
|
|
121
|
-
return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base
|
|
122
|
-
require 'mustermann/extension'
|
|
123
|
-
end
|
|
124
118
|
end
|
metadata
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mustermann
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0.alpha
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Haase
|
|
8
|
+
- Kunpei Sakai
|
|
9
|
+
- Patrik Ragnarsson
|
|
10
|
+
- Jordan Owens
|
|
8
11
|
- Zachary Scott
|
|
9
12
|
bindir: bin
|
|
10
13
|
cert_chain: []
|
|
11
14
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
15
|
dependencies: []
|
|
13
|
-
description:
|
|
16
|
+
description: |
|
|
17
|
+
Mustermann is your personal string matching expert. As an expert in the field of strings and patterns,
|
|
18
|
+
Mustermann keeps its runtime dependencies to a minimum and is fully covered with specs and documentation.
|
|
19
|
+
|
|
20
|
+
Given a string pattern, Mustermann will turn it into an object that behaves like a regular expression
|
|
21
|
+
and has comparable performance characteristics.
|
|
14
22
|
email: sinatrarb@googlegroups.com
|
|
15
23
|
executables: []
|
|
16
24
|
extensions: []
|
|
@@ -36,27 +44,34 @@ files:
|
|
|
36
44
|
- lib/mustermann/equality_map.rb
|
|
37
45
|
- lib/mustermann/error.rb
|
|
38
46
|
- lib/mustermann/expander.rb
|
|
39
|
-
- lib/mustermann/
|
|
47
|
+
- lib/mustermann/hybrid.rb
|
|
40
48
|
- lib/mustermann/identity.rb
|
|
41
|
-
- lib/mustermann/
|
|
49
|
+
- lib/mustermann/match.rb
|
|
42
50
|
- lib/mustermann/pattern.rb
|
|
43
|
-
- lib/mustermann/pattern_cache.rb
|
|
44
51
|
- lib/mustermann/rails.rb
|
|
45
52
|
- lib/mustermann/regexp.rb
|
|
46
53
|
- lib/mustermann/regexp_based.rb
|
|
47
54
|
- lib/mustermann/regular.rb
|
|
48
|
-
- lib/mustermann/
|
|
55
|
+
- lib/mustermann/router.rb
|
|
56
|
+
- lib/mustermann/set.rb
|
|
57
|
+
- lib/mustermann/set/cache.rb
|
|
58
|
+
- lib/mustermann/set/linear.rb
|
|
59
|
+
- lib/mustermann/set/match.rb
|
|
60
|
+
- lib/mustermann/set/trie.rb
|
|
49
61
|
- lib/mustermann/sinatra.rb
|
|
50
62
|
- lib/mustermann/sinatra/parser.rb
|
|
51
63
|
- lib/mustermann/sinatra/safe_renderer.rb
|
|
52
64
|
- lib/mustermann/sinatra/try_convert.rb
|
|
53
|
-
- lib/mustermann/to_pattern.rb
|
|
54
65
|
- lib/mustermann/version.rb
|
|
55
66
|
- lib/mustermann/versions.rb
|
|
56
67
|
homepage: https://github.com/sinatra/mustermann
|
|
57
68
|
licenses:
|
|
58
69
|
- MIT
|
|
59
|
-
metadata:
|
|
70
|
+
metadata:
|
|
71
|
+
bug_tracker_uri: https://github.com/sinatra/mustermann/issues
|
|
72
|
+
changelog_uri: https://github.com/sinatra/mustermann/blob/main/CHANGELOG.md
|
|
73
|
+
documentation_uri: https://github.com/sinatra/mustermann/tree/main/mustermann#readme
|
|
74
|
+
source_code_uri: https://github.com/sinatra/mustermann/tree/main/mustermann
|
|
60
75
|
rdoc_options: []
|
|
61
76
|
require_paths:
|
|
62
77
|
- lib
|
|
@@ -64,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
64
79
|
requirements:
|
|
65
80
|
- - ">="
|
|
66
81
|
- !ruby/object:Gem::Version
|
|
67
|
-
version:
|
|
82
|
+
version: 3.3.0
|
|
68
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
69
84
|
requirements:
|
|
70
85
|
- - ">="
|
data/lib/mustermann/extension.rb
DELETED