mustermann 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.test_queue_stats +0 -0
- data/.travis.yml +6 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +644 -0
- data/Rakefile +3 -0
- data/bench/capturing.rb +24 -0
- data/bench/simple_vs_sinatra.rb +23 -0
- data/bench/template_vs_addressable.rb +23 -0
- data/lib/mustermann.rb +40 -0
- data/lib/mustermann/ast.rb +403 -0
- data/lib/mustermann/error.rb +14 -0
- data/lib/mustermann/extension.rb +52 -0
- data/lib/mustermann/identity.rb +19 -0
- data/lib/mustermann/pattern.rb +142 -0
- data/lib/mustermann/rails.rb +26 -0
- data/lib/mustermann/regexp_based.rb +30 -0
- data/lib/mustermann/shell.rb +22 -0
- data/lib/mustermann/simple.rb +35 -0
- data/lib/mustermann/simple_match.rb +30 -0
- data/lib/mustermann/sinatra.rb +32 -0
- data/lib/mustermann/template.rb +140 -0
- data/lib/mustermann/version.rb +3 -0
- data/mustermann.gemspec +28 -0
- data/spec/ast_spec.rb +8 -0
- data/spec/extension_spec.rb +153 -0
- data/spec/identity_spec.rb +82 -0
- data/spec/mustermann_spec.rb +0 -0
- data/spec/pattern_spec.rb +16 -0
- data/spec/rails_spec.rb +453 -0
- data/spec/regexp_based_spec.rb +8 -0
- data/spec/shell_spec.rb +108 -0
- data/spec/simple_match_spec.rb +10 -0
- data/spec/simple_spec.rb +236 -0
- data/spec/sinatra_spec.rb +554 -0
- data/spec/support.rb +78 -0
- data/spec/template_spec.rb +814 -0
- metadata +227 -0
data/Rakefile
ADDED
data/bench/capturing.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann'
|
5
|
+
require 'addressable/template'
|
6
|
+
|
7
|
+
list = [
|
8
|
+
/\A\/(?<splat>.*?)\/(?<name>[^\/\?#]+)\Z/,
|
9
|
+
Mustermann.new('/*/:name', type: :sinatra),
|
10
|
+
Mustermann.new('/*/:name', type: :simple),
|
11
|
+
Mustermann.new('/*prefix/:name', type: :rails),
|
12
|
+
Mustermann.new('{/prefix*}/{name}', type: :template),
|
13
|
+
#Addressable::Template.new('{/prefix*}/{name}')
|
14
|
+
]
|
15
|
+
|
16
|
+
string = '/a/b/c/d'
|
17
|
+
|
18
|
+
Benchmark.bmbm do |x|
|
19
|
+
list.each do |pattern|
|
20
|
+
x.report pattern.class.to_s do
|
21
|
+
100_000.times { pattern.match(string).captures }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/simple'
|
5
|
+
require 'mustermann/sinatra'
|
6
|
+
|
7
|
+
[Mustermann::Simple, Mustermann::Sinatra].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/:name")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
splat = klass.new("/*")
|
19
|
+
x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } }
|
20
|
+
x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } }
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', __dir__)
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'mustermann/template'
|
5
|
+
require 'addressable/template'
|
6
|
+
|
7
|
+
[Mustermann::Template, Addressable::Template].each do |klass|
|
8
|
+
puts "", " #{klass} ".center(64, '=')
|
9
|
+
Benchmark.bmbm do |x|
|
10
|
+
no_capture = klass.new("/simple")
|
11
|
+
x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } }
|
12
|
+
x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } }
|
13
|
+
|
14
|
+
simple = klass.new("/{match}")
|
15
|
+
x.report("simple, match") { 1_000.times { simple.match('/simple').captures } }
|
16
|
+
x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } }
|
17
|
+
|
18
|
+
explode = klass.new("{/segments*}")
|
19
|
+
x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } }
|
20
|
+
x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } }
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
data/lib/mustermann.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Namespace and main entry point for the Mustermann library.
|
2
|
+
#
|
3
|
+
# Under normal circumstences the only external API entry point you should be using is {Mustermann.new}.
|
4
|
+
module Mustermann
|
5
|
+
# @param [String] string The string represenation of the new pattern
|
6
|
+
# @param [Hash] options The options hash
|
7
|
+
# @return [Mustermann::Pattern] pattern corresponding to string.
|
8
|
+
# @see file:README.md#Types_and_Options "Types and Options" in the README
|
9
|
+
def self.new(string, type: :sinatra, **options)
|
10
|
+
options.any? ? self[type].new(string, **options) : self[type].new(string)
|
11
|
+
end
|
12
|
+
|
13
|
+
# @!visibility private
|
14
|
+
def self.[](key)
|
15
|
+
constant, library = register.fetch(key)
|
16
|
+
require library if library
|
17
|
+
constant.respond_to?(:new) ? constant : register[key] = const_get(constant)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @!visibility private
|
21
|
+
def self.register(identifier = nil, constant = identifier.to_s.capitalize, load: "mustermann/#{identifier}")
|
22
|
+
@register ||= {}
|
23
|
+
@register[identifier] = [constant, load] if identifier
|
24
|
+
@register
|
25
|
+
end
|
26
|
+
|
27
|
+
# @!visibility private
|
28
|
+
def self.extend_object(object)
|
29
|
+
return super unless defined? ::Sinatra::Base and object < ::Sinatra::Base
|
30
|
+
require 'mustermann/extension'
|
31
|
+
object.register Extension
|
32
|
+
end
|
33
|
+
|
34
|
+
register :identity
|
35
|
+
register :rails
|
36
|
+
register :shell
|
37
|
+
register :simple
|
38
|
+
register :sinatra
|
39
|
+
register :template
|
40
|
+
end
|
@@ -0,0 +1,403 @@
|
|
1
|
+
require 'mustermann/regexp_based'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Mustermann
|
5
|
+
# Superclass for pattern styles that parse an AST from the string pattern.
|
6
|
+
# @abstract
|
7
|
+
class AST < RegexpBased
|
8
|
+
supported_options :capture, :except, :greedy, :space_matches_plus
|
9
|
+
|
10
|
+
# @!visibility private
|
11
|
+
class Node
|
12
|
+
# @!visibility private
|
13
|
+
attr_accessor :payload
|
14
|
+
|
15
|
+
# Helper for creating a new instance and calling #parse on it.
|
16
|
+
# @return [Node]
|
17
|
+
# @!visibility private
|
18
|
+
def self.parse(element = new, &block)
|
19
|
+
element.tap { |n| n.parse(&block) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
def initialize(payload = nil, **options)
|
24
|
+
options.each { |key, value| public_send("#{key}=", value) }
|
25
|
+
self.payload = payload
|
26
|
+
end
|
27
|
+
|
28
|
+
# Double dispatch helper for reading from the buffer into the payload.
|
29
|
+
# @!visibility private
|
30
|
+
def parse
|
31
|
+
self.payload ||= []
|
32
|
+
while element = yield
|
33
|
+
payload << element
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] regular expression corresponding to node
|
38
|
+
# @!visibility private
|
39
|
+
def compile(options)
|
40
|
+
Array(payload).map { |e| e.compile(options) }.join
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Node] This node after tree transformation. Might be self.
|
44
|
+
# @!visibility private
|
45
|
+
def transform
|
46
|
+
self.payload = payload.transform if payload.respond_to? :transform
|
47
|
+
return self unless Array === payload
|
48
|
+
|
49
|
+
new_payload = []
|
50
|
+
with_lookahead = []
|
51
|
+
|
52
|
+
payload.each do |element|
|
53
|
+
element = element.transform
|
54
|
+
if with_lookahead.empty?
|
55
|
+
list = element.expect_lookahead? ? with_lookahead : new_payload
|
56
|
+
list << element
|
57
|
+
elsif element.lookahead?
|
58
|
+
with_lookahead << element
|
59
|
+
else
|
60
|
+
with_lookahead = [WithLookAhead.new(with_lookahead, false)] if element.separator? and with_lookahead.size > 1
|
61
|
+
new_payload.concat(with_lookahead)
|
62
|
+
new_payload << element
|
63
|
+
with_lookahead.clear
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
with_lookahead = [WithLookAhead.new(with_lookahead, true)] if with_lookahead.size > 1
|
68
|
+
new_payload.concat(with_lookahead)
|
69
|
+
@payload = new_payload
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# @!visibility private
|
74
|
+
def separator?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
# @!visibility private
|
79
|
+
def lookahead?(in_lookahead = false)
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
# @!visibility private
|
84
|
+
def expect_lookahead?
|
85
|
+
false
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [String] Regular expression for matching the given character in all representations
|
89
|
+
# @!visibility private
|
90
|
+
def encoded(char, uri_decode, space_matches_plus)
|
91
|
+
return Regexp.escape(char) unless uri_decode
|
92
|
+
uri_parser = URI::Parser.new
|
93
|
+
encoded = uri_parser.escape(char, /./)
|
94
|
+
list = [uri_parser.escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) }
|
95
|
+
list << encoded('+', uri_decode, space_matches_plus) if space_matches_plus and char == " "
|
96
|
+
"(?:%s)" % list.join("|")
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Array<String>]
|
100
|
+
# @!visibility private
|
101
|
+
def capture_names
|
102
|
+
return payload.capture_names if payload.respond_to? :capture_names
|
103
|
+
return [] unless payload.respond_to? :map
|
104
|
+
payload.map { |e| e.capture_names if e.respond_to? :capture_names }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @!visibility private
|
109
|
+
class Char < Node
|
110
|
+
# @!visibility private
|
111
|
+
def compile(uri_decode: true, space_matches_plus: true, **options)
|
112
|
+
encoded(payload, uri_decode, space_matches_plus)
|
113
|
+
end
|
114
|
+
|
115
|
+
# @!visibility private
|
116
|
+
def lookahead?(in_lookahead = false)
|
117
|
+
in_lookahead
|
118
|
+
end
|
119
|
+
|
120
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
121
|
+
# @!visibility private
|
122
|
+
def lookahead(ahead, options)
|
123
|
+
ahead + compile(options)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# @!visibility private
|
128
|
+
class Separator < Node
|
129
|
+
# @!visibility private
|
130
|
+
def compile(options)
|
131
|
+
Regexp.escape(payload)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @!visibility private
|
135
|
+
def separator?
|
136
|
+
true
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# @!visibility private
|
141
|
+
class Optional < Node
|
142
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
143
|
+
# @!visibility private
|
144
|
+
def lookahead(ahead, options)
|
145
|
+
payload.lookahead(ahead, options)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @!visibility private
|
149
|
+
def compile(options)
|
150
|
+
"(?:%s)?" % payload.compile(options)
|
151
|
+
end
|
152
|
+
|
153
|
+
# @!visibility private
|
154
|
+
def lookahead?(in_lookahead = false)
|
155
|
+
payload.lookahead? true or payload.expect_lookahead?
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @!visibility private
|
160
|
+
class Group < Node
|
161
|
+
# @!visibility private
|
162
|
+
def initialize(payload = nil, **options)
|
163
|
+
super(Array(payload), **options)
|
164
|
+
end
|
165
|
+
|
166
|
+
# @!visibility private
|
167
|
+
def lookahead?(in_lookahead = false)
|
168
|
+
return false unless payload[0..-2].all? { |e| e.lookahead? in_lookahead }
|
169
|
+
payload.last.expect_lookahead? or payload.last.lookahead? in_lookahead
|
170
|
+
end
|
171
|
+
|
172
|
+
# @!visibility private
|
173
|
+
def transform
|
174
|
+
payload.size == 1 ? payload.first.transform : super
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!visibility private
|
178
|
+
def parse
|
179
|
+
super
|
180
|
+
rescue UnexpectedClosingGroup
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
184
|
+
# @!visibility private
|
185
|
+
def lookahead(ahead, options)
|
186
|
+
payload.inject(ahead) { |a,e| e.lookahead(a, options) }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @!visibility private
|
191
|
+
class Capture < Node
|
192
|
+
# @!visibility private
|
193
|
+
def expect_lookahead?
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
# @!visibility private
|
198
|
+
def parse
|
199
|
+
self.payload ||= ""
|
200
|
+
super
|
201
|
+
end
|
202
|
+
|
203
|
+
# @!visibility private
|
204
|
+
def capture_names
|
205
|
+
[name]
|
206
|
+
end
|
207
|
+
|
208
|
+
# @!visibility private
|
209
|
+
def name
|
210
|
+
raise CompileError, "capture name can't be empty" if payload.nil? or payload.empty?
|
211
|
+
raise CompileError, "capture name must start with underscore or lower case letter" unless payload =~ /^[a-z_]/
|
212
|
+
raise CompileError, "capture name can't be #{payload}" if payload == "splat" or payload == "captures"
|
213
|
+
payload
|
214
|
+
end
|
215
|
+
|
216
|
+
# @return [String] regexp without the named capture
|
217
|
+
# @!visibility private
|
218
|
+
def pattern(capture: nil, **options)
|
219
|
+
case capture
|
220
|
+
when Symbol then from_symbol(capture, **options)
|
221
|
+
when Array then from_array(capture, **options)
|
222
|
+
when Hash then from_hash(capture, **options)
|
223
|
+
when String then from_string(capture, **options)
|
224
|
+
when nil then from_nil(**options)
|
225
|
+
else capture
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# @return [String] regexp to be used in lookahead for semi-greedy capturing
|
230
|
+
# @!visibility private
|
231
|
+
def lookahead(ahead, options)
|
232
|
+
ahead + pattern(lookahead: ahead, greedy: false, **options).to_s
|
233
|
+
end
|
234
|
+
|
235
|
+
# @!visibility private
|
236
|
+
def compile(options)
|
237
|
+
return pattern(options) if options[:no_captures]
|
238
|
+
"(?<#{name}>#{compile(no_captures: true, **options)})"
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
def qualified(string, greedy: true, **options)
|
244
|
+
"#{string}+#{?? unless greedy}"
|
245
|
+
end
|
246
|
+
|
247
|
+
def default(**options)
|
248
|
+
"[^/\\?#]"
|
249
|
+
end
|
250
|
+
|
251
|
+
def from_nil(**options)
|
252
|
+
qualified(with_lookahead(default(**options), **options), **options)
|
253
|
+
end
|
254
|
+
|
255
|
+
def from_hash(hash, **options)
|
256
|
+
entry = hash[name.to_sym]
|
257
|
+
pattern(capture: entry, **options)
|
258
|
+
end
|
259
|
+
|
260
|
+
def from_array(array, **options)
|
261
|
+
array = array.map { |e| pattern(capture: e, **options) }
|
262
|
+
Regexp.union(*array)
|
263
|
+
end
|
264
|
+
|
265
|
+
def from_symbol(symbol, **options)
|
266
|
+
qualified(with_lookahead("[[:#{symbol}:]]", **options), **options)
|
267
|
+
end
|
268
|
+
|
269
|
+
def from_string(string, uri_decode: true, space_matches_plus: true, **options)
|
270
|
+
Regexp.new(string.chars.map { |c| encoded(c, uri_decode, space_matches_plus) }.join)
|
271
|
+
end
|
272
|
+
|
273
|
+
def with_lookahead(string, lookahead: nil, **options)
|
274
|
+
return string unless lookahead
|
275
|
+
"(?:(?!#{lookahead})#{string})"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# @!visibility private
|
280
|
+
class Splat < Capture
|
281
|
+
# @!visibility private
|
282
|
+
def expect_lookahead?
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
# @!visibility private
|
287
|
+
def name
|
288
|
+
"splat"
|
289
|
+
end
|
290
|
+
|
291
|
+
# @!visibility private
|
292
|
+
def pattern(options)
|
293
|
+
".*?"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# @!visibility private
|
298
|
+
class NamedSplat < Splat
|
299
|
+
alias_method :name, :payload
|
300
|
+
end
|
301
|
+
|
302
|
+
# @!visibility private
|
303
|
+
class WithLookAhead < Node
|
304
|
+
# @!visibility private
|
305
|
+
attr_accessor :head, :at_end
|
306
|
+
|
307
|
+
# @!visibility private
|
308
|
+
def initialize(payload, at_end)
|
309
|
+
self.head, *self.payload = payload
|
310
|
+
self.at_end = at_end
|
311
|
+
end
|
312
|
+
|
313
|
+
# @!visibility private
|
314
|
+
def compile(options)
|
315
|
+
lookahead = payload.inject('') { |l,e| e.lookahead(l, options) }
|
316
|
+
lookahead << (at_end ? '$' : '/')
|
317
|
+
head.compile(lookahead: lookahead, **options) + super
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# @!visibility private
|
322
|
+
class Root < Node
|
323
|
+
# @!visibility private
|
324
|
+
attr_accessor :pattern
|
325
|
+
|
326
|
+
# @!visibility private
|
327
|
+
def self.parse(string, &block)
|
328
|
+
root = new
|
329
|
+
root.pattern = string
|
330
|
+
super(root, &block).transform
|
331
|
+
end
|
332
|
+
|
333
|
+
# @!visibility private
|
334
|
+
def capture_names
|
335
|
+
super.flatten
|
336
|
+
end
|
337
|
+
|
338
|
+
# @!visibility private
|
339
|
+
def check_captures
|
340
|
+
names = capture_names
|
341
|
+
names.delete("splat")
|
342
|
+
raise CompileError, "can't use the same capture name twice" if names.uniq != names
|
343
|
+
end
|
344
|
+
|
345
|
+
# @!visibility private
|
346
|
+
def compile(except: nil, **options)
|
347
|
+
check_captures
|
348
|
+
except &&= "(?!#{except}\\Z)"
|
349
|
+
Regexp.new("\\A#{except}#{super(options)}\\Z")
|
350
|
+
rescue CompileError => e
|
351
|
+
e.message << ": #{pattern.inspect}"
|
352
|
+
raise e
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# @!visibility private
|
357
|
+
def parse(string)
|
358
|
+
buffer = StringScanner.new(string)
|
359
|
+
Root.parse(string) { parse_buffer(buffer) unless buffer.eos? }
|
360
|
+
rescue ParseError => e
|
361
|
+
e.message << " while parsing #{string.inspect}"
|
362
|
+
raise e
|
363
|
+
end
|
364
|
+
|
365
|
+
# @!visibility private
|
366
|
+
def compile(string, except: nil, **options)
|
367
|
+
options[:except] = compile(except, no_captures: true, **options) if except
|
368
|
+
parse(string).compile(options)
|
369
|
+
end
|
370
|
+
|
371
|
+
# @!visibility private
|
372
|
+
def parse_buffer(buffer)
|
373
|
+
parse_suffix(parse_element(buffer), buffer)
|
374
|
+
end
|
375
|
+
|
376
|
+
# @!visibility private
|
377
|
+
def parse_element(buffer)
|
378
|
+
raise NotImplementedError, 'subclass responsibility'
|
379
|
+
end
|
380
|
+
|
381
|
+
# @!visibility private
|
382
|
+
def parse_suffix(element, buffer)
|
383
|
+
element
|
384
|
+
end
|
385
|
+
|
386
|
+
# @!visibility private
|
387
|
+
def unexpected(char, exception: ParseError)
|
388
|
+
char = char.getch if char.respond_to? :getch
|
389
|
+
char = "space" if char == " "
|
390
|
+
raise exception, "unexpected #{char || "end of string"}"
|
391
|
+
end
|
392
|
+
|
393
|
+
# @!visibility private
|
394
|
+
def expect(buffer, regexp, **options)
|
395
|
+
regexp = Regexp.new Regexp.escape(regexp.to_str) unless regexp.is_a? Regexp
|
396
|
+
string = buffer.scan(regexp) || unexpected(buffer, **options)
|
397
|
+
regexp.names.any? ? regexp.match(string) : string
|
398
|
+
end
|
399
|
+
|
400
|
+
private :parse, :compile, :parse_buffer, :parse_element, :parse_suffix, :unexpected, :expect
|
401
|
+
private_constant :Node, :Char, :Separator, :Optional, :Group, :Capture, :Splat, :NamedSplat, :WithLookAhead, :Root
|
402
|
+
end
|
403
|
+
end
|