mustermann 0.3.1 → 0.4.0

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +429 -672
  3. data/lib/mustermann.rb +95 -20
  4. data/lib/mustermann/ast/boundaries.rb +44 -0
  5. data/lib/mustermann/ast/compiler.rb +13 -7
  6. data/lib/mustermann/ast/expander.rb +22 -12
  7. data/lib/mustermann/ast/node.rb +69 -5
  8. data/lib/mustermann/ast/param_scanner.rb +20 -0
  9. data/lib/mustermann/ast/parser.rb +138 -19
  10. data/lib/mustermann/ast/pattern.rb +59 -7
  11. data/lib/mustermann/ast/template_generator.rb +28 -0
  12. data/lib/mustermann/ast/transformer.rb +2 -2
  13. data/lib/mustermann/ast/translator.rb +20 -0
  14. data/lib/mustermann/ast/validation.rb +4 -3
  15. data/lib/mustermann/composite.rb +101 -0
  16. data/lib/mustermann/expander.rb +2 -2
  17. data/lib/mustermann/identity.rb +56 -0
  18. data/lib/mustermann/pattern.rb +185 -10
  19. data/lib/mustermann/pattern_cache.rb +49 -0
  20. data/lib/mustermann/regexp.rb +1 -0
  21. data/lib/mustermann/regexp_based.rb +18 -1
  22. data/lib/mustermann/regular.rb +4 -1
  23. data/lib/mustermann/simple_match.rb +5 -0
  24. data/lib/mustermann/sinatra.rb +22 -5
  25. data/lib/mustermann/to_pattern.rb +11 -6
  26. data/lib/mustermann/version.rb +1 -1
  27. data/mustermann.gemspec +1 -14
  28. data/spec/ast_spec.rb +14 -0
  29. data/spec/composite_spec.rb +147 -0
  30. data/spec/expander_spec.rb +15 -0
  31. data/spec/identity_spec.rb +44 -0
  32. data/spec/mustermann_spec.rb +17 -2
  33. data/spec/pattern_spec.rb +7 -3
  34. data/spec/regular_spec.rb +25 -0
  35. data/spec/sinatra_spec.rb +184 -9
  36. data/spec/to_pattern_spec.rb +49 -0
  37. metadata +15 -180
  38. data/.gitignore +0 -18
  39. data/.rspec +0 -2
  40. data/.travis.yml +0 -4
  41. data/.yardopts +0 -1
  42. data/Gemfile +0 -2
  43. data/LICENSE +0 -22
  44. data/Rakefile +0 -6
  45. data/internals.md +0 -64
  46. data/lib/mustermann/ast/tree_renderer.rb +0 -29
  47. data/lib/mustermann/rails.rb +0 -17
  48. data/lib/mustermann/shell.rb +0 -29
  49. data/lib/mustermann/simple.rb +0 -35
  50. data/lib/mustermann/template.rb +0 -47
  51. data/spec/rails_spec.rb +0 -521
  52. data/spec/shell_spec.rb +0 -108
  53. data/spec/simple_spec.rb +0 -236
  54. data/spec/support.rb +0 -5
  55. data/spec/support/coverage.rb +0 -16
  56. data/spec/support/env.rb +0 -16
  57. data/spec/support/expand_matcher.rb +0 -27
  58. data/spec/support/match_matcher.rb +0 -39
  59. data/spec/support/pattern.rb +0 -39
  60. data/spec/template_spec.rb +0 -814
@@ -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,29 @@ module Mustermann
31
31
  #
32
32
  # @see Mustermann::Sinatra
33
33
  # @!visibility private
34
- def self.suffix(pattern = /./, &block)
34
+ def self.suffix(pattern = /./, after: :node, &block)
35
35
  @suffix ||= []
36
- @suffix << [pattern, block] if block
36
+ @suffix << [pattern, after, block] if block
37
37
  @suffix
38
38
  end
39
39
 
40
40
  # @!visibility private
41
- attr_reader :buffer, :string
41
+ attr_reader :buffer, :string, :pattern
42
42
 
43
43
  extend Forwardable
44
- def_delegators :buffer, :eos?, :getch
44
+ def_delegators :buffer, :eos?, :getch, :pos
45
+
46
+ # @!visibility private
47
+ def initialize(pattern: nil, **options)
48
+ @pattern = pattern
49
+ end
45
50
 
46
51
  # @param [String] string to be parsed
47
52
  # @return [Mustermann::AST::Node] parse tree for string
48
53
  # @!visibility private
49
54
  def parse(string)
50
55
  @string = string
51
- @buffer = StringScanner.new(string)
56
+ @buffer = ::StringScanner.new(string)
52
57
  node(:root, string) { read unless eos? }
53
58
  end
54
59
 
@@ -59,8 +64,10 @@ module Mustermann
59
64
  # @return [Mustermann::AST::Node]
60
65
  # @!visibility private
61
66
  def node(type, *args, &block)
62
- type = Node[type] unless type.respond_to? :new
63
- block ? type.parse(*args, &block) : type.new(*args)
67
+ type = Node[type] unless type.respond_to? :new
68
+ start = pos
69
+ node = block ? type.parse(*args, &block) : type.new(*args)
70
+ min_size(start, pos, node)
64
71
  end
65
72
 
66
73
  # Create a node for a character we don't have an explicit rule for.
@@ -76,20 +83,35 @@ module Mustermann
76
83
  # @return [Mustermann::AST::Node] next element
77
84
  # @!visibility private
78
85
  def read
79
- char = getch
80
- method = "read %p" % char
81
- element = respond_to?(method) ? send(method, char) : default_node(char)
86
+ start = pos
87
+ char = getch
88
+ method = "read %p" % char
89
+ element= respond_to?(method) ? send(method, char) : default_node(char)
90
+ min_size(start, pos, element)
82
91
  read_suffix(element)
83
92
  end
84
93
 
94
+ # sets start on node to start if it's not set to a lower value.
95
+ # sets stop on node to stop if it's not set to a higher value.
96
+ # @return [Mustermann::AST::Node] the node passed as third argument
97
+ # @!visibility private
98
+ def min_size(start, stop, node)
99
+ stop ||= start
100
+ start ||= stop
101
+ node.start = start unless node.start and node.start < start
102
+ node.stop = stop unless node.stop and node.stop > stop
103
+ node
104
+ end
105
+
85
106
  # Checks for a potential suffix on the buffer.
86
107
  # @param [Mustermann::AST::Node] element node without suffix
87
108
  # @return [Mustermann::AST::Node] node with suffix
88
109
  # @!visibility private
89
110
  def read_suffix(element)
90
- self.class.suffix.inject(element) do |ele, (regexp, callback)|
91
- next ele unless payload = scan(regexp)
92
- instance_exec(payload, ele, &callback)
111
+ self.class.suffix.inject(element) do |ele, (regexp, after, callback)|
112
+ next ele unless ele.is_a?(after) and payload = scan(regexp)
113
+ content = instance_exec(payload, ele, &callback)
114
+ min_size(element.start, pos, content)
93
115
  end
94
116
  end
95
117
 
@@ -102,8 +124,25 @@ module Mustermann
102
124
  # @return [String, MatchData, nil]
103
125
  # @!visibility private
104
126
  def scan(regexp)
127
+ match_buffer(:scan, regexp)
128
+ end
129
+
130
+ # Wrapper around {StringScanner#check} that turns strings into escaped
131
+ # regular expressions and returns a MatchData if the regexp has any
132
+ # named captures.
133
+ #
134
+ # @param [Regexp, String] regexp
135
+ # @see StringScanner#check
136
+ # @return [String, MatchData, nil]
137
+ # @!visibility private
138
+ def check(regexp)
139
+ match_buffer(:check, regexp)
140
+ end
141
+
142
+ # @!visibility private
143
+ def match_buffer(method, regexp)
105
144
  regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp
106
- string = buffer.scan(regexp)
145
+ string = buffer.public_send(method, regexp)
107
146
  regexp.names.any? ? regexp.match(string) : string
108
147
  end
109
148
 
@@ -114,8 +153,85 @@ module Mustermann
114
153
  # @return [String, MatchData] the match
115
154
  # @raise [Mustermann::ParseError] if expectation wasn't met
116
155
  # @!visibility private
117
- def expect(regexp, **options)
118
- scan(regexp)|| unexpected(**options)
156
+ def expect(regexp, char: nil, **options)
157
+ scan(regexp) || unexpected(char, **options)
158
+ end
159
+
160
+ # Allows to read a string inside brackets. It does not expect the string
161
+ # to start with an opening bracket.
162
+ #
163
+ # @example
164
+ # buffer.string = "fo<o>>ba<r>"
165
+ # read_brackets(?<, ?>) # => "fo<o>"
166
+ # buffer.rest # => "ba<r>"
167
+ #
168
+ # @!visibility private
169
+ def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options)
170
+ result = ""
171
+ escape = false if escape.nil?
172
+ while current = getch
173
+ case current
174
+ when close then return result
175
+ when open then result << open << read_brackets(open, close) << close
176
+ when escape then result << escape << getch
177
+ else result << current
178
+ end
179
+ end
180
+ unexpected(char, **options)
181
+ end
182
+
183
+
184
+ # Reads an argument string of the format arg1,args2,key:value
185
+ #
186
+ # @!visibility private
187
+ def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options)
188
+ list, map = [], {}
189
+ while buffer.peek(1) != close
190
+ scan(separator)
191
+ entries = read_list(close, separator, separator: key_separator, **options)
192
+ case entries.size
193
+ when 1 then list += entries
194
+ when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last
195
+ else unexpected(key_separator)
196
+ end
197
+ buffer.pos -= 1
198
+ end
199
+ expect(close)
200
+ [list, map]
201
+ end
202
+
203
+ # Reads a separated list with the ability to quote, escape and add spaces.
204
+ #
205
+ # @!visibility private
206
+ def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options)
207
+ result = []
208
+ while current = getch
209
+ element = result.empty? ? result : result.last
210
+ case current
211
+ when *close then return result
212
+ when ignore then nil # do nothing
213
+ when separator then result << ""
214
+ when escape then element << getch
215
+ when *quotes then element << read_escaped(current, escape: escape)
216
+ else element << current
217
+ end
218
+ end
219
+ unexpected(current, **options)
220
+ end
221
+
222
+ # Read a string until a terminating character, ignoring escaped versions of said character.
223
+ #
224
+ # @!visibility private
225
+ def read_escaped(close, escape: ?\\, **options)
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,10 +240,13 @@ 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, exception: ParseError)
243
+ def unexpected(char = nil, exception: ParseError)
244
+ char ||= getch
128
245
  char = "space" if char == " "
129
246
  raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}"
130
247
  end
248
+
249
+ private :match_buffer
131
250
  end
132
251
 
133
252
  private_constant :Parser
@@ -1,7 +1,10 @@
1
1
  require 'mustermann/ast/parser'
2
+ require 'mustermann/ast/boundaries'
2
3
  require 'mustermann/ast/compiler'
3
4
  require 'mustermann/ast/transformer'
4
5
  require 'mustermann/ast/validation'
6
+ require 'mustermann/ast/template_generator'
7
+ require 'mustermann/ast/param_scanner'
5
8
  require 'mustermann/regexp_based'
6
9
  require 'mustermann/expander'
7
10
  require 'tool/equality_map'
@@ -16,8 +19,9 @@ module Mustermann
16
19
 
17
20
  extend Forwardable, SingleForwardable
18
21
  single_delegate on: :parser, suffix: :parser
19
- instance_delegate %i[parser compiler transformer validation] => 'self.class'
20
- instance_delegate parse: :parser, transform: :transformer, validate: :validation
22
+ instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class'
23
+ instance_delegate parse: :parser, transform: :transformer, validate: :validation,
24
+ generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries
21
25
 
22
26
  # @api private
23
27
  # @return [#parse] parser object for pattern
@@ -36,7 +40,14 @@ module Mustermann
36
40
  end
37
41
 
38
42
  # @api private
39
- # @return [#transform] compiler object for pattern
43
+ # @return [#set_boundaries] translator making sure start and stop is set on all nodes
44
+ # @!visibility private
45
+ def self.boundaries
46
+ Boundaries
47
+ end
48
+
49
+ # @api private
50
+ # @return [#transform] transformer object for pattern
40
51
  # @!visibility private
41
52
  def self.transformer
42
53
  Transformer
@@ -49,6 +60,20 @@ module Mustermann
49
60
  Validation
50
61
  end
51
62
 
63
+ # @api private
64
+ # @return [#generate_templates] generates URI templates for pattern
65
+ # @!visibility private
66
+ def self.template_generator
67
+ TemplateGenerator
68
+ end
69
+
70
+ # @api private
71
+ # @return [#scan_params] param scanner for pattern
72
+ # @!visibility private
73
+ def self.param_scanner
74
+ ParamScanner
75
+ end
76
+
52
77
  # @!visibility private
53
78
  def compile(**options)
54
79
  options[:except] &&= parse options[:except]
@@ -62,7 +87,12 @@ module Mustermann
62
87
  # @!visibility private
63
88
  def to_ast
64
89
  @ast_cache ||= Tool::EqualityMap.new
65
- @ast_cache.fetch(@string) { validate(transform(parse(@string))) }
90
+ @ast_cache.fetch(@string) do
91
+ ast = parse(@string, pattern: self)
92
+ ast &&= transform(ast)
93
+ ast &&= set_boundaries(ast, string: @string)
94
+ validate(ast)
95
+ end
66
96
  end
67
97
 
68
98
  # All AST-based pattern implementations support expanding.
@@ -73,12 +103,34 @@ module Mustermann
73
103
  # @raise (see Mustermann::Pattern#expand)
74
104
  # @see Mustermann::Pattern#expand
75
105
  # @see Mustermann::Expander
76
- def expand(**values)
106
+ def expand(behavior = nil, values = {})
77
107
  @expander ||= Mustermann::Expander.new(self)
78
- @expander.expand(**values)
108
+ @expander.expand(behavior, values)
109
+ end
110
+
111
+ # All AST-based pattern implementations support generating templates.
112
+ #
113
+ # @example (see Mustermann::Pattern#to_templates)
114
+ # @param (see Mustermann::Pattern#to_templates)
115
+ # @return (see Mustermann::Pattern#to_templates)
116
+ # @see Mustermann::Pattern#to_templates
117
+ def to_templates
118
+ @to_templates ||= generate_templates(to_ast)
119
+ end
120
+
121
+ # @!visibility private
122
+ # @see Mustermann::Pattern#map_param
123
+ def map_param(key, value)
124
+ return super unless param_converters.include? key
125
+ param_converters[key][super]
126
+ end
127
+
128
+ # @!visibility private
129
+ def param_converters
130
+ @param_converters ||= scan_params(to_ast)
79
131
  end
80
132
 
81
- private :compile
133
+ private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries
82
134
  end
83
135
  end
84
136
  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
@@ -44,7 +44,7 @@ module Mustermann
44
44
  self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" }
45
45
  separator = Node[:separator].new(operator.separator)
46
46
  prefix = Node[:separator].new(operator.prefix)
47
- self.payload = Array(payload.inject { |list, element| Array(list) << t(separator) << t(element) })
47
+ self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) })
48
48
  payload.unshift(prefix) if operator.prefix
49
49
  self
50
50
  end
@@ -91,7 +91,7 @@ module Mustermann
91
91
  # @!visibility private
92
92
  def create_lookahead(elements, *args)
93
93
  return elements unless elements.size > 1
94
- [Node[:with_look_ahead].new(elements, *args)]
94
+ [Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)]
95
95
  end
96
96
 
97
97
  # can the given element be used in a look-ahead?
@@ -74,6 +74,26 @@ module Mustermann
74
74
  end
75
75
  end
76
76
 
77
+ # Enables quick creation of a translator object.
78
+ #
79
+ # @example
80
+ # require 'mustermann'
81
+ # require 'mustermann/ast/translator'
82
+ #
83
+ # translator = Mustermann::AST::Translator.create do
84
+ # translate(:node) { [type, *t(payload)].flatten.compact }
85
+ # translate(Array) { map { |e| t(e) } }
86
+ # translate(Object) { }
87
+ # end
88
+ #
89
+ # ast = Mustermann.new('/:name').to_ast
90
+ # translator.translate(ast) # => [:root, :separator, :capture]
91
+ #
92
+ # @!visibility private
93
+ def self.create(&block)
94
+ Class.new(self, &block).new
95
+ end
96
+
77
97
  raises Mustermann::Error
78
98
 
79
99
  # @param [Mustermann::AST::Node, Object] node to translate
@@ -21,14 +21,15 @@ 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, forbidden: [])
29
30
  raise CompileError, "capture name can't be empty" if name.nil? or name.empty?
30
31
  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"
32
+ raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name
32
33
  raise CompileError, "can't use the same capture name twice" if names.include? name
33
34
  names << name
34
35
  end