mustermann 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +429 -672
- data/lib/mustermann.rb +95 -20
- data/lib/mustermann/ast/boundaries.rb +44 -0
- data/lib/mustermann/ast/compiler.rb +13 -7
- data/lib/mustermann/ast/expander.rb +22 -12
- data/lib/mustermann/ast/node.rb +69 -5
- data/lib/mustermann/ast/param_scanner.rb +20 -0
- data/lib/mustermann/ast/parser.rb +138 -19
- data/lib/mustermann/ast/pattern.rb +59 -7
- data/lib/mustermann/ast/template_generator.rb +28 -0
- data/lib/mustermann/ast/transformer.rb +2 -2
- data/lib/mustermann/ast/translator.rb +20 -0
- data/lib/mustermann/ast/validation.rb +4 -3
- data/lib/mustermann/composite.rb +101 -0
- data/lib/mustermann/expander.rb +2 -2
- data/lib/mustermann/identity.rb +56 -0
- data/lib/mustermann/pattern.rb +185 -10
- data/lib/mustermann/pattern_cache.rb +49 -0
- data/lib/mustermann/regexp.rb +1 -0
- data/lib/mustermann/regexp_based.rb +18 -1
- data/lib/mustermann/regular.rb +4 -1
- data/lib/mustermann/simple_match.rb +5 -0
- data/lib/mustermann/sinatra.rb +22 -5
- data/lib/mustermann/to_pattern.rb +11 -6
- data/lib/mustermann/version.rb +1 -1
- data/mustermann.gemspec +1 -14
- data/spec/ast_spec.rb +14 -0
- data/spec/composite_spec.rb +147 -0
- data/spec/expander_spec.rb +15 -0
- data/spec/identity_spec.rb +44 -0
- data/spec/mustermann_spec.rb +17 -2
- data/spec/pattern_spec.rb +7 -3
- data/spec/regular_spec.rb +25 -0
- data/spec/sinatra_spec.rb +184 -9
- data/spec/to_pattern_spec.rb +49 -0
- metadata +15 -180
- data/.gitignore +0 -18
- data/.rspec +0 -2
- data/.travis.yml +0 -4
- data/.yardopts +0 -1
- data/Gemfile +0 -2
- data/LICENSE +0 -22
- data/Rakefile +0 -6
- data/internals.md +0 -64
- data/lib/mustermann/ast/tree_renderer.rb +0 -29
- data/lib/mustermann/rails.rb +0 -17
- data/lib/mustermann/shell.rb +0 -29
- data/lib/mustermann/simple.rb +0 -35
- data/lib/mustermann/template.rb +0 -47
- data/spec/rails_spec.rb +0 -521
- data/spec/shell_spec.rb +0 -108
- data/spec/simple_spec.rb +0 -236
- data/spec/support.rb +0 -5
- data/spec/support/coverage.rb +0 -16
- data/spec/support/env.rb +0 -16
- data/spec/support/expand_matcher.rb +0 -27
- data/spec/support/match_matcher.rb +0 -39
- data/spec/support/pattern.rb +0 -39
- 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
|
63
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
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.
|
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 =
|
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 [#
|
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)
|
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(
|
106
|
+
def expand(behavior = nil, values = {})
|
77
107
|
@expander ||= Mustermann::Expander.new(self)
|
78
|
-
@expander.expand(
|
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
|
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
|
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
|