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 +4 -4
- data/README.md +114 -21
- data/lib/mustermann/ast/compiler.rb +23 -20
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/fast_pattern.rb +1 -1
- data/lib/mustermann/ast/param_scanner.rb +38 -6
- data/lib/mustermann/ast/pattern.rb +3 -3
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/concat.rb +10 -5
- data/lib/mustermann/match.rb +112 -15
- data/lib/mustermann/pattern.rb +2 -2
- data/lib/mustermann/regexp_based.rb +5 -5
- data/lib/mustermann/set/linear.rb +2 -2
- data/lib/mustermann/set/match.rb +10 -10
- data/lib/mustermann/set/trie.rb +5 -4
- data/lib/mustermann/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a382e55dcc3919ca87dfa480d405b5fc126435c8081ce37e270a63e5907e7517
|
|
4
|
+
data.tar.gz: 4ec389e09c7a625fc83acf00fa9ff98a08e05cb58c1fb5c43772b14605a92e02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
def with_lookahead(string, lookahead: nil, **options)
|
|
116
|
+
lookahead ? "(?:(?!#{lookahead})#{string})" : string
|
|
114
117
|
end
|
|
115
118
|
|
|
116
|
-
def from_hash(hash,
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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)
|
|
16
|
-
translate(
|
|
17
|
-
translate(
|
|
18
|
-
translate(
|
|
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
|
|
data/lib/mustermann/concat.rb
CHANGED
|
@@ -75,16 +75,21 @@ module Mustermann
|
|
|
75
75
|
|
|
76
76
|
# @see Mustermann::Pattern#peek_match
|
|
77
77
|
def peek_match(string)
|
|
78
|
-
|
|
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(
|
|
84
|
+
return unless part = pattern.peek_match(post_match)
|
|
83
85
|
params.merge!(part.params)
|
|
84
|
-
|
|
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
|
-
|
|
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
|
data/lib/mustermann/match.rb
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
18
|
-
when Symbol then
|
|
19
|
-
|
|
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
|
data/lib/mustermann/pattern.rb
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
|
22
|
+
values.each { |value| result << Match.new(match, value:) }
|
|
23
23
|
end
|
|
24
24
|
result
|
|
25
25
|
end
|
data/lib/mustermann/set/match.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/mustermann/set/trie.rb
CHANGED
|
@@ -148,7 +148,7 @@ module Mustermann
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
if peek
|
|
151
|
-
matches = build_matches(string
|
|
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?(
|
|
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
|
data/lib/mustermann/version.rb
CHANGED
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.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
|