mustermann19 0.3.1 → 0.3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|