json_p3 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +9 -0
- data/README.md +149 -17
- data/certs/jgrp.pem +27 -0
- data/lib/json_p3/cache.rb +1 -1
- data/lib/json_p3/environment.rb +1 -1
- data/lib/json_p3/errors.rb +9 -1
- data/lib/json_p3/filter.rb +4 -4
- data/lib/json_p3/function.rb +0 -6
- data/lib/json_p3/function_extensions/count.rb +2 -2
- data/lib/json_p3/function_extensions/length.rb +2 -2
- data/lib/json_p3/function_extensions/match.rb +3 -3
- data/lib/json_p3/function_extensions/pattern.rb +1 -1
- data/lib/json_p3/function_extensions/search.rb +3 -3
- data/lib/json_p3/function_extensions/value.rb +2 -2
- data/lib/json_p3/lexer.rb +54 -55
- data/lib/json_p3/parser.rb +112 -112
- data/lib/json_p3/patch.rb +449 -0
- data/lib/json_p3/pointer.rb +236 -0
- data/lib/json_p3/segment.rb +3 -3
- data/lib/json_p3/selector.rb +4 -4
- data/lib/json_p3/token.rb +0 -38
- data/lib/json_p3/unescape.rb +5 -5
- data/lib/json_p3/version.rb +1 -1
- data/lib/json_p3.rb +10 -0
- data/sig/json_p3.rbs +322 -104
- data.tar.gz.sig +0 -0
- metadata +6 -3
- metadata.gz.sig +0 -0
@@ -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
|
data/lib/json_p3/segment.rb
CHANGED
@@ -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)
|
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]
|