mustermann19 0.3.1 → 0.3.1.1
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 +7 -0
- data/.travis.yml +4 -3
- data/README.md +680 -376
- data/lib/mustermann/ast/compiler.rb +13 -7
- data/lib/mustermann/ast/expander.rb +11 -5
- data/lib/mustermann/ast/node.rb +27 -1
- data/lib/mustermann/ast/param_scanner.rb +20 -0
- data/lib/mustermann/ast/parser.rb +131 -12
- data/lib/mustermann/ast/pattern.rb +45 -6
- data/lib/mustermann/ast/template_generator.rb +28 -0
- data/lib/mustermann/ast/validation.rb +5 -3
- data/lib/mustermann/composite.rb +103 -0
- data/lib/mustermann/expander.rb +1 -1
- data/lib/mustermann/express.rb +34 -0
- data/lib/mustermann/flask.rb +204 -0
- data/lib/mustermann/identity.rb +54 -0
- data/lib/mustermann/pattern.rb +186 -12
- data/lib/mustermann/pattern_cache.rb +49 -0
- data/lib/mustermann/pyramid.rb +25 -0
- data/lib/mustermann/regexp_based.rb +18 -1
- data/lib/mustermann/regular.rb +1 -1
- data/lib/mustermann/shell.rb +8 -0
- data/lib/mustermann/simple.rb +1 -1
- data/lib/mustermann/simple_match.rb +5 -0
- data/lib/mustermann/sinatra.rb +19 -5
- data/lib/mustermann/string_scanner.rb +314 -0
- data/lib/mustermann/template.rb +10 -0
- data/lib/mustermann/to_pattern.rb +11 -6
- data/lib/mustermann/version.rb +1 -1
- data/lib/mustermann.rb +52 -3
- data/mustermann.gemspec +1 -1
- data/spec/composite_spec.rb +147 -0
- data/spec/expander_spec.rb +15 -0
- data/spec/express_spec.rb +209 -0
- data/spec/flask_spec.rb +361 -0
- data/spec/flask_subclass_spec.rb +368 -0
- data/spec/identity_spec.rb +44 -0
- data/spec/mustermann_spec.rb +14 -0
- data/spec/pattern_spec.rb +7 -3
- data/spec/pyramid_spec.rb +101 -0
- data/spec/rails_spec.rb +76 -2
- data/spec/regular_spec.rb +25 -0
- data/spec/shell_spec.rb +33 -0
- data/spec/simple_spec.rb +25 -0
- data/spec/sinatra_spec.rb +184 -9
- data/spec/string_scanner_spec.rb +271 -0
- data/spec/support/expand_matcher.rb +7 -5
- data/spec/support/generate_template_matcher.rb +27 -0
- data/spec/support/pattern.rb +3 -0
- data/spec/support/scan_matcher.rb +63 -0
- data/spec/support.rb +2 -1
- data/spec/template_spec.rb +22 -0
- data/spec/to_pattern_spec.rb +49 -0
- metadata +47 -61
- data/internals.md +0 -64
@@ -15,6 +15,10 @@ module Mustermann
|
|
15
15
|
translate(:optional) { |o = {}| "(?:%s)?" % t(payload, o) }
|
16
16
|
translate(:char) { |o = {}| t.encoded(payload, o) }
|
17
17
|
|
18
|
+
translate :union do |options = {}|
|
19
|
+
"(?:%s)" % payload.map { |e| "(?:%s)" % t(e, options) }.join(?|)
|
20
|
+
end
|
21
|
+
|
18
22
|
translate :expression do |options = {}|
|
19
23
|
greedy = options.fetch(:greedy, true)
|
20
24
|
t(payload, options.merge(allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved,
|
@@ -57,7 +61,7 @@ module Mustermann
|
|
57
61
|
private
|
58
62
|
def qualified(string, options = {})
|
59
63
|
greedy = options.fetch(:greedy, true)
|
60
|
-
"#{string}+#{?? unless greedy}"
|
64
|
+
"#{string}#{qualifier || "+#{?? unless greedy}"}"
|
61
65
|
end
|
62
66
|
|
63
67
|
def with_lookahead(string, options = {})
|
@@ -69,7 +73,7 @@ module Mustermann
|
|
69
73
|
def from_symbol(symbol, options = {}) qualified(with_lookahead("[[:#{symbol}:]]", options), options) end
|
70
74
|
def from_string(string, options = {}) Regexp.new(string.chars.map { |c| t.encoded(c, options) }.join) end
|
71
75
|
def from_nil(options = {}) qualified(with_lookahead(default(options), options), options) end
|
72
|
-
def default(options = {}) "[^/\\?#]" end
|
76
|
+
def default(options = {}) constraint || "[^/\\?#]" end
|
73
77
|
end
|
74
78
|
|
75
79
|
# @!visibility private
|
@@ -78,7 +82,7 @@ module Mustermann
|
|
78
82
|
# splats are always non-greedy
|
79
83
|
# @!visibility private
|
80
84
|
def pattern(options = {})
|
81
|
-
".*?"
|
85
|
+
constraint || ".*?"
|
82
86
|
end
|
83
87
|
end
|
84
88
|
|
@@ -137,7 +141,10 @@ module Mustermann
|
|
137
141
|
return Regexp.escape(char) unless uri_decode
|
138
142
|
encoded = escape(char, escape: /./)
|
139
143
|
list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
|
140
|
-
|
144
|
+
if char == " "
|
145
|
+
list << encoded('+') if space_matches_plus
|
146
|
+
list << " "
|
147
|
+
end
|
141
148
|
"(?:%s)" % list.join("|")
|
142
149
|
end
|
143
150
|
|
@@ -157,9 +164,8 @@ module Mustermann
|
|
157
164
|
# @!visibility private
|
158
165
|
def compile(ast, options = {})
|
159
166
|
except = options.delete(:except)
|
160
|
-
except
|
161
|
-
|
162
|
-
Regexp.new(expression)
|
167
|
+
except &&= "(?!#{translate(except, options.merge(no_captures: true))}\\Z)"
|
168
|
+
Regexp.new("#{except}#{translate(ast, options)}")
|
163
169
|
end
|
164
170
|
end
|
165
171
|
|
@@ -46,6 +46,10 @@ module Mustermann
|
|
46
46
|
nested
|
47
47
|
end
|
48
48
|
|
49
|
+
translate :union do
|
50
|
+
payload.map { |e| t(e) }.inject(:+)
|
51
|
+
end
|
52
|
+
|
49
53
|
# helper method for captures
|
50
54
|
# @!visibility private
|
51
55
|
def for_capture(node)
|
@@ -70,7 +74,7 @@ module Mustermann
|
|
70
74
|
def add(ast)
|
71
75
|
translate(ast).each do |keys, pattern, filter|
|
72
76
|
self.keys.concat(keys).uniq!
|
73
|
-
mappings[keys.
|
77
|
+
mappings[keys.sort] ||= [keys, pattern, filter]
|
74
78
|
end
|
75
79
|
end
|
76
80
|
|
@@ -82,10 +86,12 @@ module Mustermann
|
|
82
86
|
|
83
87
|
# @see Mustermann::Pattern#expand
|
84
88
|
# @!visibility private
|
85
|
-
def expand(values
|
86
|
-
|
89
|
+
def expand(values)
|
90
|
+
values = values.each_with_object({}){ |(key, value), new_hash|
|
91
|
+
new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value }
|
92
|
+
keys, pattern, filters = mappings.fetch(values.keys.flatten.sort) { error_for(values) }
|
87
93
|
filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) }
|
88
|
-
pattern % values.values_at(*keys)
|
94
|
+
pattern % (values[keys] || values.values_at(*keys))
|
89
95
|
end
|
90
96
|
|
91
97
|
# @see Mustermann::Pattern#expandable?
|
@@ -112,7 +118,7 @@ module Mustermann
|
|
112
118
|
# @see Mustermann::AST::Translator#expand
|
113
119
|
# @!visibility private
|
114
120
|
def escape(string, *args)
|
115
|
-
# URI::Parser is pretty slow, let's not
|
121
|
+
# URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary
|
116
122
|
string =~ /\A\w*\Z/ ? string : super
|
117
123
|
end
|
118
124
|
|
data/lib/mustermann/ast/node.rb
CHANGED
@@ -18,6 +18,12 @@ module Mustermann
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
# @!visibility private
|
22
|
+
def is_a?(type)
|
23
|
+
type = Node[type] if type.is_a? Symbol
|
24
|
+
super(type)
|
25
|
+
end
|
26
|
+
|
21
27
|
# @!visibility private
|
22
28
|
# @param [Symbol] name of the node
|
23
29
|
# @return [String] qualified name of factory for the node
|
@@ -65,6 +71,18 @@ module Mustermann
|
|
65
71
|
|
66
72
|
# @!visibility private
|
67
73
|
class Capture < Node
|
74
|
+
# @see Mustermann::AST::Compiler::Capture#default
|
75
|
+
# @!visibility private
|
76
|
+
attr_accessor :constraint
|
77
|
+
|
78
|
+
# @see Mustermann::AST::Compiler::Capture#qualified
|
79
|
+
# @!visibility private
|
80
|
+
attr_accessor :qualifier
|
81
|
+
|
82
|
+
# @see Mustermann::AST::Pattern#map_param
|
83
|
+
# @!visibility private
|
84
|
+
attr_accessor :convert
|
85
|
+
|
68
86
|
# @see Mustermann::AST::Node#parse
|
69
87
|
# @!visibility private
|
70
88
|
def parse
|
@@ -88,7 +106,7 @@ module Mustermann
|
|
88
106
|
end
|
89
107
|
|
90
108
|
# @!visibility private
|
91
|
-
class
|
109
|
+
class Composition < Node
|
92
110
|
# @!visibility private
|
93
111
|
def initialize(payload = nil, options = {})
|
94
112
|
options, payload = payload, nil if payload.is_a?(Hash)
|
@@ -96,6 +114,14 @@ module Mustermann
|
|
96
114
|
end
|
97
115
|
end
|
98
116
|
|
117
|
+
# @!visibility private
|
118
|
+
class Group < Composition
|
119
|
+
end
|
120
|
+
|
121
|
+
# @!visibility private
|
122
|
+
class Union < Composition
|
123
|
+
end
|
124
|
+
|
99
125
|
# @!visibility private
|
100
126
|
class Optional < Node
|
101
127
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module AST
|
5
|
+
# Scans an AST for param converters.
|
6
|
+
# @!visibility private
|
7
|
+
# @see Mustermann::AST::Pattern#to_templates
|
8
|
+
class ParamScanner < Translator
|
9
|
+
# @!visibility private
|
10
|
+
def self.scan_params(ast)
|
11
|
+
new.translate(ast)
|
12
|
+
end
|
13
|
+
|
14
|
+
translate(:node) { t(payload) }
|
15
|
+
translate(Array) { map { |e| t(e) }.inject(:merge) }
|
16
|
+
translate(Object) { {} }
|
17
|
+
translate(:capture) { convert ? { name => convert } : {} }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -11,8 +11,8 @@ module Mustermann
|
|
11
11
|
# @param [String] string to be parsed
|
12
12
|
# @return [Mustermann::AST::Node] parse tree for string
|
13
13
|
# @!visibility private
|
14
|
-
def self.parse(string)
|
15
|
-
new.parse(string)
|
14
|
+
def self.parse(string, options = {})
|
15
|
+
new(options).parse(string)
|
16
16
|
end
|
17
17
|
|
18
18
|
# Defines another grammar rule for first character.
|
@@ -31,24 +31,31 @@ module Mustermann
|
|
31
31
|
#
|
32
32
|
# @see Mustermann::Sinatra
|
33
33
|
# @!visibility private
|
34
|
-
def self.suffix(pattern = /./, &block)
|
34
|
+
def self.suffix(pattern = /./, options = {}, &block)
|
35
|
+
after = options[:after] || :node
|
35
36
|
@suffix ||= []
|
36
|
-
@suffix << [pattern, block] if block
|
37
|
+
@suffix << [pattern, after, block] if block
|
37
38
|
@suffix
|
38
39
|
end
|
39
40
|
|
40
41
|
# @!visibility private
|
41
|
-
attr_reader :buffer, :string
|
42
|
+
attr_reader :buffer, :string, :pattern
|
42
43
|
|
43
44
|
extend Forwardable
|
44
45
|
def_delegators :buffer, :eos?, :getch
|
45
46
|
|
47
|
+
# @!visibility private
|
48
|
+
def initialize(options = {})
|
49
|
+
pattern = options.delete(:pattern)
|
50
|
+
@pattern = pattern
|
51
|
+
end
|
52
|
+
|
46
53
|
# @param [String] string to be parsed
|
47
54
|
# @return [Mustermann::AST::Node] parse tree for string
|
48
55
|
# @!visibility private
|
49
56
|
def parse(string)
|
50
57
|
@string = string
|
51
|
-
@buffer = StringScanner.new(string)
|
58
|
+
@buffer = ::StringScanner.new(string)
|
52
59
|
node(:root, string) { read unless eos? }
|
53
60
|
end
|
54
61
|
|
@@ -87,8 +94,8 @@ module Mustermann
|
|
87
94
|
# @return [Mustermann::AST::Node] node with suffix
|
88
95
|
# @!visibility private
|
89
96
|
def read_suffix(element)
|
90
|
-
self.class.suffix.inject(element) do |ele, (regexp, callback)|
|
91
|
-
next ele unless payload = scan(regexp)
|
97
|
+
self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
|
98
|
+
next ele unless ele.is_a?(after) and payload = scan(regexp)
|
92
99
|
instance_exec(payload, ele, &callback)
|
93
100
|
end
|
94
101
|
end
|
@@ -102,8 +109,25 @@ module Mustermann
|
|
102
109
|
# @return [String, MatchData, nil]
|
103
110
|
# @!visibility private
|
104
111
|
def scan(regexp)
|
112
|
+
match_buffer(:scan, regexp)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Wrapper around {StringScanner#check} that turns strings into escaped
|
116
|
+
# regular expressions and returns a MatchData if the regexp has any
|
117
|
+
# named captures.
|
118
|
+
#
|
119
|
+
# @param [Regexp, String] regexp
|
120
|
+
# @see StringScanner#check
|
121
|
+
# @return [String, MatchData, nil]
|
122
|
+
# @!visibility private
|
123
|
+
def check(regexp)
|
124
|
+
match_buffer(:check, regexp)
|
125
|
+
end
|
126
|
+
|
127
|
+
# @!visibility private
|
128
|
+
def match_buffer(method, regexp)
|
105
129
|
regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
|
106
|
-
string = buffer.
|
130
|
+
string = buffer.public_send(method, regexp)
|
107
131
|
regexp.names.any? ? regexp.match(string) : string
|
108
132
|
end
|
109
133
|
|
@@ -115,7 +139,99 @@ module Mustermann
|
|
115
139
|
# @raise [Mustermann::ParseError] if expectation wasn't met
|
116
140
|
# @!visibility private
|
117
141
|
def expect(regexp, options = {})
|
118
|
-
|
142
|
+
char = options.delete(:char) || nil
|
143
|
+
scan(regexp) || unexpected(char, options)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Allows to read a string inside brackets. It does not expect the string
|
147
|
+
# to start with an opening bracket.
|
148
|
+
#
|
149
|
+
# @example
|
150
|
+
# buffer.string = "fo<o>>ba<r>"
|
151
|
+
# read_brackets(?<, ?>) # => "fo<o>"
|
152
|
+
# buffer.rest # => "ba<r>"
|
153
|
+
#
|
154
|
+
# @!visibility private
|
155
|
+
def read_brackets(open, close, options = {})
|
156
|
+
char = options.delete(:char) || nil
|
157
|
+
escape = options.delete(:escape) || '\\'
|
158
|
+
quote = options.delete(:quote) || false
|
159
|
+
result = ""
|
160
|
+
escape = false if escape.nil?
|
161
|
+
while current = getch
|
162
|
+
case current
|
163
|
+
when close then return result
|
164
|
+
when open then result << open << read_brackets(open, close) << close
|
165
|
+
when escape then result << escape << getch
|
166
|
+
else result << current
|
167
|
+
end
|
168
|
+
end
|
169
|
+
unexpected(char, options)
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# Reads an argument string of the format arg1,args2,key:value
|
174
|
+
#
|
175
|
+
# @!visibility private
|
176
|
+
def read_args(key_separator, close, options = {})
|
177
|
+
separator = options.delete(:separator) || ?,
|
178
|
+
symbol_keys = options.fetch(:symbol_keys, true)
|
179
|
+
options.delete(:symbol_keys)
|
180
|
+
list, map = [], {}
|
181
|
+
while buffer.peek(1) != close
|
182
|
+
scan(separator)
|
183
|
+
entries = read_list(close, separator, options.merge(separator: key_separator))
|
184
|
+
case entries.size
|
185
|
+
when 1 then list += entries
|
186
|
+
when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
|
187
|
+
else unexpected(key_separator)
|
188
|
+
end
|
189
|
+
buffer.pos -= 1
|
190
|
+
end
|
191
|
+
expect(close)
|
192
|
+
[list, map]
|
193
|
+
end
|
194
|
+
|
195
|
+
# Reads a separated list with the ability to quote, escape and add spaces.
|
196
|
+
#
|
197
|
+
# @!visibility private
|
198
|
+
#def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", options)
|
199
|
+
def read_list(*close)
|
200
|
+
options = close.last.kind_of?(Hash) ? close.pop : {}
|
201
|
+
separator = options.delete(:separator) || ?,
|
202
|
+
escape = options.delete(:escape) || '\\'
|
203
|
+
quotes = options.delete(:quotes) || [?", ?']
|
204
|
+
ignore = options.delete(:ignore) || " "
|
205
|
+
result = []
|
206
|
+
while current = getch
|
207
|
+
element = result.empty? ? result : result.last
|
208
|
+
case current
|
209
|
+
when *close then return result
|
210
|
+
when ignore then nil # do nothing
|
211
|
+
when separator then result << ""
|
212
|
+
when escape then element << getch
|
213
|
+
when *quotes then element << read_escaped(current, escape: escape)
|
214
|
+
else element << current
|
215
|
+
end
|
216
|
+
end
|
217
|
+
unexpected(current, options)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Read a string until a terminating character, ignoring escaped versions of said character.
|
221
|
+
#
|
222
|
+
# @!visibility private
|
223
|
+
#def read_escaped(close, escape: ?\\, **options)
|
224
|
+
def read_escaped(close, options = {})
|
225
|
+
escape = options.delete(:escape) || '\\'
|
226
|
+
result = ""
|
227
|
+
while current = getch
|
228
|
+
case current
|
229
|
+
when close then return result
|
230
|
+
when escape then result << getch
|
231
|
+
else result << current
|
232
|
+
end
|
233
|
+
end
|
234
|
+
unexpected(current, options)
|
119
235
|
end
|
120
236
|
|
121
237
|
# Helper for raising an exception for an unexpected character.
|
@@ -124,12 +240,15 @@ module Mustermann
|
|
124
240
|
# @param [String, nil] char the unexpected character
|
125
241
|
# @raise [Mustermann::ParseError, Exception]
|
126
242
|
# @!visibility private
|
127
|
-
def unexpected(char =
|
128
|
-
options, char = char,
|
243
|
+
def unexpected(char = nil, options = {})
|
244
|
+
options, char = char, nil if char.is_a?(Hash)
|
245
|
+
char ||= getch
|
129
246
|
exception = options.fetch(:exception, ParseError)
|
130
247
|
char = "space" if char == " "
|
131
248
|
raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
|
132
249
|
end
|
250
|
+
|
251
|
+
private :match_buffer
|
133
252
|
end
|
134
253
|
|
135
254
|
#private_constant :Parser
|
@@ -2,6 +2,8 @@ require 'mustermann/ast/parser'
|
|
2
2
|
require 'mustermann/ast/compiler'
|
3
3
|
require 'mustermann/ast/transformer'
|
4
4
|
require 'mustermann/ast/validation'
|
5
|
+
require 'mustermann/ast/template_generator'
|
6
|
+
require 'mustermann/ast/param_scanner'
|
5
7
|
require 'mustermann/regexp_based'
|
6
8
|
require 'mustermann/expander'
|
7
9
|
require 'mustermann/equality_map'
|
@@ -16,8 +18,9 @@ module Mustermann
|
|
16
18
|
|
17
19
|
extend Forwardable, SingleForwardable
|
18
20
|
single_delegate on: :parser, suffix: :parser
|
19
|
-
instance_delegate %w[parser compiler transformer validation].map(&:to_sym) => 'self.class'
|
20
|
-
instance_delegate parse: :parser, transform: :transformer, validate: :validation
|
21
|
+
instance_delegate %w[parser compiler transformer validation template_generator param_scanner].map(&:to_sym) => 'self.class'
|
22
|
+
instance_delegate parse: :parser, transform: :transformer, validate: :validation,
|
23
|
+
generate_templates: :template_generator, scan_params: :param_scanner
|
21
24
|
|
22
25
|
# @api private
|
23
26
|
# @return [#parse] parser object for pattern
|
@@ -49,6 +52,20 @@ module Mustermann
|
|
49
52
|
Validation
|
50
53
|
end
|
51
54
|
|
55
|
+
# @api private
|
56
|
+
# @return [#generate_templates] generates URI templates for pattern
|
57
|
+
# @!visibility private
|
58
|
+
def self.template_generator
|
59
|
+
TemplateGenerator
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api private
|
63
|
+
# @return [#scan_params] param scanner for pattern
|
64
|
+
# @!visibility private
|
65
|
+
def self.param_scanner
|
66
|
+
ParamScanner
|
67
|
+
end
|
68
|
+
|
52
69
|
# @!visibility private
|
53
70
|
def compile(options = {})
|
54
71
|
options[:except] &&= parse options[:except]
|
@@ -62,7 +79,7 @@ module Mustermann
|
|
62
79
|
# @!visibility private
|
63
80
|
def to_ast
|
64
81
|
@ast_cache ||= EqualityMap.new
|
65
|
-
@ast_cache.fetch(@string) { validate(transform(parse(@string))) }
|
82
|
+
@ast_cache.fetch(@string) { validate(transform(parse(@string, pattern: self))) }
|
66
83
|
end
|
67
84
|
|
68
85
|
# All AST-based pattern implementations support expanding.
|
@@ -73,12 +90,34 @@ module Mustermann
|
|
73
90
|
# @raise (see Mustermann::Pattern#expand)
|
74
91
|
# @see Mustermann::Pattern#expand
|
75
92
|
# @see Mustermann::Expander
|
76
|
-
def expand(values = {})
|
93
|
+
def expand(behavior = nil, values = {})
|
77
94
|
@expander ||= Mustermann::Expander.new(self)
|
78
|
-
@expander.expand(values)
|
95
|
+
@expander.expand(behavior, values)
|
96
|
+
end
|
97
|
+
|
98
|
+
# All AST-based pattern implementations support generating templates.
|
99
|
+
#
|
100
|
+
# @example (see Mustermann::Pattern#to_templates)
|
101
|
+
# @param (see Mustermann::Pattern#to_templates)
|
102
|
+
# @return (see Mustermann::Pattern#to_templates)
|
103
|
+
# @see Mustermann::Pattern#to_templates
|
104
|
+
def to_templates
|
105
|
+
@to_templates ||= generate_templates(to_ast)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @!visibility private
|
109
|
+
# @see Mustermann::Pattern#map_param
|
110
|
+
def map_param(key, value)
|
111
|
+
return super unless param_converters.include? key
|
112
|
+
param_converters[key][super]
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!visibility private
|
116
|
+
def param_converters
|
117
|
+
@param_converters ||= scan_params(to_ast)
|
79
118
|
end
|
80
119
|
|
81
|
-
private :compile
|
120
|
+
private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params
|
82
121
|
end
|
83
122
|
end
|
84
123
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'mustermann/ast/translator'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
module AST
|
5
|
+
# Turns an AST into an Array of URI templates representing the AST.
|
6
|
+
# @!visibility private
|
7
|
+
# @see Mustermann::AST::Pattern#to_templates
|
8
|
+
class TemplateGenerator < Translator
|
9
|
+
# @!visibility private
|
10
|
+
def self.generate_templates(ast)
|
11
|
+
new.translate(ast).uniq
|
12
|
+
end
|
13
|
+
|
14
|
+
# translate(:expression) is not needed, since template patterns simply call to_s
|
15
|
+
translate(:root, :group) { t(payload) || [""] }
|
16
|
+
translate(:separator, :char) { t.escape(payload) }
|
17
|
+
translate(:capture) { "{#{name}}" }
|
18
|
+
translate(:optional) { [t(payload), ""] }
|
19
|
+
translate(:named_splat, :splat) { "{+#{name}}" }
|
20
|
+
translate(:with_look_ahead) { t([head, payload]) }
|
21
|
+
translate(:union) { payload.flat_map { |e| t(e) } }
|
22
|
+
|
23
|
+
translate(Array) do
|
24
|
+
map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -21,14 +21,16 @@ module Mustermann
|
|
21
21
|
translate(Object, :splat) {}
|
22
22
|
translate(:node) { t(payload) }
|
23
23
|
translate(Array) { each { |p| t(p)} }
|
24
|
-
translate(:capture
|
24
|
+
translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])}
|
25
|
+
translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')}
|
25
26
|
|
26
27
|
# @raise [Mustermann::CompileError] if name is not acceptable
|
27
28
|
# @!visibility private
|
28
|
-
def check_name(name)
|
29
|
+
def check_name(name, options = {})
|
30
|
+
forbidden = options[:forbidden]
|
29
31
|
raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
|
30
32
|
raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/
|
31
|
-
raise CompileError, "capture name can't be #{name}" if
|
33
|
+
raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
|
32
34
|
raise CompileError, "can't use the same capture name twice" if names.include? name
|
33
35
|
names << name
|
34
36
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Mustermann
|
2
|
+
# Class for pattern objects composed of multiple patterns using binary logic.
|
3
|
+
# @see Mustermann::Pattern#&
|
4
|
+
# @see Mustermann::Pattern#|
|
5
|
+
# @see Mustermann::Pattern#^
|
6
|
+
class Composite < Pattern
|
7
|
+
attr_reader :patterns, :operator
|
8
|
+
supported_options :operator, :type
|
9
|
+
|
10
|
+
# @see Mustermann::Pattern.supported?
|
11
|
+
def self.supported?(option, options = {})
|
12
|
+
return true if super
|
13
|
+
options[:type] and Mustermann[options[:type]].supported?(option, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Mustermann::Pattern] a new composite pattern
|
17
|
+
def self.new(*patterns)
|
18
|
+
options = patterns.last.kind_of?(Hash) ? patterns.pop : {}
|
19
|
+
patterns = patterns.flatten
|
20
|
+
case patterns.size
|
21
|
+
when 0 then raise ArgumentError, 'cannot create empty composite pattern'
|
22
|
+
when 1 then patterns.first
|
23
|
+
else super(patterns, options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(patterns, options = {})
|
28
|
+
operator = options.delete(:operator) || :|
|
29
|
+
@operator = operator.to_sym
|
30
|
+
@patterns = patterns.flat_map { |p| patterns_from(p, options) }
|
31
|
+
end
|
32
|
+
|
33
|
+
# @see Mustermann::Pattern#==
|
34
|
+
def ==(pattern)
|
35
|
+
patterns == patterns_from(pattern)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @see Mustermann::Pattern#===
|
39
|
+
def ===(string)
|
40
|
+
patterns.map { |p| p === string }.inject(operator)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @see Mustermann::Pattern#params
|
44
|
+
def params(string)
|
45
|
+
with_matching(string, :params)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @see Mustermann::Pattern#match
|
49
|
+
def match(string)
|
50
|
+
with_matching(string, :match)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @!visibility private
|
54
|
+
def respond_to_special?(method)
|
55
|
+
return false unless operator == :|
|
56
|
+
patterns.all? { |p| p.respond_to?(method) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# (see Mustermann::Pattern#expand)
|
60
|
+
def expand(behavior = nil, values = {})
|
61
|
+
raise NotImplementedError, 'expanding not supported' unless respond_to? :expand
|
62
|
+
@expander ||= Mustermann::Expander.new(*patterns)
|
63
|
+
@expander.expand(behavior, values)
|
64
|
+
end
|
65
|
+
|
66
|
+
# (see Mustermann::Pattern#expand)
|
67
|
+
def to_templates
|
68
|
+
raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates
|
69
|
+
patterns.flat_map(&:to_templates).uniq
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [String] the string representation of the pattern
|
73
|
+
def to_s
|
74
|
+
simple_inspect
|
75
|
+
end
|
76
|
+
|
77
|
+
# @!visibility private
|
78
|
+
def inspect
|
79
|
+
"#<%p:%s>" % [self.class, simple_inspect]
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!visibility private
|
83
|
+
def simple_inspect
|
84
|
+
pattern_strings = patterns.map { |p| p.simple_inspect }
|
85
|
+
"(#{pattern_strings.join(" #{operator} ")})"
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!visibility private
|
89
|
+
def with_matching(string, method)
|
90
|
+
return unless self === string
|
91
|
+
pattern = patterns.detect { |p| p === string }
|
92
|
+
pattern.public_send(method, string) if pattern
|
93
|
+
end
|
94
|
+
|
95
|
+
# @!visibility private
|
96
|
+
def patterns_from(pattern, options = nil)
|
97
|
+
return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator
|
98
|
+
[options ? Mustermann.new(pattern, options) : pattern]
|
99
|
+
end
|
100
|
+
|
101
|
+
private :with_matching, :patterns_from
|
102
|
+
end
|
103
|
+
end
|
data/lib/mustermann/expander.rb
CHANGED
@@ -140,7 +140,7 @@ module Mustermann
|
|
140
140
|
# @raise [NotImplementedError] raised if expand is not supported.
|
141
141
|
# @raise [Mustermann::ExpandError] raised if a value is missing or unknown
|
142
142
|
def expand(behavior = nil, values = {})
|
143
|
-
|
143
|
+
behavior, values = nil, behavior if behavior.is_a? Hash
|
144
144
|
values = map_values(values)
|
145
145
|
|
146
146
|
case behavior || additional_values
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'mustermann/ast/pattern'
|
2
|
+
|
3
|
+
module Mustermann
|
4
|
+
# Express style pattern implementation.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# Mustermann.new('/:foo', type: :express) === '/bar' # => true
|
8
|
+
#
|
9
|
+
# @see Mustermann::Pattern
|
10
|
+
# @see file:README.md#flask Syntax description in the README
|
11
|
+
class Express < AST::Pattern
|
12
|
+
on(nil, ??, ?+, ?*, ?)) { |c| unexpected(c) }
|
13
|
+
on(?:) { |c| node(:capture) { scan(/\w+/) } }
|
14
|
+
on(?() { |c| node(:splat, constraint: read_brackets(?(, ?))) }
|
15
|
+
|
16
|
+
suffix ??, after: :capture do |char, element|
|
17
|
+
unexpected(char) unless element.is_a? :capture
|
18
|
+
node(:optional, element)
|
19
|
+
end
|
20
|
+
|
21
|
+
suffix ?*, after: :capture do |match, element|
|
22
|
+
node(:named_splat, element.name)
|
23
|
+
end
|
24
|
+
|
25
|
+
suffix ?+, after: :capture do |match, element|
|
26
|
+
node(:named_splat, element.name, constraint: ".+")
|
27
|
+
end
|
28
|
+
|
29
|
+
suffix ?(, after: :capture do |match, element|
|
30
|
+
element.constraint = read_brackets(?(, ?))
|
31
|
+
element
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|