json_p3 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,553 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+
6
+ require_relative "errors"
7
+ require_relative "filter"
8
+ require_relative "function"
9
+ require_relative "segment"
10
+ require_relative "selector"
11
+ require_relative "token"
12
+ require_relative "unescape"
13
+
14
+ module JSONP3
15
+ # Step through tokens
16
+ class Stream
17
+ def initialize(tokens)
18
+ @tokens = tokens
19
+ @index = 0
20
+ @eoi = tokens.last
21
+ end
22
+
23
+ def next
24
+ token = @tokens.fetch(@index)
25
+ @index += 1
26
+ token
27
+ rescue IndexError
28
+ @eor
29
+ end
30
+
31
+ def peek
32
+ @tokens.fetch(@index)
33
+ rescue IndexError
34
+ @eor
35
+ end
36
+
37
+ def expect(token_type)
38
+ return if peek.type == token_type
39
+
40
+ token = self.next
41
+ raise JSONPathSyntaxError.new("expected #{token_type}, found #{token.type}", token)
42
+ end
43
+
44
+ def expect_not(token_type, message)
45
+ return unless peek.type == token_type
46
+
47
+ token = self.next
48
+ raise JSONPathSyntaxError.new(message, token)
49
+ end
50
+
51
+ def to_s
52
+ "JSONP3::stream(head=#{peek.inspect})"
53
+ end
54
+ end
55
+
56
+ class Precedence
57
+ LOWEST = 1
58
+ LOGICAL_OR = 3
59
+ LOGICAL_AND = 4
60
+ RELATIONAL = 5
61
+ PREFIX = 7
62
+ end
63
+
64
+ # A JSONPath expression parser.
65
+ class Parser # rubocop:disable Metrics/ClassLength
66
+ def initialize(env)
67
+ @env = env
68
+ @name_selector = env.class::NAME_SELECTOR
69
+ @index_selector = env.class::INDEX_SELECTOR
70
+ end
71
+
72
+ # Parse an array of tokens into an abstract syntax tree.
73
+ # @param tokens [Array<Token>] tokens from the lexer.
74
+ # @return [Array<Segment>]
75
+ def parse(tokens)
76
+ stream = Stream.new(tokens)
77
+ stream.expect(Token::ROOT)
78
+ stream.next
79
+ parse_query(stream)
80
+ end
81
+
82
+ protected
83
+
84
+ def parse_query(stream) # rubocop:disable Metrics/MethodLength
85
+ segments = []
86
+
87
+ loop do
88
+ case stream.peek.type
89
+ when Token::DOUBLE_DOT
90
+ token = stream.next
91
+ selectors = parse_selectors(stream)
92
+ segments << RecursiveDescentSegment.new(@env, token, selectors)
93
+ when Token::LBRACKET, Token::NAME, Token::WILD
94
+ token = stream.peek
95
+ selectors = parse_selectors(stream)
96
+ segments << ChildSegment.new(@env, token, selectors)
97
+ else
98
+ break
99
+ end
100
+ end
101
+
102
+ segments
103
+ end
104
+
105
+ def parse_selectors(stream) # rubocop:disable Metrics/MethodLength
106
+ case stream.peek.type
107
+ when Token::NAME
108
+ token = stream.next
109
+ [@name_selector.new(@env, token, token.value)]
110
+ when Token::WILD
111
+ [WildcardSelector.new(@env, stream.next)]
112
+ when Token::LBRACKET
113
+ parse_bracketed_selection(stream)
114
+ else
115
+ []
116
+ end
117
+ end
118
+
119
+ def parse_bracketed_selection(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
120
+ stream.expect Token::LBRACKET
121
+ segment_token = stream.next
122
+
123
+ selectors = []
124
+
125
+ loop do # rubocop:disable Metrics/BlockLength
126
+ case stream.peek.type
127
+ when Token::RBRACKET
128
+ break
129
+ when Token::INDEX
130
+ selectors << parse_index_or_slice(stream)
131
+ when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
132
+ token = stream.next
133
+ selectors << @name_selector.new(@env, token, decode_string_literal(token))
134
+ when Token::COLON
135
+ selectors << parse_slice_selector(stream)
136
+ when Token::WILD
137
+ selectors << WildcardSelector.new(@env, stream.next)
138
+ when Token::FILTER
139
+ selectors << parse_filter_selector(stream)
140
+ when Token::EOI
141
+ raise JSONPathSyntaxError.new("unexpected end of query", stream.next)
142
+ else
143
+ raise JSONPathSyntaxError.new("unexpected token in bracketed selection", stream.next)
144
+ end
145
+
146
+ case stream.peek.type
147
+ when Token::EOI
148
+ raise JSONPathSyntaxError.new("unexpected end of selector list", stream.next)
149
+ when Token::RBRACKET
150
+ break
151
+ else
152
+ stream.expect Token::COMMA
153
+ stream.next
154
+ stream.expect_not(Token::RBRACKET, "unexpected trailing comma")
155
+ end
156
+ end
157
+
158
+ stream.expect(Token::RBRACKET)
159
+ stream.next
160
+
161
+ raise JSONPathSyntaxError.new("empty segment", segment_token) if selectors.empty?
162
+
163
+ selectors
164
+ end
165
+
166
+ def parse_index_or_slice(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
167
+ token = stream.next
168
+ index = parse_i_json_int(token)
169
+
170
+ return @index_selector.new(@env, token, index) unless stream.peek.type == Token::COLON
171
+
172
+ stream.next # move past colon
173
+ stop = nil
174
+ step = nil
175
+
176
+ case stream.peek.type
177
+ when Token::INDEX
178
+ stop = parse_i_json_int(stream.next)
179
+ when Token::COLON
180
+ stream.next # move past colon
181
+ end
182
+
183
+ stream.next if stream.peek.type == Token::COLON
184
+
185
+ case stream.peek.type
186
+ when Token::INDEX
187
+ step = parse_i_json_int(stream.next)
188
+ when Token::RBRACKET
189
+ nil
190
+ else
191
+ error_token = stream.next
192
+ raise JSONPathSyntaxError.new("expected a slice, found '#{error_token.value}'", error_token)
193
+ end
194
+
195
+ SliceSelector.new(@env, token, index, stop, step)
196
+ end
197
+
198
+ def parse_slice_selector(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
199
+ stream.expect(Token::COLON)
200
+ token = stream.next
201
+
202
+ start = nil
203
+ stop = nil
204
+ step = nil
205
+
206
+ case stream.peek.type
207
+ when Token::INDEX
208
+ stop = parse_i_json_int(stream.next)
209
+ when Token::COLON
210
+ stream.next # move past colon
211
+ end
212
+
213
+ stream.next if stream.peek.type == Token::COLON
214
+
215
+ case stream.peek.type
216
+ when Token::INDEX
217
+ step = parse_i_json_int(stream.next)
218
+ when Token::RBRACKET
219
+ nil
220
+ else
221
+ error_token = stream.next
222
+ raise JSONPathSyntaxError.new("expected a slice, found '#{token.value}'", error_token)
223
+ end
224
+
225
+ SliceSelector.new(@env, token, start, stop, step)
226
+ end
227
+
228
+ def parse_filter_selector(stream) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
229
+ token = stream.next
230
+ expression = parse_filter_expression(stream)
231
+
232
+ # Raise if expression must be compared.
233
+ if expression.is_a? FunctionExpression
234
+ func = @env.function_extensions[expression.name]
235
+ if func.class::RETURN_TYPE == ExpressionType::VALUE
236
+ raise JSONPathTypeError.new("result of #{expression.name}() must be compared", expression.token)
237
+ end
238
+ end
239
+
240
+ # Raise if expression is a literal.
241
+ if expression.is_a? FilterExpressionLiteral
242
+ raise JSONPathSyntaxError.new("filter expression literals must be compared", expression.token)
243
+ end
244
+
245
+ FilterSelector.new(@env, token, FilterExpression.new(token, expression))
246
+ end
247
+
248
+ def parse_filter_expression(stream, precedence = Precedence::LOWEST) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
249
+ left = case stream.peek.type
250
+ when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
251
+ token = stream.next
252
+ StringLiteral.new(token, decode_string_literal(token))
253
+ when Token::FALSE
254
+ BooleanLiteral.new(stream.next, false)
255
+ when Token::TRUE
256
+ BooleanLiteral.new(stream.next, true)
257
+ when Token::FLOAT
258
+ parse_float_literal(stream)
259
+ when Token::FUNCTION
260
+ parse_function_expression(stream)
261
+ when Token::INT
262
+ parse_integer_literal(stream)
263
+ when Token::LPAREN
264
+ parse_grouped_expression(stream)
265
+ when Token::NOT
266
+ parse_prefix_expression(stream)
267
+ when Token::NULL
268
+ NullLiteral.new(stream.next, nil)
269
+ when Token::ROOT
270
+ parse_root_query(stream)
271
+ when Token::CURRENT
272
+ parse_relative_query(stream)
273
+ else
274
+ token = stream.next
275
+ raise JSONPathSyntaxError.new("unexpected '#{token.value}'", token)
276
+ end
277
+
278
+ loop do
279
+ peeked = stream.peek
280
+ if peeked.type == Token::EOI ||
281
+ peeked.type == Token::RBRACKET ||
282
+ PRECEDENCES.fetch(peeked.type, Precedence::LOWEST) < precedence
283
+ break
284
+ end
285
+
286
+ return left unless BINARY_OPERATORS.key?(peeked.type)
287
+
288
+ left = parse_infix_expression(stream, left)
289
+ end
290
+
291
+ left
292
+ end
293
+
294
+ def parse_integer_literal(stream)
295
+ token = stream.next
296
+ value = token.value
297
+ raise JSONPathSyntaxError.new("invalid integer literal", token) if value.start_with?("0") && value.length > 1
298
+
299
+ IntegerLiteral.new(token, Integer(Float(token.value)))
300
+ end
301
+
302
+ def parse_float_literal(stream)
303
+ token = stream.next
304
+ value = token.value
305
+ if value.start_with?("0") && value.split(".").first.length > 1
306
+ raise JSONPathSyntaxError.new("invalid float literal", token)
307
+ end
308
+
309
+ begin
310
+ FloatLiteral.new(token, Float(value))
311
+ rescue ArgumentError
312
+ raise JSONPathSyntaxError.new("invalid float literal", token)
313
+ end
314
+ end
315
+
316
+ def parse_function_expression(stream) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
317
+ token = stream.next
318
+ args = []
319
+
320
+ while stream.peek.type != Token::RPAREN
321
+ expr = case stream.peek.type
322
+ when Token::DOUBLE_QUOTE_STRING, Token::SINGLE_QUOTE_STRING
323
+ arg_token = stream.next
324
+ StringLiteral.new(arg_token, decode_string_literal(arg_token))
325
+ when Token::FALSE
326
+ BooleanLiteral.new(stream.next, false)
327
+ when Token::TRUE
328
+ BooleanLiteral.new(stream.next, true)
329
+ when Token::FLOAT
330
+ parse_float_literal(stream)
331
+ when Token::FUNCTION
332
+ parse_function_expression(stream)
333
+ when Token::INT
334
+ parse_integer_literal(stream)
335
+ when Token::NULL
336
+ NullLiteral.new(stream.next, nil)
337
+ when Token::ROOT
338
+ parse_root_query(stream)
339
+ when Token::CURRENT
340
+ parse_relative_query(stream)
341
+ else
342
+ arg_token = stream.next
343
+ raise JSONPathSyntaxError.new("unexpected '#{arg_token.value}'", arg_token)
344
+ end
345
+
346
+ expr = parse_infix_expression(stream, expr) while BINARY_OPERATORS.key? stream.peek.type
347
+
348
+ args << expr
349
+
350
+ if stream.peek.type != Token::RPAREN
351
+ stream.expect(Token::COMMA)
352
+ stream.next
353
+ end
354
+ end
355
+
356
+ stream.expect(Token::RPAREN)
357
+ stream.next
358
+
359
+ validate_function_extension_signature(token, args)
360
+ FunctionExpression.new(token, token.value, args)
361
+ end
362
+
363
+ def parse_grouped_expression(stream)
364
+ stream.next # discard "("
365
+ expr = parse_filter_expression(stream)
366
+
367
+ while stream.peek.type != Token::RPAREN
368
+ raise JSONPathSyntaxError.new("unbalanced parentheses", stream.peek) if stream.peek.type == Token::EOI
369
+
370
+ expr = parse_infix_expression(stream, expr)
371
+ end
372
+
373
+ stream.expect(Token::RPAREN)
374
+ stream.next
375
+ expr
376
+ end
377
+
378
+ def parse_prefix_expression(stream)
379
+ token = stream.next
380
+ LogicalNotExpression.new(token, parse_filter_expression(stream, Precedence::PREFIX))
381
+ end
382
+
383
+ def parse_root_query(stream)
384
+ token = stream.next
385
+ RootQueryExpression.new(token, JSONPath.new(@env, parse_query(stream)))
386
+ end
387
+
388
+ def parse_relative_query(stream)
389
+ token = stream.next
390
+ RelativeQueryExpression.new(token, JSONPath.new(@env, parse_query(stream)))
391
+ end
392
+
393
+ def parse_infix_expression(stream, left) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
394
+ token = stream.next
395
+ precedence = PRECEDENCES.fetch(token.type, Precedence::LOWEST)
396
+ right = parse_filter_expression(stream, precedence)
397
+
398
+ if COMPARISON_OPERATORS.member? token.value
399
+ raise_for_non_comparable_function(left)
400
+ raise_for_non_comparable_function(right)
401
+ case token.type
402
+ when Token::EQ
403
+ EqExpression.new(token, left, right)
404
+ when Token::GE
405
+ GeExpression.new(token, left, right)
406
+ when Token::GT
407
+ GtExpression.new(token, left, right)
408
+ when Token::LE
409
+ LeExpression.new(token, left, right)
410
+ when Token::LT
411
+ LtExpression.new(token, left, right)
412
+ when Token::NE
413
+ NeExpression.new(token, left, right)
414
+ else
415
+ raise JSONPathSyntaxError.new("unexpected token", token)
416
+ end
417
+ else
418
+ raise_for_uncompared_literal(left)
419
+ raise_for_uncompared_literal(right)
420
+ case token.type
421
+ when Token::AND
422
+ LogicalAndExpression.new(token, left, right)
423
+ when Token::OR
424
+ LogicalOrExpression.new(token, left, right)
425
+ else
426
+ raise JSONPathSyntaxError.new("unexpected token", token)
427
+ end
428
+ end
429
+ end
430
+
431
+ def parse_i_json_int(token) # rubocop:disable Metrics/MethodLength
432
+ value = token.value
433
+
434
+ if value.length > 1 && value.start_with?("0", "-0")
435
+ raise JSONPathSyntaxError.new("invalid index '#{value}'", token)
436
+ end
437
+
438
+ begin
439
+ int = Integer(value)
440
+ rescue ArgumentError
441
+ raise JSONPathSyntaxError.new("invalid I-JSON integer", token)
442
+ end
443
+
444
+ if int < @env.class::MIN_INT_INDEX || int > @env.class::MAX_INT_INDEX
445
+ raise JSONPathSyntaxError.new("index out of range",
446
+ token)
447
+ end
448
+
449
+ int
450
+ end
451
+
452
+ def decode_string_literal(token)
453
+ if token.type == Token::SINGLE_QUOTE_STRING
454
+ JSONP3.unescape_string(token.value, "'", token)
455
+ else
456
+ JSONP3.unescape_string(token.value, '"', token)
457
+ end
458
+ end
459
+
460
+ def raise_for_non_comparable_function(expression)
461
+ if expression.is_a?(QueryExpression) && !expression.query.singular?
462
+ raise JSONPathSyntaxError.new("non-singular query is not comparable", expression.token)
463
+ end
464
+
465
+ return unless expression.is_a?(FunctionExpression)
466
+
467
+ func = @env.function_extensions[expression.name]
468
+ return unless func.class::RETURN_TYPE != ExpressionType::VALUE
469
+
470
+ raise JSONPathTypeError.new("result of #{expression.name}() is not comparable", expression.token)
471
+ end
472
+
473
+ def raise_for_uncompared_literal(expression)
474
+ return unless expression.is_a? FilterExpressionLiteral
475
+
476
+ raise JSONPathSyntaxError.new("expression literals must be compared",
477
+ expression.token)
478
+ end
479
+
480
+ def validate_function_extension_signature(token, args) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
481
+ func = @env.function_extensions.fetch(token.value)
482
+ count = func.class::ARG_TYPES.length
483
+
484
+ unless args.length == count
485
+ raise JSONPathTypeError.new(
486
+ "#{token.value}() takes #{count} argument#{count == 1 ? "" : "s"} (#{args.length} given)",
487
+ token
488
+ )
489
+ end
490
+
491
+ func.class::ARG_TYPES.each_with_index do |t, i|
492
+ arg = args[i]
493
+ case t
494
+ when ExpressionType::VALUE
495
+ unless arg.is_a?(FilterExpressionLiteral) ||
496
+ (arg.is_a?(QueryExpression) && arg.query.singular?) ||
497
+ (function_return_type(arg) == ExpressionType::VALUE)
498
+ raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of ValueType", arg.token)
499
+ end
500
+ when ExpressionType::LOGICAL
501
+ unless arg.is_a?(QueryExpression) || arg.is_a?(InfixExpression)
502
+ raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of LogicalType", arg.token)
503
+ end
504
+ when ExpressionType::NODES
505
+ unless arg.is_a?(QueryExpression) || function_return_type(arg) == ExpressionType::NODES
506
+ raise JSONPathTypeError.new("#{token.value}() argument #{i} must be of NodesType", arg.token)
507
+ end
508
+ end
509
+ end
510
+ rescue KeyError
511
+ raise JSONPathNameError.new("function '#{token.value}' is not defined", token)
512
+ end
513
+
514
+ def function_return_type(expression)
515
+ return nil unless expression.is_a? FunctionExpression
516
+
517
+ @env.function_extensions[expression.name].class::RETURN_TYPE
518
+ end
519
+
520
+ PRECEDENCES = {
521
+ Token::AND => Precedence::LOGICAL_AND,
522
+ Token::OR => Precedence::LOGICAL_OR,
523
+ Token::NOT => Precedence::PREFIX,
524
+ Token::EQ => Precedence::RELATIONAL,
525
+ Token::GE => Precedence::RELATIONAL,
526
+ Token::GT => Precedence::RELATIONAL,
527
+ Token::LE => Precedence::RELATIONAL,
528
+ Token::LT => Precedence::RELATIONAL,
529
+ Token::NE => Precedence::RELATIONAL,
530
+ Token::RPAREN => Precedence::LOWEST
531
+ }.freeze
532
+
533
+ BINARY_OPERATORS = {
534
+ Token::AND => "&&",
535
+ Token::OR => "||",
536
+ Token::EQ => "==",
537
+ Token::GE => ">=",
538
+ Token::GT => ">",
539
+ Token::LE => "<=",
540
+ Token::LT => "<",
541
+ Token::NE => "!="
542
+ }.freeze
543
+
544
+ COMPARISON_OPERATORS = Set[
545
+ "==",
546
+ ">=",
547
+ ">",
548
+ "<=",
549
+ "<",
550
+ "!=",
551
+ ]
552
+ end
553
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "node"
4
+
5
+ module JSONP3
6
+ # A compiled JSONPath expression ready to be applied to JSON-like values.
7
+ class JSONPath
8
+ def initialize(env, segments)
9
+ @env = env
10
+ @segments = segments
11
+ end
12
+
13
+ def to_s
14
+ "$#{@segments.map(&:to_s).join}"
15
+ end
16
+
17
+ # Apply this JSONPath expression to JSON-like value _root_.
18
+ # @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
19
+ # @return [Array<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
20
+ def find(root)
21
+ nodes = [JSONPathNode.new(root, [], root)]
22
+ @segments.each { |segment| nodes = segment.resolve(nodes) }
23
+ JSONPathNodeList.new(nodes)
24
+ end
25
+
26
+ alias apply find
27
+
28
+ # Return _true_ if this JSONPath expression is a singular query.
29
+ def singular?
30
+ @segments.each do |segment|
31
+ return false if segment.instance_of? RecursiveDescentSegment
32
+ return false unless segment.selectors.length == 1 && segment.selectors[0].singular?
33
+ end
34
+ true
35
+ end
36
+
37
+ # Return _true_ if this JSONPath expression has no segments.
38
+ def empty?
39
+ @segments.empty?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSONP3
4
+ # Base class for all JSONPath segments.
5
+ class Segment
6
+ # @dynamic token, selectors
7
+ attr_reader :token, :selectors
8
+
9
+ def initialize(env, token, selectors)
10
+ @env = env
11
+ @token = token
12
+ @selectors = selectors
13
+ end
14
+
15
+ # Select the children of each node in _nodes_.
16
+ def resolve(_nodes)
17
+ raise "segments must implement resolve(nodes)"
18
+ end
19
+ end
20
+
21
+ # The child selection segment.
22
+ class ChildSegment < Segment
23
+ def resolve(nodes)
24
+ rv = []
25
+ nodes.each do |node|
26
+ @selectors.each do |selector|
27
+ rv.concat selector.resolve(node)
28
+ end
29
+ end
30
+ rv
31
+ end
32
+
33
+ def to_s
34
+ "[#{@selectors.map(&:to_s).join(", ")}]"
35
+ end
36
+
37
+ def ==(other)
38
+ self.class == other.class &&
39
+ @selectors == other.selectors &&
40
+ @token == other.token
41
+ end
42
+
43
+ alias eql? ==
44
+
45
+ def hash
46
+ [@selectors, @token].hash
47
+ end
48
+ end
49
+
50
+ # The recursive descent segment
51
+ class RecursiveDescentSegment < Segment
52
+ def resolve(nodes)
53
+ rv = []
54
+ nodes.each do |node|
55
+ visit(node).each do |descendant|
56
+ @selectors.each do |selector|
57
+ rv.concat selector.resolve(descendant)
58
+ end
59
+ end
60
+ end
61
+ rv
62
+ end
63
+
64
+ def to_s
65
+ "..[#{@selectors.map(&:to_s).join(", ")}]"
66
+ end
67
+
68
+ def ==(other)
69
+ self.class == other.class &&
70
+ @selectors == other.selectors &&
71
+ @token == other.token
72
+ end
73
+
74
+ alias eql? ==
75
+
76
+ def hash
77
+ ["..", @selectors, @token].hash
78
+ end
79
+
80
+ protected
81
+
82
+ def visit(node, depth = 1) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
83
+ raise JSONPathRecursionError.new("recursion limit exceeded", @token) if depth > @env.class::MAX_RECURSION_DEPTH
84
+
85
+ rv = [node]
86
+
87
+ if node.value.is_a? Array
88
+ node.value.each_with_index do |value, i|
89
+ child = JSONPathNode.new(value, [node.location, i], node.root)
90
+ rv.concat visit(child, depth + 1)
91
+ end
92
+ elsif node.value.is_a? Hash
93
+ node.value.each do |key, value|
94
+ child = JSONPathNode.new(value, [node.location, key], node.root)
95
+ rv.concat visit(child, depth + 1)
96
+ end
97
+ end
98
+
99
+ rv
100
+ end
101
+ end
102
+ end