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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +4 -3
  3. data/README.md +680 -376
  4. data/lib/mustermann/ast/compiler.rb +13 -7
  5. data/lib/mustermann/ast/expander.rb +11 -5
  6. data/lib/mustermann/ast/node.rb +27 -1
  7. data/lib/mustermann/ast/param_scanner.rb +20 -0
  8. data/lib/mustermann/ast/parser.rb +131 -12
  9. data/lib/mustermann/ast/pattern.rb +45 -6
  10. data/lib/mustermann/ast/template_generator.rb +28 -0
  11. data/lib/mustermann/ast/validation.rb +5 -3
  12. data/lib/mustermann/composite.rb +103 -0
  13. data/lib/mustermann/expander.rb +1 -1
  14. data/lib/mustermann/express.rb +34 -0
  15. data/lib/mustermann/flask.rb +204 -0
  16. data/lib/mustermann/identity.rb +54 -0
  17. data/lib/mustermann/pattern.rb +186 -12
  18. data/lib/mustermann/pattern_cache.rb +49 -0
  19. data/lib/mustermann/pyramid.rb +25 -0
  20. data/lib/mustermann/regexp_based.rb +18 -1
  21. data/lib/mustermann/regular.rb +1 -1
  22. data/lib/mustermann/shell.rb +8 -0
  23. data/lib/mustermann/simple.rb +1 -1
  24. data/lib/mustermann/simple_match.rb +5 -0
  25. data/lib/mustermann/sinatra.rb +19 -5
  26. data/lib/mustermann/string_scanner.rb +314 -0
  27. data/lib/mustermann/template.rb +10 -0
  28. data/lib/mustermann/to_pattern.rb +11 -6
  29. data/lib/mustermann/version.rb +1 -1
  30. data/lib/mustermann.rb +52 -3
  31. data/mustermann.gemspec +1 -1
  32. data/spec/composite_spec.rb +147 -0
  33. data/spec/expander_spec.rb +15 -0
  34. data/spec/express_spec.rb +209 -0
  35. data/spec/flask_spec.rb +361 -0
  36. data/spec/flask_subclass_spec.rb +368 -0
  37. data/spec/identity_spec.rb +44 -0
  38. data/spec/mustermann_spec.rb +14 -0
  39. data/spec/pattern_spec.rb +7 -3
  40. data/spec/pyramid_spec.rb +101 -0
  41. data/spec/rails_spec.rb +76 -2
  42. data/spec/regular_spec.rb +25 -0
  43. data/spec/shell_spec.rb +33 -0
  44. data/spec/simple_spec.rb +25 -0
  45. data/spec/sinatra_spec.rb +184 -9
  46. data/spec/string_scanner_spec.rb +271 -0
  47. data/spec/support/expand_matcher.rb +7 -5
  48. data/spec/support/generate_template_matcher.rb +27 -0
  49. data/spec/support/pattern.rb +3 -0
  50. data/spec/support/scan_matcher.rb +63 -0
  51. data/spec/support.rb +2 -1
  52. data/spec/template_spec.rb +22 -0
  53. data/spec/to_pattern_spec.rb +49 -0
  54. metadata +47 -61
  55. 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
- list << encoded('+') if space_matches_plus and char == " "
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 &&= "(?!#{translate(except, options.merge(no_captures: true))}\\Z)"
161
- expression = "\\A#{except}#{translate(ast, options)}\\Z"
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.uniq.sort] ||= [keys, pattern, filter]
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
- keys, pattern, filters = mappings.fetch(values.keys.sort) { error_for(values) }
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 had every string to it, even if it's unnecessary
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
 
@@ -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 Group < Node
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.scan(regexp)
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
- scan(regexp)|| unexpected(options)
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 = getch, options = {})
128
- options, char = char, getch if char.is_a?(Hash)
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, :variable, :named_splat) { t.check_name(name) }
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 name == "splat" or name == "captures"
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
@@ -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
- values, behavior = behavior, nil if behavior.is_a?(Hash)
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