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.
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