mustermann 4.0.0.alpha3 → 4.0.0.beta1

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: c6a0beea1e6d365356c6444910ec10200dcaf98f1968e88ac2fe91e0a7b23b93
4
- data.tar.gz: 1be43d10761af60bbd4cdd5d087214073984825b05f397de8d1475a7f35686d6
3
+ metadata.gz: a382e55dcc3919ca87dfa480d405b5fc126435c8081ce37e270a63e5907e7517
4
+ data.tar.gz: 4ec389e09c7a625fc83acf00fa9ff98a08e05cb58c1fb5c43772b14605a92e02
5
5
  SHA512:
6
- metadata.gz: 710a7afa55bdbd0f6fb17845458572ea9adaf8149674c9b1e54ddba333382be9e05136d37d7680595bead97b21bee76334d129cbf96c320b0f975688b20ad29c
7
- data.tar.gz: 64e15b87da6f731fce7d0e50eb4ee13511c175b979765d9a233a066c026026c0c82300e22b8492db8220d2f89995cadb471c78ac00da60ea8be360a22b8290be
6
+ metadata.gz: 7c8595c1df98b37d768691d0e1b591edd5a7b219aeff7569ee6e7e925b29ef2065125e7845d487e4b3aab68e2b7c06ffc7d696c74433a82b0d6012dd80a1d88e
7
+ data.tar.gz: 8f4bf30d82cd54534a5f4e31c1843868e1a0fe0112be8656167321b6554baac4d8455e947146c3ec5f9340f8af20ca8edd79499630a571b2a39851ddaef55f38
data/README.md CHANGED
@@ -455,27 +455,6 @@ set.expand(id: '5') # => '/users/5' (first applicable pattern)
455
455
  set.expand(:posts, id: '5') # => '/posts/5' (patterns for a specific value)
456
456
  ```
457
457
 
458
- <a name="-duck-typing"></a>
459
- ## Duck Typing
460
-
461
- <a name="-duck-typing-to-pattern"></a>
462
- ### `to_pattern`
463
-
464
- All methods converting string input to pattern objects will also accept any arbitrary object that implements `to_pattern`:
465
-
466
- ``` ruby
467
- require 'mustermann'
468
-
469
- class MyObject
470
- def to_pattern(**options)
471
- Mustermann.new("/foo", **options)
472
- end
473
- end
474
-
475
- object = MyObject.new
476
- Mustermann.new(object, type: :rails) # => #<Mustermann::Rails:"/foo">
477
- ```
478
-
479
458
  ### Match order
480
459
 
481
460
  A set can match patterns and values in loose or strict insertion order.
@@ -536,6 +515,27 @@ set.match("/static").value # => :first
536
515
  set.match_all("/static").map(&:value) # => [:first, :second, :third]
537
516
  ```
538
517
 
518
+ <a name="-duck-typing"></a>
519
+ ## Duck Typing
520
+
521
+ <a name="-duck-typing-to-pattern"></a>
522
+ ### `to_pattern`
523
+
524
+ All methods converting string input to pattern objects will also accept any arbitrary object that implements `to_pattern`:
525
+
526
+ ``` ruby
527
+ require 'mustermann'
528
+
529
+ class MyObject
530
+ def to_pattern(**options)
531
+ Mustermann.new("/foo", **options)
532
+ end
533
+ end
534
+
535
+ object = MyObject.new
536
+ Mustermann.new(object, type: :rails) # => #<Mustermann::Rails:"/foo">
537
+ ```
538
+
539
539
  <a name="-duck-typing-respond-to"></a>
540
540
  ### `respond_to?`
541
541
 
@@ -595,6 +595,99 @@ Mustermann.new('/:id.:ext', capture: { id: /\d+/, ext: ['png', 'jpg'] })
595
595
 
596
596
  Available POSIX character classes are: `:alnum`, `:alpha`, `:blank`, `:cntrl`, `:digit`, `:graph`, `:lower`, `:print`, `:punct`, `:space`, `:upper`, `:xdigit`, `:word` and `:ascii`.
597
597
 
598
+ #### Typed Captures
599
+
600
+ Certain Ruby classes and named symbols can be passed as a capture value. They constrain what the capture matches **and** automatically convert the captured string in `params` to the appropriate type.
601
+
602
+ ``` ruby
603
+ require 'mustermann'
604
+ require 'date'
605
+
606
+ # Integer: only matches integers, converts to Integer in params
607
+ pattern = Mustermann.new('/:id', capture: Integer)
608
+ pattern.match('/42') # matches
609
+ pattern.match('/foo') # does not match
610
+ pattern.params('/42') # => { "id" => 42 }
611
+
612
+ # Float: matches integers and decimals, converts to Float in params
613
+ pattern = Mustermann.new('/:price', capture: Float)
614
+ pattern.params('/3.14') # => { "price" => 3.14 }
615
+ pattern.params('/5') # => { "price" => 5.0 }
616
+
617
+ # Symbol: only matches word characters (\w+), converts to Symbol in params
618
+ pattern = Mustermann.new('/:format', capture: Symbol)
619
+ pattern.params('/json') # => { "format" => :json }
620
+ pattern.match('/with-hyphen') # does not match
621
+
622
+ # Date: only matches YYYY-MM-DD dates, converts to Date in params
623
+ pattern = Mustermann.new('/:date', capture: Date)
624
+ pattern.params('/2026-04-23') # => { "date" => #<Date: 2026-04-23> }
625
+ pattern.match('/04-23-2026') # does not match
626
+
627
+ # Gem::Version: matches version strings, converts to Gem::Version in params
628
+ require 'rubygems/version'
629
+ pattern = Mustermann.new('/:version', capture: Gem::Version)
630
+ pattern.params('/1.2.3') # => { "version" => #<Gem::Version "1.2.3"> }
631
+ ```
632
+
633
+ Lowercase symbol aliases are also available: `:integer`, `:float`, `:symbol`, `:date`, `:version`. They behave identically to their class counterparts:
634
+
635
+ ``` ruby
636
+ pattern = Mustermann.new('/:id', capture: :integer)
637
+ pattern.params('/42') # => { "id" => 42 }
638
+ ```
639
+
640
+ These can be mixed with other capture types in a hash:
641
+
642
+ ``` ruby
643
+ pattern = Mustermann.new('/:id(.:format)?', capture: { id: Integer, format: :slug })
644
+ pattern.params('/42') # => { "id" => 42, "format" => nil }
645
+ pattern.params('/42.json') # => { "id" => 42, "format" => "json" }
646
+ ```
647
+
648
+ Like all other capture types, these can also be used in an array:
649
+
650
+ ``` ruby
651
+ pattern = Mustermann.new('/score/:score', capture: [Integer, Float])
652
+ pattern.params('/42') # => { "score" => 42 }
653
+ pattern.params('/3.14') # => { "score" => 3.14 }
654
+ ```
655
+
656
+ #### Other Symbols
657
+
658
+ The following symbols constrain the capture with a regex but do **not** perform any type conversion — `params` still returns a string:
659
+
660
+ | Symbol | Matches |
661
+ |-----------|---------|
662
+ | `:locale` | BCP 47 language tags (`en`, `en-US`, `zh-Hans-CN`) |
663
+ | `:slug` | Lowercase URL slugs (`hello-world`, `foo-bar-baz`) |
664
+ | `:uuid` | UUIDs (`f47ac10b-58cc-4372-a567-0e02b2c3d479`, case-insensitive) |
665
+
666
+ ``` ruby
667
+ Mustermann.new('/:lang', capture: :locale).match('/zh-Hans-CN') # matches
668
+ Mustermann.new('/:slug', capture: :slug).match('/Hello') # does not match
669
+ Mustermann.new('/:id', capture: :uuid).match('/not-a-uuid') # does not match
670
+ ```
671
+
672
+ Again, these can be mixed with other capture types in a hash or array:
673
+
674
+ ```ruby
675
+ set = Mustermann::Set.new(capture: { id: [Integer, :uuid], locale: :locale })
676
+
677
+ set.add("(/:locale)?/:id", :show)
678
+ set.add("/(:locale)?", :index)
679
+
680
+ # without capture constraints, this would match the :show pattern instead
681
+ match = set.match('/en')
682
+ match.value # => :index
683
+
684
+ match = set.match('/f47ac10b-58cc-4372-a567-0e02b2c3d479')
685
+ match.value # => :show
686
+
687
+ match = set.match('/en/12')
688
+ match.value # => :show
689
+ ```
690
+
598
691
  <a name="-available-options--except"></a>
599
692
  ### `except`
600
693
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mustermann/ast/translator'
4
+ require 'mustermann/ast/converters'
4
5
 
5
6
  module Mustermann
6
7
  # @see Mustermann::AST::Pattern
@@ -99,46 +100,48 @@ module Mustermann
99
100
  when Hash then from_hash(capture, **options)
100
101
  when String then from_string(capture, **options)
101
102
  when nil then from_nil(**options)
102
- else capture
103
+ when Regexp then capture
104
+ when Class then from_class(capture, **options)
105
+ else raise CompileError, "invalid capture constraint %p for %p" % [capture, name]
103
106
  end
104
107
  end
105
108
 
106
109
  private
107
110
 
108
- def qualified(string, greedy: true,
109
- **options) "#{string}#{qualifier || "+#{'?' unless greedy}"}"
111
+ def qualified(string, greedy: true, **options)
112
+ "#{string}#{qualifier || "+#{'?' unless greedy}"}"
110
113
  end
111
114
 
112
- def with_lookahead(string, lookahead: nil,
113
- **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string
115
+ def with_lookahead(string, lookahead: nil, **options)
116
+ lookahead ? "(?:(?!#{lookahead})#{string})" : string
114
117
  end
115
118
 
116
- def from_hash(hash,
117
- **options) pattern(capture: hash[name.to_sym],
118
- **options)
119
+ def from_hash(hash, **options)
120
+ pattern(capture: hash[name.to_sym], **options)
119
121
  end
120
122
 
121
123
  def from_array(array, **options)
122
- Regexp.union(*array.map do |e|
123
- pattern(capture: e, **options)
124
- end)
124
+ Regexp.union(*array.map { |e| pattern(capture: e, **options) })
125
125
  end
126
126
 
127
- def from_symbol(symbol,
128
- **options) qualified(with_lookahead("[[:#{symbol}:]]", **options),
129
- **options)
127
+ def from_symbol(symbol, **options)
128
+ capture, _ = CONVERTERS[symbol]
129
+ return pattern(capture:, **options) if capture
130
+ qualified(with_lookahead("[[:#{symbol}:]]", **options), **options)
130
131
  end
131
132
 
132
133
  def from_string(string, **options)
133
- Regexp.new(string.chars.map do |c|
134
- t.encoded(c, **options)
135
- end.join)
134
+ Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join)
136
135
  end
137
136
 
138
137
  def from_nil(**options)
139
- qualified(
140
- with_lookahead(default(**options), **options), **options
141
- )
138
+ qualified(with_lookahead(default(**options), **options), **options)
139
+ end
140
+
141
+ def from_class(klass, **options)
142
+ capture, _ = CONVERTERS[klass.name]
143
+ raise CompileError, "no converter for class %p" % klass unless capture
144
+ pattern(capture:, **options)
142
145
  end
143
146
 
144
147
  def default(**options) = constraint || '[^/\\?#]'
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ require "rubygems/version"
3
+ require "date"
4
+
5
+ module Mustermann
6
+ module AST
7
+ CONVERTERS = {
8
+ "Integer" => [ /-?\d+/, :to_i ],
9
+ "Symbol" => [ /\w+/, :to_sym ],
10
+ "String" => [ nil, :to_s ],
11
+ "Float" => [ /-?\d+(?:\.\d+)?/, :to_f ],
12
+
13
+ "Date" => [
14
+ /\d{4}-\d{2}-\d{2}/,
15
+ ->(string) { Date.parse(string) }
16
+ ],
17
+
18
+ "Gem::Version" => [
19
+ Regexp.new(Gem::Version::VERSION_PATTERN),
20
+ ->(string) { Gem::Version.new(string) }
21
+ ],
22
+
23
+ locale: [ /(?:[A-Za-z]{2,3}|i)(-[A-Za-z0-9]{1,8})*/ ],
24
+ slug: [ /[a-z0-9]+(?:-[a-z0-9]+)*/ ],
25
+ uuid: [ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i ],
26
+ }
27
+
28
+ CONVERTERS.merge!({
29
+ integer: CONVERTERS["Integer"],
30
+ symbol: CONVERTERS["Symbol"],
31
+ string: CONVERTERS["String"],
32
+ float: CONVERTERS["Float"],
33
+ date: CONVERTERS["Date"],
34
+ version: CONVERTERS["Gem::Version"],
35
+ })
36
+
37
+ CONVERTERS.freeze
38
+
39
+ private_constant :CONVERTERS
40
+ end
41
+ end
@@ -34,7 +34,7 @@ module Mustermann
34
34
  return unless match = @regexp.match(string)
35
35
  params = match.named_captures
36
36
  params.transform_values! { |v| unescape(v) } if string.include?('%')
37
- Match.new(self, string, params)
37
+ Match.new(self, match, params:)
38
38
  end
39
39
 
40
40
  # Public override: fast path for simple patterns, falls through to super otherwise.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'mustermann/ast/converters'
2
3
  require 'mustermann/ast/translator'
3
4
 
4
5
  module Mustermann
@@ -8,14 +9,45 @@ module Mustermann
8
9
  # @see Mustermann::AST::Pattern#to_templates
9
10
  class ParamScanner < Translator
10
11
  # @!visibility private
11
- def self.scan_params(ast)
12
- new.translate(ast)
12
+ def self.scan_params(ast, options)
13
+ new.translate(ast, options)
13
14
  end
14
15
 
15
- translate(:node) { t(payload) }
16
- translate(Array) { map { |e| t(e) }.inject(:merge) }
17
- translate(Object) { {} }
18
- translate(:capture) { convert ? { name => convert } : {} }
16
+ translate(:node) { |o| t(payload, o) }
17
+ translate(:with_look_ahead) { |o| t(head, o).merge(t(payload, o)) }
18
+ translate(Array) { |o| map { |e| t(e, o) }.inject(:merge) }
19
+ translate(Object) { |o| {} }
20
+
21
+ class Capture < NodeTranslator
22
+ register :capture
23
+
24
+ def translate(options)
25
+ return { name => convert } if convert
26
+ _, converter = converter(options[:capture])
27
+ converter ? { name => converter } : {}
28
+ end
29
+
30
+ def converter(capture)
31
+ case capture
32
+ when Hash then return converter(capture[name.to_sym])
33
+ when Class then regexp, converter = CONVERTERS[capture.name]
34
+ when Symbol then regexp, converter = CONVERTERS[capture]
35
+ when Array
36
+ entries = capture.map { |item| converter(item) }.compact
37
+ regexp = Regexp.union(entries.map(&:first))
38
+
39
+ entries.map! { |r, c| [/\A#{r}\Z/, c] }
40
+
41
+ converter = ->(string) do
42
+ _, c = entries.find { |r, _| r.match?(string) }
43
+ c&.call(string) || string
44
+ end
45
+ end
46
+
47
+ return unless converter
48
+ [regexp, converter.to_proc]
49
+ end
50
+ end
19
51
  end
20
52
  end
21
53
  end
@@ -101,13 +101,13 @@ module Mustermann
101
101
  # Internal AST representation of pattern.
102
102
  # @!visibility private
103
103
  def to_ast
104
- ast = self.class.ast_cache.fetch(@string) do
104
+ ast = self.class.ast_cache.fetch([@string, options]) do
105
105
  ast = parse(@string, pattern: self)
106
106
  ast &&= transform(ast)
107
107
  ast &&= set_boundaries(ast, string: @string)
108
108
  validate(ast)
109
109
  end
110
- @param_converters ||= scan_params(ast) if ast
110
+ @param_converters ||= scan_params(ast, options) if ast
111
111
  ast
112
112
  end
113
113
 
@@ -143,7 +143,7 @@ module Mustermann
143
143
 
144
144
  # @!visibility private
145
145
  def param_converters
146
- @param_converters ||= scan_params(to_ast)
146
+ @param_converters ||= scan_params(to_ast, options)
147
147
  end
148
148
 
149
149
  # @api private
@@ -22,6 +22,13 @@ module Mustermann
22
22
  node
23
23
  end
24
24
 
25
+ # eliminate redundant optional nodes - this helps avoid regexp warnings
26
+ translate(:optional) do
27
+ return t(payload) if payload.is_a? Node[:optional]
28
+ node.payload = t(payload)
29
+ node
30
+ end
31
+
25
32
  # ignore unknown objects on the tree
26
33
  translate(Object) { node }
27
34
 
@@ -75,16 +75,21 @@ module Mustermann
75
75
 
76
76
  # @see Mustermann::Pattern#peek_match
77
77
  def peek_match(string)
78
- substring = string
79
- params = {}
78
+ post_match = string
79
+ params = {}
80
+ captures = []
81
+ named_captures = {}
80
82
 
81
83
  patterns.each do |pattern|
82
- return unless part = pattern.peek_match(substring)
84
+ return unless part = pattern.peek_match(post_match)
83
85
  params.merge!(part.params)
84
- substring = substring[part.to_s.size..-1]
86
+ named_captures.merge!(part.named_captures)
87
+ captures.concat(part.captures)
88
+ post_match = post_match[part.to_s.size..-1]
85
89
  end
86
90
 
87
- Match.new(self, string[0, string.size - substring.size], params, post_match: substring)
91
+ matched = string[0, string.size - post_match.size]
92
+ Match.new(self, string, matched:, params:, captures:, named_captures:, post_match:)
88
93
  end
89
94
 
90
95
  # @see Mustermann::Pattern#peek_params
@@ -1,39 +1,136 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
6
  class Match
5
- attr_reader :pattern, :string, :params, :post_match, :pre_match
6
-
7
- def initialize(pattern, string, params = {}, post_match: '', pre_match: '')
8
- @pattern = pattern
9
- @string = string.freeze
10
- @params = params.freeze
11
- @post_match = post_match.freeze
12
- @pre_match = pre_match.freeze
7
+ # @return [Mustermann::Pattern] the pattern that produced the match
8
+ attr_reader :pattern
9
+
10
+ # @return [String] the string that was matched
11
+ attr_reader :string
12
+
13
+ # @return [Hash] the params hash
14
+ attr_reader :params
15
+
16
+ # @return [Array] the captures array
17
+ attr_reader :captures
18
+
19
+ # @return [Hash] the named captures hash, usually identical to {#params}
20
+ attr_reader :named_captures
21
+
22
+ # @return [String] the post match string
23
+ attr_reader :post_match
24
+
25
+ # @return [String] the pre match string
26
+ attr_reader :pre_match
27
+
28
+ # @return [Regexp, nil] the regular expression that produced the match, if available
29
+ attr_reader :regexp
30
+
31
+ # @overload initialize(pattern, string, **options)
32
+ # @param pattern [Mustermann::Pattern] the pattern that produced the match
33
+ # @param string [String] the string that was matched
34
+ #
35
+ # @overload initialize(match, **options)
36
+ # @param match [Mustermann::Match] the match to copy pattern and string from
37
+ #
38
+ # @overload initialize(pattern, match, **options)
39
+ # @param match [Mustermann::Match, MatchData] the match to copy string from
40
+ #
41
+ # @option options [Array] :captures the captures array
42
+ # @option options [Hash] :named_captures the named captures hash
43
+ # @option options [String] :matched the matched substring (defaults to string for full matches)
44
+ # @option options [Hash] :params the params hash
45
+ # @option options [Regexp] :regexp the regular expression that produced the match
46
+ # @option options [String] :post_match the post match string
47
+ # @option options [String] :pre_match the pre match string
48
+ def initialize(pattern_or_match, string_or_match = nil, matched: nil, params: nil, post_match: nil, pre_match: nil, captures: nil, named_captures: nil, regexp: nil)
49
+ case pattern_or_match
50
+ when Mustermann::Match, MatchData then match = pattern_or_match
51
+ when Mustermann::Pattern then pattern = pattern_or_match
52
+ else raise ArgumentError, "first argument must be a Mustermann::Pattern or a MatchData, not #{pattern_or_match.class}"
53
+ end
54
+
55
+ case string_or_match
56
+ when Mustermann::Match, MatchData then match ||= string_or_match
57
+ when String then string = string_or_match
58
+ when nil # ignore
59
+ else raise ArgumentError, "second argument must be a String or a MatchData, not #{string_or_match.class}"
60
+ end
61
+
62
+ @pattern = pattern || match&.pattern
63
+ @string = string || match&.string || ''
64
+ @params = params || match&.params || {}
65
+ @post_match = post_match || match&.post_match || ''
66
+ @pre_match = pre_match || match&.pre_match || ''
67
+ @captures = captures || match&.captures || @params.values
68
+ @named_captures = named_captures || match&.named_captures || @params
69
+ @matched = matched || match&.to_s || @string
70
+
71
+ unless @regexp = regexp
72
+ @regexp = match.regexp if match.respond_to?(:regexp)
73
+ @regexp ||= pattern.respond_to?(:regexp) ? pattern.regexp : nil
74
+ end
13
75
  end
14
76
 
15
- def [](key)
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)
16
101
  case key
17
- when String then params[key]
18
- when Symbol then params[key.to_s]
19
- else raise ArgumentError, "key must be a String or Symbol, not #{key.class}"
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}"
20
107
  end
21
108
  end
22
-
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
23
114
  def deconstruct_keys(keys) = keys.to_h { |key| [key, self[key]] }
24
115
 
116
+ # @see Object#hash
25
117
  def hash = pattern.hash ^ string.hash ^ params.hash
26
118
 
119
+ # @see Object#eql?
27
120
  def eql?(other)
28
121
  return false unless other.is_a? self.class
29
122
  pattern == other.pattern && string == other.string && params == other.params
30
123
  end
31
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
32
128
  def values_at(*keys) = keys.map { |key| self[key] }
33
129
 
130
+ # @return [String] the matched substring (like MatchData#to_s)
131
+ def to_s = @matched
132
+
34
133
  alias == eql?
35
- alias to_s string
36
134
  alias to_h params
37
-
38
135
  end
39
136
  end
@@ -165,7 +165,7 @@ module Mustermann
165
165
  # @see #peek_params
166
166
  def peek_match(string)
167
167
  matched = peek(string)
168
- Match.new(self, matched, {}, post_match: string[matched.size..-1]) if matched
168
+ Match.new(self, string, matched:, params: {}, post_match: string[matched.size..-1]) if matched
169
169
  end
170
170
 
171
171
  # Tries to match the pattern against the beginning of the string (as opposed to the full string).
@@ -182,7 +182,7 @@ module Mustermann
182
182
  # @return [Array<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
183
183
  def peek_params(string)
184
184
  match = peek_match(string)
185
- match ? [match.params, match.string.size] : nil
185
+ match ? [match.params, match.to_s.size] : nil
186
186
  end
187
187
 
188
188
  # @param [String] string the string to match against
@@ -35,10 +35,10 @@ module Mustermann
35
35
  # @see (see Mustermann::Pattern#peek_match)
36
36
  def peek_match(string) = build_match(@peek_regexp.match(string))
37
37
 
38
- def match(string)
39
- return unless match = @regexp.match(string)
40
- Match.new(self, string, build_params(match))
41
- end
38
+ # @param (see Mustermann::Pattern#match)
39
+ # @return (see Mustermann::Pattern#match)
40
+ # @see (see Mustermann::Pattern#match)
41
+ def match(string) = build_match(@regexp.match(string))
42
42
 
43
43
  extend Forwardable
44
44
  def_delegators :regexp, :===, :=~, :names
@@ -47,7 +47,7 @@ module Mustermann
47
47
 
48
48
  def build_match(match)
49
49
  return unless match
50
- Match.new(self, match.to_s, build_params(match), post_match: match.post_match, pre_match: match.pre_match)
50
+ Match.new(self, match, params: build_params(match))
51
51
  end
52
52
 
53
53
  def build_params(match)
@@ -17,9 +17,9 @@ module Mustermann
17
17
  result = [] if all
18
18
  @patterns.each do |pattern|
19
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
20
+ return Match.new(match, value: @set.values_for_pattern(pattern)&.first) unless all
21
21
  values = @set.values_for_pattern(pattern) || [nil]
22
- values.each { |value| result << Match.new(match:, value:) }
22
+ values.each { |value| result << Match.new(match, value:) }
23
23
  end
24
24
  result
25
25
  end
@@ -3,21 +3,21 @@ require 'mustermann/match'
3
3
 
4
4
  module Mustermann
5
5
  class Set
6
+
7
+ # Subclass of {Mustermann::Match} that also includes the value associated with the pattern that produced the match.
6
8
  class Match < Mustermann::Match
9
+ # @return the value associated with the pattern that produced the match, if any
7
10
  attr_reader :value
8
11
 
9
- def initialize(pattern = nil, string = nil, params = {}, value: nil, match: nil, post_match: '', pre_match: '')
12
+ # (see Mustermann::Match#initialize)
13
+ # @option options [Object] :value the value associated with the pattern that produced the match, if any
14
+ def initialize(*, value: nil, **)
10
15
  @value = value
11
- if match
12
- @pattern = match.pattern
13
- @string = match.string
14
- @params = match.params
15
- @post_match = match.post_match
16
- @pre_match = match.pre_match
17
- else
18
- super(pattern, string, params, post_match:, pre_match:)
19
- end
16
+ super(*, **)
20
17
  end
18
+
19
+ # @see Mustermann::Match#eql?
20
+ def eql?(other) = super && value == other.value
21
21
  end
22
22
  end
23
23
  end
@@ -148,7 +148,7 @@ module Mustermann
148
148
  end
149
149
 
150
150
  if peek
151
- matches = build_matches(string[0, position], params, all:, post_match: string[position..], pre_match: '')
151
+ matches = build_matches(string, params, all:, matched_length: position, post_match: string[position..], pre_match: '')
152
152
  return matches unless all
153
153
  result.concat(matches)
154
154
  end
@@ -158,17 +158,18 @@ module Mustermann
158
158
 
159
159
  NIL_VALUES = [nil].freeze
160
160
 
161
- def build_matches(string, params, all: false, post_match: '', pre_match: '')
161
+ def build_matches(string, params, all: false, matched_length: string.size, post_match: '', pre_match: '')
162
162
  result = [] if all
163
+ matched = string[0, matched_length]
163
164
 
164
165
  @patterns.each do |pattern|
165
- next if pattern.except_regexp&.match?(string)
166
+ next if pattern.except_regexp&.match?(matched)
166
167
 
167
168
  pattern_params = build_pattern_params(pattern, params)
168
169
 
169
170
  values = @set.values_for_pattern(pattern) || NIL_VALUES
170
171
  values.each do |value|
171
- match = Set::Match.new(pattern, string, pattern_params, value:, post_match:, pre_match:)
172
+ match = Set::Match.new(pattern, string, matched:, params: pattern_params, value:, post_match:, pre_match:)
172
173
  return match unless all
173
174
  result << match
174
175
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Mustermann
3
- VERSION ||= '4.0.0.alpha3'
3
+ VERSION ||= '4.0.0.beta1'
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.alpha3
4
+ version: 4.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Haase
@@ -29,6 +29,7 @@ files:
29
29
  - lib/mustermann.rb
30
30
  - lib/mustermann/ast/boundaries.rb
31
31
  - lib/mustermann/ast/compiler.rb
32
+ - lib/mustermann/ast/converters.rb
32
33
  - lib/mustermann/ast/expander.rb
33
34
  - lib/mustermann/ast/fast_pattern.rb
34
35
  - lib/mustermann/ast/node.rb