mustermann 4.0.0.alpha4 → 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 +93 -0
- data/lib/mustermann/ast/compiler.rb +23 -20
- data/lib/mustermann/ast/converters.rb +41 -0
- 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/match.rb +7 -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
|
@@ -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
|
|
@@ -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/match.rb
CHANGED
|
@@ -74,10 +74,13 @@ module Mustermann
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# @return [Array<String>] the names of the named captures
|
|
78
|
+
def names = named_captures.keys
|
|
79
|
+
|
|
77
80
|
# @overload [](key)
|
|
78
|
-
# Access
|
|
81
|
+
# Access named captures by key.
|
|
79
82
|
# @param key [String, Symbol] the key to access
|
|
80
|
-
# @return the value of the
|
|
83
|
+
# @return the value of the named capture, or nil if not found
|
|
81
84
|
#
|
|
82
85
|
# @overload [](index)
|
|
83
86
|
# Access captures by index.
|
|
@@ -96,8 +99,8 @@ module Mustermann
|
|
|
96
99
|
# @return [Array] the values of the captures
|
|
97
100
|
def [](key, length = nil)
|
|
98
101
|
case key
|
|
99
|
-
when String then
|
|
100
|
-
when Symbol then
|
|
102
|
+
when String then named_captures[key]
|
|
103
|
+
when Symbol then named_captures[key.to_s]
|
|
101
104
|
when Integer then length ? captures[key, length] : captures[key]
|
|
102
105
|
when Range then captures[key]
|
|
103
106
|
else raise ArgumentError, "key must be a String, Symbol, Integer, or Range, not #{key.class}"
|
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
|