json_p3 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +5 -0
- data/README.md +142 -16
- 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 +441 -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 +4 -1
- metadata +5 -2
- metadata.gz.sig +0 -0
@@ -0,0 +1,441 @@
|
|
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
|
+
dest_parent[dest_target.to_i] = source_obj
|
224
|
+
elsif dest_parent.is_a?(Hash)
|
225
|
+
dest_parent[dest_target] = source_obj
|
226
|
+
end
|
227
|
+
|
228
|
+
value
|
229
|
+
end
|
230
|
+
|
231
|
+
def to_h
|
232
|
+
{ "op" => name, "from" => @from.to_s, "path" => @pointer.to_s }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# The JSON Patch _copy_ operation.
|
237
|
+
class OpCopy < Op
|
238
|
+
# @param from [JSONPointer]
|
239
|
+
# @param pointer [JSONPointer]
|
240
|
+
def initialize(from, pointer)
|
241
|
+
super()
|
242
|
+
@from = from
|
243
|
+
@pointer = pointer
|
244
|
+
end
|
245
|
+
|
246
|
+
def name
|
247
|
+
"copy"
|
248
|
+
end
|
249
|
+
|
250
|
+
def apply(value, index)
|
251
|
+
# Grab the source value.
|
252
|
+
_source_parent, source_obj = @from.resolve_with_parent(value)
|
253
|
+
if source_obj == JSONP3::JSONPointer::UNDEFINED
|
254
|
+
raise JSONPatchError,
|
255
|
+
"source object does not exist (#{name}:#{index})"
|
256
|
+
end
|
257
|
+
|
258
|
+
# Find the parent of the destination pointer.
|
259
|
+
dest_parent, _dest_obj = @pointer.resolve_with_parent(value)
|
260
|
+
return deep_copy(source_obj) if dest_parent == JSONP3::JSONPointer::UNDEFINED
|
261
|
+
|
262
|
+
dest_target = @pointer.tokens.last
|
263
|
+
if dest_target == JSONP3::JSONPointer::UNDEFINED
|
264
|
+
raise JSONPatchError,
|
265
|
+
"unexpected operation (#{name}:#{index})"
|
266
|
+
end
|
267
|
+
|
268
|
+
# Write the source value to the destination.
|
269
|
+
if dest_parent.is_a?(Array)
|
270
|
+
dest_parent.insert(dest_target.to_i, deep_copy(source_obj))
|
271
|
+
elsif dest_parent.is_a?(Hash)
|
272
|
+
dest_parent[dest_target] = deep_copy(source_obj)
|
273
|
+
else
|
274
|
+
raise JSONPatchError, "unexpected operation on #{dest_parent.class} (#{name}:#{index})"
|
275
|
+
end
|
276
|
+
|
277
|
+
value
|
278
|
+
end
|
279
|
+
|
280
|
+
def to_h
|
281
|
+
{ "op" => name, "from" => @from.to_s, "path" => @pointer.to_s }
|
282
|
+
end
|
283
|
+
|
284
|
+
private
|
285
|
+
|
286
|
+
def deep_copy(obj)
|
287
|
+
Marshal.load(Marshal.dump(obj))
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# The JSON Patch _test_ operation.
|
292
|
+
class OpTest < Op
|
293
|
+
# @param pointer [JSONPointer]
|
294
|
+
# @param value [JSON-like value]
|
295
|
+
def initialize(pointer, value)
|
296
|
+
super()
|
297
|
+
@pointer = pointer
|
298
|
+
@value = value
|
299
|
+
end
|
300
|
+
|
301
|
+
def name
|
302
|
+
"test"
|
303
|
+
end
|
304
|
+
|
305
|
+
def apply(value, index)
|
306
|
+
obj = @pointer.resolve(value)
|
307
|
+
raise JSONPatchTestFailure, "test failed (#{name}:#{index})" if obj != @value
|
308
|
+
|
309
|
+
value
|
310
|
+
end
|
311
|
+
|
312
|
+
def to_h
|
313
|
+
{ "op" => name, "path" => @pointer.to_s, "value" => @value }
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# A JSON Patch containing zero or more patch operations.
|
318
|
+
class JSONPatch
|
319
|
+
# @param ops [Array<Op | Hash<String, untyped>>?]
|
320
|
+
def initialize(ops = nil)
|
321
|
+
@ops = []
|
322
|
+
build(ops) unless ops.nil?
|
323
|
+
end
|
324
|
+
|
325
|
+
# @param pointer [String | JSONPointer]
|
326
|
+
# @param value [JSON-like value]
|
327
|
+
# @return [self]
|
328
|
+
def add(pointer, value)
|
329
|
+
@ops.push(OpAdd.new(ensure_pointer(pointer, :add, @ops.length), value))
|
330
|
+
self
|
331
|
+
end
|
332
|
+
|
333
|
+
# @param pointer [String | JSONPointer]
|
334
|
+
# @return [self]
|
335
|
+
def remove(pointer)
|
336
|
+
@ops.push(OpRemove.new(ensure_pointer(pointer, :remove, @ops.length)))
|
337
|
+
self
|
338
|
+
end
|
339
|
+
|
340
|
+
# @param pointer [String | JSONPointer]
|
341
|
+
# @param value [JSON-like value]
|
342
|
+
# @return [self]
|
343
|
+
def replace(pointer, value)
|
344
|
+
@ops.push(OpReplace.new(ensure_pointer(pointer, :replace, @ops.length), value))
|
345
|
+
self
|
346
|
+
end
|
347
|
+
|
348
|
+
# @param from [String | JSONPointer]
|
349
|
+
# @param pointer [String | JSONPointer]
|
350
|
+
# @return [self]
|
351
|
+
def move(from, pointer)
|
352
|
+
@ops.push(OpMove.new(
|
353
|
+
ensure_pointer(from, :move, @ops.length),
|
354
|
+
ensure_pointer(pointer, :move, @ops.length)
|
355
|
+
))
|
356
|
+
self
|
357
|
+
end
|
358
|
+
|
359
|
+
# @param from [String | JSONPointer]
|
360
|
+
# @param pointer [String | JSONPointer]
|
361
|
+
# @return [self]
|
362
|
+
def copy(from, pointer)
|
363
|
+
@ops.push(OpCopy.new(
|
364
|
+
ensure_pointer(from, :copy, @ops.length),
|
365
|
+
ensure_pointer(pointer, :copy, @ops.length)
|
366
|
+
))
|
367
|
+
self
|
368
|
+
end
|
369
|
+
|
370
|
+
# @param pointer [String | JSONPointer]
|
371
|
+
# @param value [JSON-like value]
|
372
|
+
# @return [self]
|
373
|
+
def test(pointer, value)
|
374
|
+
@ops.push(OpTest.new(ensure_pointer(pointer, :test, @ops.length), value))
|
375
|
+
self
|
376
|
+
end
|
377
|
+
|
378
|
+
# Apply this patch to JSON-like value _value_.
|
379
|
+
def apply(value)
|
380
|
+
@ops.each_with_index { |op, i| value = op.apply(value, i) }
|
381
|
+
value
|
382
|
+
end
|
383
|
+
|
384
|
+
def to_a
|
385
|
+
@ops.map(&:to_h)
|
386
|
+
end
|
387
|
+
|
388
|
+
private
|
389
|
+
|
390
|
+
# @param ops [Array<Op | Hash<String, untyped>>?]
|
391
|
+
# @return void
|
392
|
+
def build(ops)
|
393
|
+
ops.each_with_index do |obj, i|
|
394
|
+
if obj.is_a?(Op)
|
395
|
+
@ops << obj
|
396
|
+
next
|
397
|
+
end
|
398
|
+
|
399
|
+
case obj["op"]
|
400
|
+
when "add"
|
401
|
+
add(op_pointer(obj, "path", "add", i), op_value(obj, "value", "add", i))
|
402
|
+
when "remove"
|
403
|
+
remove(op_pointer(obj, "path", "remove", i))
|
404
|
+
when "replace"
|
405
|
+
replace(op_pointer(obj, "path", "replace", i), op_value(obj, "value", "replace", i))
|
406
|
+
when "move"
|
407
|
+
move(op_pointer(obj, "from", "move", i), op_pointer(obj, "path", "move", i))
|
408
|
+
when "copy"
|
409
|
+
copy(op_pointer(obj, "from", "copy", i), op_pointer(obj, "path", "copy", i))
|
410
|
+
when "test"
|
411
|
+
test(op_pointer(obj, "path", "test", i), op_value(obj, "value", "test", i))
|
412
|
+
else
|
413
|
+
raise JSONPatchError,
|
414
|
+
"expected 'op' to be one of 'add', 'remove', 'replace', 'move', 'copy' or 'test' (#{obj["op"]}:#{i})"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def op_pointer(obj, key, op, index)
|
420
|
+
raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
|
421
|
+
|
422
|
+
JSONP3::JSONPointer.new(obj[key])
|
423
|
+
rescue JSONPointerError
|
424
|
+
raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
|
425
|
+
end
|
426
|
+
|
427
|
+
def op_value(obj, key, op, index)
|
428
|
+
raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
|
429
|
+
|
430
|
+
obj[key]
|
431
|
+
end
|
432
|
+
|
433
|
+
def ensure_pointer(pointer, op, index)
|
434
|
+
return pointer unless pointer.is_a?(String)
|
435
|
+
|
436
|
+
JSONP3::JSONPointer.new(pointer)
|
437
|
+
rescue JSONPointerError
|
438
|
+
raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
|
439
|
+
end
|
440
|
+
end
|
441
|
+
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]
|