json_p3 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,449 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+ require_relative "errors"
5
+
6
+ module JSONP3
7
+ # Base class for all JSON Patch operations
8
+ class Op
9
+ # Return the name of the patch operation.
10
+ def name
11
+ raise "JSON Patch operations must implement #name"
12
+ end
13
+
14
+ # Apply the patch operation to _value_.
15
+ def apply(_value, _index)
16
+ raise "JSON Patch operations must implement apply(value, index)"
17
+ end
18
+
19
+ # Return a JSON-like representation of this patch operation.
20
+ def to_h
21
+ raise "JSON Patch operations must implement #to_h"
22
+ end
23
+ end
24
+
25
+ # The JSON Patch _add_ operation.
26
+ class OpAdd < Op
27
+ # @param pointer [JSONPointer]
28
+ # @param value [JSON-like value]
29
+ def initialize(pointer, value)
30
+ super()
31
+ @pointer = pointer
32
+ @value = value
33
+ end
34
+
35
+ def name
36
+ "add"
37
+ end
38
+
39
+ def apply(value, index)
40
+ parent, obj = @pointer.resolve_with_parent(value)
41
+ return @value if parent == JSONP3::JSONPointer::UNDEFINED && @pointer.tokens.empty?
42
+
43
+ if parent == JSONP3::JSONPointer::UNDEFINED
44
+ raise JSONPatchError,
45
+ "no such property or item '#{@pointer.parent}' (#{name}:#{index})"
46
+ end
47
+
48
+ target = @pointer.tokens.last
49
+ if parent.is_a?(Array)
50
+ if obj == JSONP3::JSONPointer::UNDEFINED
51
+ raise JSONPatchError, "index out of range (#{name}:#{index})" unless target == "-"
52
+
53
+ parent << @value
54
+ else
55
+ parent.insert(target.to_i, @value)
56
+ end
57
+ elsif parent.is_a?(Hash)
58
+ parent[target] = @value
59
+ else
60
+ raise JSONPatchError, "unexpected operation on #{parent.class} (#{name}:#{index})"
61
+ end
62
+
63
+ value
64
+ end
65
+
66
+ def to_h
67
+ { "op" => name, "path" => @pointer.to_s, "value" => @value }
68
+ end
69
+ end
70
+
71
+ # The JSON Patch _remove_ operation.
72
+ class OpRemove < Op
73
+ # @param pointer [JSONPointer]
74
+ def initialize(pointer)
75
+ super()
76
+ @pointer = pointer
77
+ end
78
+
79
+ def name
80
+ "remove"
81
+ end
82
+
83
+ def apply(value, index)
84
+ parent, obj = @pointer.resolve_with_parent(value)
85
+
86
+ if parent == JSONP3::JSONPointer::UNDEFINED && @pointer.tokens.empty?
87
+ raise JSONPatchError,
88
+ "can't remove root (#{name}:#{index})"
89
+ end
90
+
91
+ if parent == JSONP3::JSONPointer::UNDEFINED
92
+ raise JSONPatchError,
93
+ "no such property or item '#{@pointer.parent}' (#{name}:#{index})"
94
+ end
95
+
96
+ target = @pointer.tokens.last
97
+ if target == JSONP3::JSONPointer::UNDEFINED
98
+ raise JSONPatchError,
99
+ "unexpected operation (#{name}:#{index})"
100
+ end
101
+
102
+ if parent.is_a?(Array)
103
+ raise JSONPatchError, "no item to remove (#{name}:#{index})" if obj == JSONP3::JSONPointer::UNDEFINED
104
+
105
+ parent.delete_at(target.to_i)
106
+ elsif parent.is_a?(Hash)
107
+ raise JSONPatchError, "no property to remove (#{name}:#{index})" if obj == JSONP3::JSONPointer::UNDEFINED
108
+
109
+ parent.delete(target)
110
+ else
111
+ raise JSONPatchError, "unexpected operation on #{parent.class} (#{name}:#{index})"
112
+ end
113
+
114
+ value
115
+ end
116
+
117
+ def to_h
118
+ { "op" => name, "path" => @pointer.to_s }
119
+ end
120
+ end
121
+
122
+ # The JSON Patch _replace_ operation.
123
+ class OpReplace < Op
124
+ # @param pointer [JSONPointer]
125
+ # @param value [JSON-like value]
126
+ def initialize(pointer, value)
127
+ super()
128
+ @pointer = pointer
129
+ @value = value
130
+ end
131
+
132
+ def name
133
+ "replace"
134
+ end
135
+
136
+ def apply(value, index)
137
+ parent, obj = @pointer.resolve_with_parent(value)
138
+ return @value if parent == JSONP3::JSONPointer::UNDEFINED && @pointer.tokens.empty?
139
+
140
+ if parent == JSONP3::JSONPointer::UNDEFINED
141
+ raise JSONPatchError,
142
+ "no such property or item '#{@pointer.parent}' (#{name}:#{index})"
143
+ end
144
+
145
+ target = @pointer.tokens.last
146
+ if target == JSONP3::JSONPointer::UNDEFINED
147
+ raise JSONPatchError,
148
+ "unexpected operation (#{name}:#{index})"
149
+ end
150
+
151
+ if parent.is_a?(Array)
152
+ raise JSONPatchError, "no item to replace (#{name}:#{index})" if obj == JSONP3::JSONPointer::UNDEFINED
153
+
154
+ parent[target.to_i] = @value
155
+ elsif parent.is_a?(Hash)
156
+ raise JSONPatchError, "no property to replace (#{name}:#{index})" if obj == JSONP3::JSONPointer::UNDEFINED
157
+
158
+ parent[target] = @value
159
+ else
160
+ raise JSONPatchError, "unexpected operation on #{parent.class} (#{name}:#{index})"
161
+ end
162
+
163
+ value
164
+ end
165
+
166
+ def to_h
167
+ { "op" => name, "path" => @pointer.to_s, "value" => @value }
168
+ end
169
+ end
170
+
171
+ # The JSON Patch _move_ operation.
172
+ class OpMove < Op
173
+ # @param from [JSONPointer]
174
+ # @param pointer [JSONPointer]
175
+ def initialize(from, pointer)
176
+ super()
177
+ @from = from
178
+ @pointer = pointer
179
+ end
180
+
181
+ def name
182
+ "move"
183
+ end
184
+
185
+ def apply(value, index)
186
+ if @pointer.relative_to?(@from)
187
+ raise JSONPatchError,
188
+ "can't move object to one of its children (#{name}:#{index})"
189
+ end
190
+
191
+ # Grab the source value.
192
+ source_parent, source_obj = @from.resolve_with_parent(value)
193
+ if source_obj == JSONP3::JSONPointer::UNDEFINED
194
+ raise JSONPatchError,
195
+ "source object does not exist (#{name}:#{index})"
196
+ end
197
+
198
+ source_target = @from.tokens.last
199
+ if source_target == JSONP3::JSONPointer::UNDEFINED
200
+ raise JSONPatchError,
201
+ "unexpected operation (#{name}:#{index})"
202
+ end
203
+
204
+ # Delete the target value from the source location.
205
+ if source_parent.is_a?(Array)
206
+ source_parent.delete_at(source_target.to_i)
207
+ elsif source_parent.is_a?(Hash)
208
+ source_parent.delete(source_target)
209
+ end
210
+
211
+ # Find the parent of the destination pointer.
212
+ dest_parent, _dest_obj = @pointer.resolve_with_parent(value)
213
+ return source_obj if dest_parent == JSONP3::JSONPointer::UNDEFINED
214
+
215
+ dest_target = @pointer.tokens.last
216
+ if dest_target == JSONP3::JSONPointer::UNDEFINED
217
+ raise JSONPatchError,
218
+ "unexpected operation (#{name}:#{index})"
219
+ end
220
+
221
+ # Write the source value to the destination.
222
+ if dest_parent.is_a?(Array)
223
+ if dest_target == "-"
224
+ dest_parent << source_obj
225
+ else
226
+ dest_parent[dest_target.to_i] = source_obj
227
+ end
228
+ elsif dest_parent.is_a?(Hash)
229
+ dest_parent[dest_target] = source_obj
230
+ end
231
+
232
+ value
233
+ end
234
+
235
+ def to_h
236
+ { "op" => name, "from" => @from.to_s, "path" => @pointer.to_s }
237
+ end
238
+ end
239
+
240
+ # The JSON Patch _copy_ operation.
241
+ class OpCopy < Op
242
+ # @param from [JSONPointer]
243
+ # @param pointer [JSONPointer]
244
+ def initialize(from, pointer)
245
+ super()
246
+ @from = from
247
+ @pointer = pointer
248
+ end
249
+
250
+ def name
251
+ "copy"
252
+ end
253
+
254
+ def apply(value, index)
255
+ # Grab the source value.
256
+ _source_parent, source_obj = @from.resolve_with_parent(value)
257
+ if source_obj == JSONP3::JSONPointer::UNDEFINED
258
+ raise JSONPatchError,
259
+ "source object does not exist (#{name}:#{index})"
260
+ end
261
+
262
+ # Find the parent of the destination pointer.
263
+ dest_parent, _dest_obj = @pointer.resolve_with_parent(value)
264
+ return deep_copy(source_obj) if dest_parent == JSONP3::JSONPointer::UNDEFINED
265
+
266
+ dest_target = @pointer.tokens.last
267
+ if dest_target == JSONP3::JSONPointer::UNDEFINED
268
+ raise JSONPatchError,
269
+ "unexpected operation (#{name}:#{index})"
270
+ end
271
+
272
+ # Write the source value to the destination.
273
+ if dest_parent.is_a?(Array)
274
+ if dest_target == "-"
275
+ dest_parent << source_obj
276
+ else
277
+ dest_parent.insert(dest_target.to_i, deep_copy(source_obj))
278
+ end
279
+ elsif dest_parent.is_a?(Hash)
280
+ dest_parent[dest_target] = deep_copy(source_obj)
281
+ else
282
+ raise JSONPatchError, "unexpected operation on #{dest_parent.class} (#{name}:#{index})"
283
+ end
284
+
285
+ value
286
+ end
287
+
288
+ def to_h
289
+ { "op" => name, "from" => @from.to_s, "path" => @pointer.to_s }
290
+ end
291
+
292
+ private
293
+
294
+ def deep_copy(obj)
295
+ Marshal.load(Marshal.dump(obj))
296
+ end
297
+ end
298
+
299
+ # The JSON Patch _test_ operation.
300
+ class OpTest < Op
301
+ # @param pointer [JSONPointer]
302
+ # @param value [JSON-like value]
303
+ def initialize(pointer, value)
304
+ super()
305
+ @pointer = pointer
306
+ @value = value
307
+ end
308
+
309
+ def name
310
+ "test"
311
+ end
312
+
313
+ def apply(value, index)
314
+ obj = @pointer.resolve(value)
315
+ raise JSONPatchTestFailure, "test failed (#{name}:#{index})" if obj != @value
316
+
317
+ value
318
+ end
319
+
320
+ def to_h
321
+ { "op" => name, "path" => @pointer.to_s, "value" => @value }
322
+ end
323
+ end
324
+
325
+ # A JSON Patch containing zero or more patch operations.
326
+ class JSONPatch
327
+ # @param ops [Array<Op | Hash<String, untyped>>?]
328
+ def initialize(ops = nil)
329
+ @ops = []
330
+ build(ops) unless ops.nil?
331
+ end
332
+
333
+ # @param pointer [String | JSONPointer]
334
+ # @param value [JSON-like value]
335
+ # @return [self]
336
+ def add(pointer, value)
337
+ @ops.push(OpAdd.new(ensure_pointer(pointer, :add, @ops.length), value))
338
+ self
339
+ end
340
+
341
+ # @param pointer [String | JSONPointer]
342
+ # @return [self]
343
+ def remove(pointer)
344
+ @ops.push(OpRemove.new(ensure_pointer(pointer, :remove, @ops.length)))
345
+ self
346
+ end
347
+
348
+ # @param pointer [String | JSONPointer]
349
+ # @param value [JSON-like value]
350
+ # @return [self]
351
+ def replace(pointer, value)
352
+ @ops.push(OpReplace.new(ensure_pointer(pointer, :replace, @ops.length), value))
353
+ self
354
+ end
355
+
356
+ # @param from [String | JSONPointer]
357
+ # @param pointer [String | JSONPointer]
358
+ # @return [self]
359
+ def move(from, pointer)
360
+ @ops.push(OpMove.new(
361
+ ensure_pointer(from, :move, @ops.length),
362
+ ensure_pointer(pointer, :move, @ops.length)
363
+ ))
364
+ self
365
+ end
366
+
367
+ # @param from [String | JSONPointer]
368
+ # @param pointer [String | JSONPointer]
369
+ # @return [self]
370
+ def copy(from, pointer)
371
+ @ops.push(OpCopy.new(
372
+ ensure_pointer(from, :copy, @ops.length),
373
+ ensure_pointer(pointer, :copy, @ops.length)
374
+ ))
375
+ self
376
+ end
377
+
378
+ # @param pointer [String | JSONPointer]
379
+ # @param value [JSON-like value]
380
+ # @return [self]
381
+ def test(pointer, value)
382
+ @ops.push(OpTest.new(ensure_pointer(pointer, :test, @ops.length), value))
383
+ self
384
+ end
385
+
386
+ # Apply this patch to JSON-like value _value_.
387
+ def apply(value)
388
+ @ops.each_with_index { |op, i| value = op.apply(value, i) }
389
+ value
390
+ end
391
+
392
+ def to_a
393
+ @ops.map(&:to_h)
394
+ end
395
+
396
+ private
397
+
398
+ # @param ops [Array<Op | Hash<String, untyped>>?]
399
+ # @return void
400
+ def build(ops)
401
+ ops.each_with_index do |obj, i|
402
+ if obj.is_a?(Op)
403
+ @ops << obj
404
+ next
405
+ end
406
+
407
+ case obj["op"]
408
+ when "add"
409
+ add(op_pointer(obj, "path", "add", i), op_value(obj, "value", "add", i))
410
+ when "remove"
411
+ remove(op_pointer(obj, "path", "remove", i))
412
+ when "replace"
413
+ replace(op_pointer(obj, "path", "replace", i), op_value(obj, "value", "replace", i))
414
+ when "move"
415
+ move(op_pointer(obj, "from", "move", i), op_pointer(obj, "path", "move", i))
416
+ when "copy"
417
+ copy(op_pointer(obj, "from", "copy", i), op_pointer(obj, "path", "copy", i))
418
+ when "test"
419
+ test(op_pointer(obj, "path", "test", i), op_value(obj, "value", "test", i))
420
+ else
421
+ raise JSONPatchError,
422
+ "expected 'op' to be one of 'add', 'remove', 'replace', 'move', 'copy' or 'test' (#{obj["op"]}:#{i})"
423
+ end
424
+ end
425
+ end
426
+
427
+ def op_pointer(obj, key, op, index)
428
+ raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
429
+
430
+ JSONP3::JSONPointer.new(obj[key])
431
+ rescue JSONPointerError
432
+ raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
433
+ end
434
+
435
+ def op_value(obj, key, op, index)
436
+ raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
437
+
438
+ obj[key]
439
+ end
440
+
441
+ def ensure_pointer(pointer, op, index)
442
+ return pointer unless pointer.is_a?(String)
443
+
444
+ JSONP3::JSONPointer.new(pointer)
445
+ rescue JSONPointerError
446
+ raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
447
+ end
448
+ end
449
+ end
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module JSONP3
6
+ # Identify a single value in JSON-like data, as per RFC 6901.
7
+ class JSONPointer
8
+ RE_INT = /\A(0|[1-9][0-9]*)\z/
9
+ UNDEFINED = :__undefined
10
+
11
+ attr_reader :tokens
12
+
13
+ # Encode an array of strings and integers into a JSON Pointer.
14
+ # @param tokens [Array<String | Integer> | nil]
15
+ # @return [String]
16
+ def self.encode(tokens)
17
+ return "" if tokens.nil? || tokens.empty?
18
+
19
+ encoded = tokens.map do |token|
20
+ token.is_a?(Integer) ? token.to_s : token.gsub("~", "~0").gsub("/", "~1")
21
+ end
22
+
23
+ "/#{encoded.join("/")}"
24
+ end
25
+
26
+ # @param pointer [String]
27
+ def initialize(pointer)
28
+ @tokens = parse(pointer)
29
+ @pointer = JSONPointer.encode(@tokens)
30
+ end
31
+
32
+ # Resolve this pointer against JSON-like data _value_.
33
+ # @param value [Object]
34
+ # @param default [Object] the value to return if this pointer can not be
35
+ # resolved against _value_.
36
+ def resolve(value, default: UNDEFINED)
37
+ item = value
38
+
39
+ @tokens.each do |token|
40
+ item = get_item(item, token)
41
+ return default if item == UNDEFINED
42
+ end
43
+
44
+ item
45
+ end
46
+
47
+ # Resolve this pointer against _value_, returning the resolved object and its
48
+ # parent object.
49
+ #
50
+ # @param value [Object]
51
+ # @return [Array<Object>] an array with exactly two elements, one or both of
52
+ # which could be undefined.
53
+ def resolve_with_parent(value)
54
+ return [UNDEFINED, resolve(value)] if @tokens.empty?
55
+
56
+ parent = value
57
+ (@tokens[...-1] || raise).each do |token|
58
+ parent = get_item(parent, token)
59
+ break if parent == UNDEFINED
60
+ end
61
+
62
+ [parent, get_item(parent, @tokens.last)]
63
+ end
64
+
65
+ # Return true if this pointer is relative to _pointer_.
66
+ # @param pointer [JSONPointer]
67
+ # @return [bool]
68
+ def relative_to?(pointer)
69
+ pointer.tokens.length < @tokens.length && @tokens[...pointer.tokens.length] == pointer.tokens
70
+ end
71
+
72
+ # @param parts [String]
73
+ # @return [JSONPointer]
74
+ def join(*parts)
75
+ pointer = self
76
+ parts.each do |part|
77
+ pointer = pointer._join(part)
78
+ end
79
+ pointer
80
+ end
81
+
82
+ # Return _true_ if this pointer can be resolved against _value_, even if the resolved
83
+ # value is false or nil.
84
+ # @param value [Object]
85
+ def exist?(value)
86
+ resolve(value) != UNDEFINED
87
+ end
88
+
89
+ # Return this pointer's parent as a new pointer. If this pointer points to the
90
+ # document root, self is returned.
91
+ def parent
92
+ return self if @tokens.empty?
93
+
94
+ JSONPointer.new(JSONPointer.encode((@tokens[...-1] || raise)))
95
+ end
96
+
97
+ # Return a new pointer relative to this pointer using Relative JSON Pointer syntax.
98
+ # @param rel [String | RelativeJSONPointer]
99
+ # @return [JSONPointer]
100
+ def to(rel)
101
+ p = rel.is_a?(String) ? RelativeJSONPointer.new(rel) : rel
102
+ p.to(self)
103
+ end
104
+
105
+ def to_s
106
+ @pointer
107
+ end
108
+
109
+ protected
110
+
111
+ # @param pointer [String]
112
+ # @return [Array<String | Integer>]
113
+ def parse(pointer)
114
+ if pointer.length.positive? && !pointer.start_with?("/")
115
+ raise JSONPointerSyntaxError,
116
+ "pointers must start with a slash or be the empty string"
117
+ end
118
+
119
+ return [] if pointer.empty?
120
+ return [""] if pointer == "/"
121
+
122
+ (pointer[1..] || raise).split("/", -1).map do |token|
123
+ token.match?(/\A(?:0|[1-9][0-9]*)\z/) ? Integer(token) : token.gsub("~1", "/").gsub("~0", "~")
124
+ end
125
+ end
126
+
127
+ # @param value [Object]
128
+ # @param token [String | Integer]
129
+ # @return [Object] the "fetched" object from _value_ or UNDEFINED.
130
+ def get_item(value, token)
131
+ if value.is_a?(Array)
132
+ if token.is_a?(String) && token.start_with?("#")
133
+ maybe_index = token[1..] || raise
134
+ return maybe_index.to_i if RE_INT.match?(maybe_index)
135
+ end
136
+
137
+ return UNDEFINED unless token.is_a?(Integer)
138
+ return UNDEFINED if token.negative? || token >= value.length
139
+
140
+ value[token]
141
+ elsif value.is_a?(Hash)
142
+ return value[token] if value.key?(token)
143
+
144
+ # Handle "#" from relative JSON pointer
145
+ return token[1..] if token.is_a?(String) && token.start_with?("#") && value.key?(token[1..])
146
+
147
+ # Token might be an integer. Force it to a string and try again.
148
+ string_token = token.to_s
149
+ value.key?(string_token) ? value[string_token] : UNDEFINED
150
+ else
151
+ UNDEFINED
152
+ end
153
+ end
154
+
155
+ # Like `#parse`, but assumes there's no leading slash.
156
+ # @param pointer [String]
157
+ # @return [Array<String | Integer>]
158
+ def _parse(pointer)
159
+ return [] if pointer.empty?
160
+
161
+ pointer.split("/", -1).map do |token|
162
+ token.match?(/\A(?:0|[1-9][0-9]*)\z/) ? Integer(token) : token.gsub("~1", "/").gsub("~0", "~")
163
+ end
164
+ end
165
+
166
+ def _join(other)
167
+ raise JSONPointerTypeError, "unsupported join part" unless other.is_a?(String)
168
+
169
+ part = other.lstrip
170
+ part.start_with?("/") ? JSONPointer.new(part) : JSONPointer.new(JSONPointer.encode(@tokens + _parse(part)))
171
+ end
172
+ end
173
+
174
+ # A relative JSON Pointer.
175
+ # See https://datatracker.ietf.org/doc/html/draft-hha-relative-json-pointer
176
+ class RelativeJSONPointer
177
+ RE_RELATIVE_POINTER = /\A(?<ORIGIN>\d+)(?<INDEX_G>(?<SIGN>[+-])(?<INDEX>\d))?(?<POINTER>.*)\z/m
178
+ RE_INT = /\A(0|[1-9][0-9]*)\z/
179
+
180
+ # @param rel [String]
181
+ def initialize(rel)
182
+ match = RE_RELATIVE_POINTER.match(rel)
183
+
184
+ raise JSONPointerSyntaxError, "failed to parse relative pointer" if match.nil?
185
+
186
+ @origin = parse_int(match[:ORIGIN] || raise)
187
+ @index = 0
188
+
189
+ if match[:INDEX_G]
190
+ @index = parse_int(match[:INDEX] || raise)
191
+ raise JSONPointerSyntaxError, "index offset can't be zero" if @index.zero?
192
+
193
+ @index = -@index if match[:SIGN] == "-"
194
+ end
195
+
196
+ @pointer = match[:POINTER] == "#" ? "#" : JSONPointer.new(match[:POINTER] || raise)
197
+ end
198
+
199
+ def to_s
200
+ sign = @index.positive? ? "+" : ""
201
+ index = @index.zero? ? "" : "#{sign}#{@index}"
202
+ "#{@origin}#{index}#{@pointer}"
203
+ end
204
+
205
+ # Return a new JSON Pointer by applying this relative pointer to _pointer_.
206
+ # @param pointer [String | JSONPointer]
207
+ # @return [JSONPointer]
208
+ def to(pointer)
209
+ p = pointer.is_a?(String) ? JSONPointer.new(pointer) : pointer
210
+
211
+ raise JSONPointerIndexError, "origin (#{@origin}) exceeds root (#{p.tokens.length})" if @origin > p.tokens.length
212
+
213
+ tokens = @origin < 1 ? p.tokens[0..] || raise : p.tokens[0...-@origin] || raise
214
+ tokens[-1] = (tokens[-1] || raise) + @index if @index != 0 && tokens.length.positive? && tokens[-1].is_a?(Integer)
215
+
216
+ if @pointer == "#"
217
+ tokens[-1] = "##{tokens[-1]}"
218
+ else
219
+ tokens.concat(@pointer.tokens) # steep:ignore
220
+ end
221
+
222
+ JSONPointer.new(JSONPointer.encode(tokens))
223
+ end
224
+
225
+ private
226
+
227
+ # @param token [String]
228
+ # @return [Integer]
229
+ def parse_int(token)
230
+ raise JSONPointerSyntaxError, "unexpected leading zero" if token.start_with?("0") && token.length > 1
231
+ raise JSONPointerSyntaxError, "expected an integer, found '#{token}'" unless RE_INT.match?(token)
232
+
233
+ token.to_i
234
+ end
235
+ end
236
+ end
@@ -21,7 +21,7 @@ module JSONP3
21
21
  # The child selection segment.
22
22
  class ChildSegment < Segment
23
23
  def resolve(nodes)
24
- rv = []
24
+ rv = [] # : Array[JSONPathNode]
25
25
  nodes.each do |node|
26
26
  @selectors.each do |selector|
27
27
  rv.concat selector.resolve(node)
@@ -50,7 +50,7 @@ module JSONP3
50
50
  # The recursive descent segment
51
51
  class RecursiveDescentSegment < Segment
52
52
  def resolve(nodes)
53
- rv = []
53
+ rv = [] # : Array[JSONPathNode]
54
54
  nodes.each do |node|
55
55
  visit(node).each do |descendant|
56
56
  @selectors.each do |selector|
@@ -79,7 +79,7 @@ module JSONP3
79
79
 
80
80
  protected
81
81
 
82
- def visit(node, depth = 1) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
82
+ def visit(node, depth = 1)
83
83
  raise JSONPathRecursionError.new("recursion limit exceeded", @token) if depth > @env.class::MAX_RECURSION_DEPTH
84
84
 
85
85
  rv = [node]