mustermann 4.0.0.alpha4 → 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 +4 -4
- data/README.md +97 -4
- data/lib/mustermann/ast/compiler.rb +23 -20
- data/lib/mustermann/ast/converters.rb +41 -0
- data/lib/mustermann/ast/fast_pattern.rb +16 -11
- data/lib/mustermann/ast/param_scanner.rb +38 -6
- data/lib/mustermann/ast/pattern.rb +5 -4
- data/lib/mustermann/ast/transformer.rb +7 -0
- data/lib/mustermann/composite.rb +25 -6
- data/lib/mustermann/concat.rb +2 -2
- data/lib/mustermann/expander.rb +17 -0
- data/lib/mustermann/match.rb +26 -4
- data/lib/mustermann/pattern.rb +10 -0
- data/lib/mustermann/rails.rb +14 -0
- data/lib/mustermann/regexp_based.rb +47 -7
- data/lib/mustermann/set.rb +37 -0
- data/lib/mustermann/sinatra/try_convert.rb +49 -11
- data/lib/mustermann/sinatra.rb +33 -11
- 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: cc7122898bc258ae00b79d44d7f81fb78e95da49f2c80000c1d04c69dc68ed7f
|
|
4
|
+
data.tar.gz: bc039a0b69440d1fc07253be7575f752938476386f08237c8984d9f21145b5ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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') # => #<
|
|
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
|
|
|
@@ -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
|
|
|
@@ -630,10 +723,10 @@ Semi-greedy behavior is not specific to dots, it works with all characters or st
|
|
|
630
723
|
|
|
631
724
|
``` ruby
|
|
632
725
|
pattern = Mustermann.new(':a.:b', greedy: true)
|
|
633
|
-
pattern.match('a.b.c.d') # => #<
|
|
726
|
+
pattern.match('a.b.c.d') # => #<Mustermann::Match>
|
|
634
727
|
|
|
635
728
|
pattern = Mustermann.new(':a.:b', greedy: false)
|
|
636
|
-
pattern.match('a.b.c.d') # => #<
|
|
729
|
+
pattern.match('a.b.c.d') # => #<Mustermann::Match>
|
|
637
730
|
```
|
|
638
731
|
|
|
639
732
|
<a name="-available-options--space_matches_plus"></a>
|
|
@@ -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
|
|
@@ -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? &&
|
|
@@ -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
|
|
|
@@ -138,12 +138,13 @@ 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
|
-
|
|
141
|
+
converted = super
|
|
142
|
+
converted.nil? ? converted : param_converters[key][converted]
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
# @!visibility private
|
|
145
146
|
def param_converters
|
|
146
|
-
@param_converters ||= scan_params(to_ast)
|
|
147
|
+
@param_converters ||= scan_params(to_ast, options)
|
|
147
148
|
end
|
|
148
149
|
|
|
149
150
|
# @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/composite.rb
CHANGED
|
@@ -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
|
-
"
|
|
91
|
+
"(#{simple_inspect})"
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
# @!visibility private
|
|
92
95
|
def simple_inspect
|
|
93
|
-
|
|
94
|
-
|
|
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
|
data/lib/mustermann/concat.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
data/lib/mustermann/expander.rb
CHANGED
|
@@ -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
|
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}"
|
|
@@ -129,5 +132,24 @@ module Mustermann
|
|
|
129
132
|
|
|
130
133
|
alias == eql?
|
|
131
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
|
|
132
154
|
end
|
|
133
155
|
end
|
data/lib/mustermann/pattern.rb
CHANGED
|
@@ -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]
|
data/lib/mustermann/rails.rb
CHANGED
|
@@ -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
|
|
20
|
-
@peek_regexp
|
|
21
|
-
@regexp
|
|
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) =
|
|
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) =
|
|
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
|
|
49
|
-
|
|
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
|
|
data/lib/mustermann/set.rb
CHANGED
|
@@ -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
|
|
34
|
+
def new(input, escape: false, **opts)
|
|
25
35
|
input = Mustermann::Sinatra.escape(input) if escape
|
|
26
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
|
data/lib/mustermann/sinatra.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
63
|
-
return super
|
|
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
|
|
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" => "
|
|
75
|
+
# Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "foo" }
|
|
76
76
|
#
|
|
77
|
-
# @return [String] string
|
|
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 =
|
|
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
|
|
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
|
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.rc1
|
|
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
|