mustermann 0.3.1 → 0.4.0

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