mustermann 4.0.0.beta1 → 4.0.0.rc1

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: a382e55dcc3919ca87dfa480d405b5fc126435c8081ce37e270a63e5907e7517
4
- data.tar.gz: 4ec389e09c7a625fc83acf00fa9ff98a08e05cb58c1fb5c43772b14605a92e02
3
+ metadata.gz: cc7122898bc258ae00b79d44d7f81fb78e95da49f2c80000c1d04c69dc68ed7f
4
+ data.tar.gz: bc039a0b69440d1fc07253be7575f752938476386f08237c8984d9f21145b5ee
5
5
  SHA512:
6
- metadata.gz: 7c8595c1df98b37d768691d0e1b591edd5a7b219aeff7569ee6e7e925b29ef2065125e7845d487e4b3aab68e2b7c06ffc7d696c74433a82b0d6012dd80a1d88e
7
- data.tar.gz: 8f4bf30d82cd54534a5f4e31c1843868e1a0fe0112be8656167321b6554baac4d8455e947146c3ec5f9340f8af20ca8edd79499630a571b2a39851ddaef55f38
6
+ metadata.gz: 955b763efdad19885a42b27e706509f024329b9ff96707d17198407428fe75b531295cd01dc0e5f2a38aae2458fcd6c2180af98dbe71038e4d24f74a6a1fa67e
7
+ data.tar.gz: cbc49d4686b678c0f11898c4f5bd91d31d975551bbc6de1ba418de3be1ee3c6a093a4e5caa12edbd3e7bfdb1cfb9239d98b0ecc0fb5d68fa287f8e8f403242e7
data/README.md CHANGED
@@ -42,7 +42,7 @@ pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] }
42
42
  These features are included in the library, but not loaded by default
43
43
 
44
44
  * **[Pattern Set](#-pattern-set):** A collection of patterns with associated values, designed for building routing tables that dispatch efficiently as the number of routes grows.
45
- * **Mustermann::Router:** A very basic rack router built on top of `Mustermann::Set` for demonstration purposes. Simple and fast.
45
+ * **Mustermann::Router:** A very basic rack router built on top of `Mustermann::Set` for demonstration purposes or small-footprint routing in Rack middleware. Simple and fast.
46
46
 
47
47
  <a name="-pattern-types"></a>
48
48
  ## Pattern Types
@@ -120,7 +120,7 @@ require 'mustermann'
120
120
 
121
121
  pattern = Mustermann.new('/:page')
122
122
  pattern.match('/') # => nil
123
- pattern.match('/home') # => #<MatchData "/home" page:"home">
123
+ pattern.match('/home') # => #<Mustermann::Match>
124
124
  pattern =~ '/home' # => 0
125
125
  pattern === '/home' # => true (this allows using it in case statements)
126
126
 
@@ -723,10 +723,10 @@ Semi-greedy behavior is not specific to dots, it works with all characters or st
723
723
 
724
724
  ``` ruby
725
725
  pattern = Mustermann.new(':a.:b', greedy: true)
726
- pattern.match('a.b.c.d') # => #<MatchData a:"a.b.c" b:"d">
726
+ pattern.match('a.b.c.d') # => #<Mustermann::Match>
727
727
 
728
728
  pattern = Mustermann.new(':a.:b', greedy: false)
729
- pattern.match('a.b.c.d') # => #<MatchData a:"a" b:"b.c.d">
729
+ pattern.match('a.b.c.d') # => #<Mustermann::Match>
730
730
  ```
731
731
 
732
732
  <a name="-available-options--space_matches_plus"></a>
@@ -26,17 +26,6 @@ module Mustermann
26
26
 
27
27
  private_constant :SIMPLE, :ENCODED, :SEGMENT_SCAN
28
28
 
29
- # Bypasses the generic build_match overhead for simple patterns: uses
30
- # MatchData#named_captures directly and avoids match.to_s / post_match /
31
- # pre_match calls (all no-ops for \A…\Z anchored regexps).
32
- def match(string)
33
- return super unless @fast_match
34
- return unless match = @regexp.match(string)
35
- params = match.named_captures
36
- params.transform_values! { |v| unescape(v) } if string.include?('%')
37
- Match.new(self, match, params:)
38
- end
39
-
40
29
  # Public override: fast path for simple patterns, falls through to super otherwise.
41
30
  # Must remain public to match AST::Pattern#to_ast visibility.
42
31
  def to_ast
@@ -46,8 +35,24 @@ module Mustermann
46
35
  ast
47
36
  end
48
37
 
38
+ def params(string = nil)
39
+ return super unless @fast_match
40
+ return unless md = @regexp.match(string)
41
+ result = md.named_captures
42
+ result.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%')
43
+ result
44
+ end
45
+
49
46
  private
50
47
 
48
+ def build_match(regexp, string)
49
+ return super unless @fast_match
50
+ return unless match = regexp.match(string)
51
+ params = match.named_captures
52
+ params.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%')
53
+ Match.new(self, match, params: params)
54
+ end
55
+
51
56
  def simple_pattern?
52
57
  options[:capture].nil? &&
53
58
  options[:except].nil? &&
@@ -138,7 +138,8 @@ module Mustermann
138
138
  # @see Mustermann::Pattern#map_param
139
139
  def map_param(key, value)
140
140
  return super unless param_converters.include? key
141
- param_converters[key][super]
141
+ converted = super
142
+ converted.nil? ? converted : param_converters[key][converted]
142
143
  end
143
144
 
144
145
  # @!visibility private
@@ -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
- "#<%p:%s>" % [self.class, simple_inspect]
91
+ "(#{simple_inspect})"
89
92
  end
90
93
 
91
94
  # @!visibility private
92
95
  def simple_inspect
93
- pattern_strings = patterns.map { |p| p.simple_inspect }
94
- "(#{pattern_strings.join(" #{operator} ")})"
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
@@ -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
- return super unless native = native_concat(other)
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
 
@@ -159,6 +159,23 @@ module Mustermann
159
159
  end
160
160
  end
161
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
+
162
179
  # @see Object#==
163
180
  def ==(other)
164
181
  return false unless other.class == self.class
@@ -132,5 +132,24 @@ module Mustermann
132
132
 
133
133
  alias == eql?
134
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
135
154
  end
136
155
  end
@@ -90,6 +90,9 @@ module Mustermann
90
90
  Match.new(self, string) if self === string
91
91
  end
92
92
 
93
+ # @return [Array<String>] list of named captures in the pattern
94
+ def names = []
95
+
93
96
  # @param [String] string The string to match against
94
97
  # @return [Integer, nil] nil if pattern does not match the string, zero if it does.
95
98
  # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~
@@ -339,6 +342,13 @@ module Mustermann
339
342
  method(method).owner != Mustermann::Pattern
340
343
  end
341
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
+
342
352
  # @!visibility private
343
353
  def inspect
344
354
  "#<%p:%p>" % [self.class, @string]
@@ -45,5 +45,19 @@ module Mustermann
45
45
 
46
46
  # Rails 5.0 fixes |
47
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
48
62
  end
49
63
  end
@@ -11,15 +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 = compile(**options)
20
- @peek_regexp = /\A#{regexp}/
21
- @regexp = /\A#{regexp}\Z/
24
+ regexp = compile(**options)
25
+ @peek_regexp = /\A#{regexp}/
26
+ @regexp = /\A#{regexp}\Z/
22
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
23
45
  end
24
46
 
25
47
  # @param (see Mustermann::Pattern#peek_size)
@@ -33,20 +55,38 @@ module Mustermann
33
55
  # @param (see Mustermann::Pattern#peek_match)
34
56
  # @return (see Mustermann::Pattern#peek_match)
35
57
  # @see (see Mustermann::Pattern#peek_match)
36
- def peek_match(string) = build_match(@peek_regexp.match(string))
58
+ def peek_match(string) = cache_match(@peek_cache, @peek_regexp, string)
37
59
 
38
60
  # @param (see Mustermann::Pattern#match)
39
61
  # @return (see Mustermann::Pattern#match)
40
62
  # @see (see Mustermann::Pattern#match)
41
- def match(string) = build_match(@regexp.match(string))
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)
72
+ end
42
73
 
43
74
  extend Forwardable
44
75
  def_delegators :regexp, :===, :=~, :names
45
76
 
46
77
  private
47
78
 
48
- def build_match(match)
49
- return unless match
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)
50
90
  Match.new(self, match, params: build_params(match))
51
91
  end
52
92
 
@@ -185,6 +185,11 @@ module Mustermann
185
185
  # @return [self]
186
186
  # @raise [ArgumentError] if the pattern is not AST-based, or if a reserved symbol is used as a value
187
187
  def add(pattern, *values)
188
+ if pattern.is_a? Composite and pattern.operator == :|
189
+ pattern.patterns.each { |p| add(p, *values) }
190
+ return self
191
+ end
192
+
188
193
  pattern = Mustermann.new(pattern, **options)
189
194
  raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast
190
195
 
@@ -380,6 +385,38 @@ module Mustermann
380
385
  # Runs trie optimizations pro-actively and explicitly rather than at match time.
381
386
  def optimize! = @matcher&.optimize!
382
387
 
388
+ # @!visibility private
389
+ def inspect # :nodoc:
390
+ mapping = @mapping.map do |pattern, values|
391
+ if values.size > 1 or values.first.is_a? Array
392
+ "%p => %p" % [pattern.to_s, values]
393
+ elsif values.first != nil
394
+ "%p => %p" % [pattern.to_s, values.first]
395
+ else
396
+ "%p" % pattern.to_s
397
+ end
398
+ end
399
+ "#<#{self.class.name}: #{mapping.join(", ")}>"
400
+ end
401
+
402
+ # @!visibility private
403
+ def pretty_print(q) # :nodoc:
404
+ q.text "#<#{self.class.name}"
405
+ q.group(1, "", ">") do
406
+ @mapping.each_with_index do |(pattern, values), index|
407
+ q.breakable(index == 0 ? " " : ", ")
408
+ q.pp(pattern.to_s)
409
+ if values.size > 1 or values.first.is_a? Array
410
+ q.text " => "
411
+ q.pp(values)
412
+ elsif values.first != nil
413
+ q.text " => "
414
+ q.pp(values.first)
415
+ end
416
+ end
417
+ end
418
+ end
419
+
383
420
  protected
384
421
 
385
422
  attr_reader :mapping
@@ -6,24 +6,34 @@ module Mustermann
6
6
  class TryConvert < AST::Translator
7
7
  # @return [Mustermann::Sinatra, nil]
8
8
  # @!visibility private
9
- def self.convert(input, **options)
10
- new(options).translate(input)
9
+ def self.convert(type, input, **options)
10
+ new(type, **options).translate(input)
11
11
  end
12
12
 
13
+ # Reserved variable names.
14
+ # @!visibility private
15
+ attr_reader :names
16
+
13
17
  # Expected options for the resulting pattern.
14
18
  # @!visibility private
15
19
  attr_reader :options
16
20
 
21
+ # Expected pattern type for the resulting pattern.
22
+ # @!visibility private
23
+ attr_reader :type
24
+
17
25
  # @!visibility private
18
- def initialize(options)
26
+ def initialize(type, names: [], **options)
27
+ @names = names
19
28
  @options = options
29
+ @type = type
20
30
  end
21
31
 
22
32
  # @return [Mustermann::Sinatra]
23
33
  # @!visibility private
24
- def new(input, escape = false)
34
+ def new(input, escape: false, **opts)
25
35
  input = Mustermann::Sinatra.escape(input) if escape
26
- Mustermann::Sinatra.new(input, **options)
36
+ type.new(input, **opts, **options, ignore_unknown_options: true)
27
37
  end
28
38
 
29
39
  # @return [true, false] whether or not expected pattern should have uri_decode option set
@@ -32,15 +42,43 @@ module Mustermann
32
42
  options.fetch(:uri_decode, true)
33
43
  end
34
44
 
35
- translate(Object) { nil }
36
- translate(String) { t.new(self, true) }
45
+ # @return [true, false] whether or not the given options are compatible with the expected options
46
+ # @!visibility private
47
+ def compatible_options?(other_options)
48
+ other_options.all? do |key, value|
49
+ case key
50
+ when :capture then compatible_capture_option?(value)
51
+ else value == options[key]
52
+ end
53
+ end
54
+ end
37
55
 
38
- translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode }
39
- translate(Sinatra) { node if options == t.options }
56
+ # @return [true, false] whether or not the given capture option is compatible with the expected capture option
57
+ # @!visibility private
58
+ def compatible_capture_option?(capture)
59
+ return true if names.empty?
60
+ case capture
61
+ when Hash then capture.all? { |n, o| !names.include?(n.to_s) and compatible_capture_option?(o) }
62
+ when Array then capture.all? { |o| compatible_capture_option?(o) }
63
+ else true
64
+ end
65
+ end
66
+
67
+ translate(Object) { nil }
68
+ translate(String) { t.new(self, escape: true) }
69
+ translate(Identity) { t.new(self, escape: true) if uri_decode == t.uri_decode }
70
+
71
+ translate(Sinatra) do
72
+ if node.class == t.type and t.options == options
73
+ node
74
+ elsif t.compatible_options? options
75
+ t.new(to_s, **options)
76
+ end
77
+ end
40
78
 
41
79
  translate AST::Pattern do
42
- next unless options == t.options
43
- t.new(SafeRenderer.translate(to_ast)) rescue nil
80
+ next unless t.compatible_options? options
81
+ t.new(SafeRenderer.translate(to_ast), **options) rescue nil
44
82
  end
45
83
  end
46
84
 
@@ -37,13 +37,13 @@ module Mustermann
37
37
  # @return [Mustermann::Sinatra, nil] the converted pattern, if possible
38
38
  # @!visibility private
39
39
  def self.try_convert(input, **options)
40
- TryConvert.convert(input, **options)
40
+ TryConvert.convert(self, input, **options)
41
41
  end
42
42
 
43
43
  # Creates a pattern that matches any string matching either one of the patterns.
44
44
  # If a string is supplied, it is treated as a fully escaped Sinatra pattern.
45
45
  #
46
- # If the other pattern is also a Sintara pattern, it might join the two to a third
46
+ # If the other pattern is also a Sinatra pattern, it might join the two to a third
47
47
  # sinatra pattern instead of generating a composite for efficiency reasons.
48
48
  #
49
49
  # This only happens if the sinatra pattern behaves exactly the same as a composite
@@ -59,12 +59,12 @@ module Mustermann
59
59
  # @return [Mustermann::Pattern] a composite pattern
60
60
  # @see Mustermann::Pattern#|
61
61
  def |(other)
62
- return super unless converted = self.class.try_convert(other, **options)
63
- return super unless converted.names.empty? or names.empty?
64
- self.class.new(safe_string + "|" + converted.safe_string, **options)
62
+ return super unless converted = try_convert(other)
63
+ return super if converted.names.any? { |name| names.include?(name) }
64
+ self.class.new(safe_string + "|" + converted.safe_string, **converted.options)
65
65
  end
66
66
 
67
- # Generates a string represenation of the pattern that can safely be used for def interpolation
67
+ # Generates a string representation of the pattern that can safely be used for def interpolation
68
68
  # without changing its semantics.
69
69
  #
70
70
  # @example
@@ -72,19 +72,41 @@ module Mustermann
72
72
  # unsafe = Mustermann.new("/:name")
73
73
  #
74
74
  # Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" }
75
- # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "bar" }
75
+ # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "foo" }
76
76
  #
77
- # @return [String] string representatin of the pattern
77
+ # @return [String] string representations of the pattern
78
78
  def safe_string
79
79
  @safe_string ||= SafeRenderer.translate(to_ast)
80
80
  end
81
81
 
82
82
  # @!visibility private
83
83
  def native_concat(other)
84
- return unless converted = self.class.try_convert(other, **options)
85
- safe_string + converted.safe_string
84
+ return unless converted = try_convert(other)
85
+ [safe_string + converted.safe_string, converted.options]
86
86
  end
87
87
 
88
- private :native_concat
88
+ # @!visibility private
89
+ def normalize_capture(pattern)
90
+ case capture = pattern.options[:capture]
91
+ when Hash then capture.slice(*pattern.names.map(&:to_sym))
92
+ when nil then {}
93
+ else pattern.names.to_h { |name| [name.to_sym, capture] }
94
+ end
95
+ end
96
+
97
+ # @!visibility private
98
+ def try_convert(other)
99
+ options = self.options
100
+
101
+ if other.is_a? Pattern and other.names.any?
102
+ capture = normalize_capture(other).merge(normalize_capture(self))
103
+ capture = nil if capture.empty?
104
+ options = options.merge(capture:)
105
+ end
106
+
107
+ self.class.try_convert(other, names:, **options)
108
+ end
109
+
110
+ private :native_concat, :normalize_capture, :try_convert
89
111
  end
90
112
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Mustermann
3
- VERSION ||= '4.0.0.beta1'
3
+ VERSION ||= '4.0.0.rc1'
4
4
  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.beta1
4
+ version: 4.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Haase