mustermann 4.0.0 → 4.0.1

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