json_p3 0.4.0 → 1.0.0

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +26 -7
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +58 -0
  6. data/README.md +125 -123
  7. data/Rakefile +3 -3
  8. data/certs/jgrp.pem +21 -21
  9. data/lib/json_p3/errors.rb +51 -43
  10. data/lib/json_p3/patch/op.rb +23 -0
  11. data/lib/json_p3/patch/op_add.rb +51 -0
  12. data/lib/json_p3/patch/op_copy.rb +64 -0
  13. data/lib/json_p3/patch/op_move.rb +74 -0
  14. data/lib/json_p3/patch/op_remove.rb +56 -0
  15. data/lib/json_p3/patch/op_replace.rb +54 -0
  16. data/lib/json_p3/patch/op_test.rb +31 -0
  17. data/lib/json_p3/patch.rb +15 -330
  18. data/lib/json_p3/path/environment.rb +113 -0
  19. data/lib/json_p3/path/filter.rb +463 -0
  20. data/lib/json_p3/path/function.rb +12 -0
  21. data/lib/json_p3/path/function_extensions/count.rb +15 -0
  22. data/lib/json_p3/path/function_extensions/length.rb +17 -0
  23. data/lib/json_p3/path/function_extensions/match.rb +62 -0
  24. data/lib/json_p3/path/function_extensions/pattern.rb +42 -0
  25. data/lib/json_p3/path/function_extensions/search.rb +44 -0
  26. data/lib/json_p3/path/function_extensions/value.rb +15 -0
  27. data/lib/json_p3/path/lexer.rb +220 -0
  28. data/lib/json_p3/path/node.rb +48 -0
  29. data/lib/json_p3/path/parser.rb +676 -0
  30. data/lib/json_p3/path/query.rb +74 -0
  31. data/lib/json_p3/path/segment.rb +172 -0
  32. data/lib/json_p3/path/selector.rb +304 -0
  33. data/lib/json_p3/path/serialize.rb +16 -0
  34. data/lib/json_p3/path/unescape.rb +134 -0
  35. data/lib/json_p3/pointer.rb +15 -76
  36. data/lib/json_p3/relative_pointer.rb +69 -0
  37. data/lib/json_p3/version.rb +1 -1
  38. data/lib/json_p3.rb +50 -13
  39. data/sig/json_p3/cache.rbs +21 -0
  40. data/sig/json_p3/errors.rbs +55 -0
  41. data/sig/json_p3/patch.rbs +145 -0
  42. data/sig/json_p3/path/environment.rbs +81 -0
  43. data/sig/json_p3/path/filter.rbs +196 -0
  44. data/sig/json_p3/path/function.rbs +94 -0
  45. data/sig/json_p3/path/lexer.rbs +62 -0
  46. data/sig/json_p3/path/node.rbs +46 -0
  47. data/sig/json_p3/path/parser.rbs +92 -0
  48. data/sig/json_p3/path/query.rbs +47 -0
  49. data/sig/json_p3/path/segment.rbs +54 -0
  50. data/sig/json_p3/path/selector.rbs +100 -0
  51. data/sig/json_p3/path/serialize.rbs +9 -0
  52. data/sig/json_p3/path/unescape.rbs +12 -0
  53. data/sig/json_p3/pointer.rbs +64 -0
  54. data/sig/json_p3/relative_pointer.rbs +30 -0
  55. data/sig/json_p3.rbs +24 -1313
  56. data.tar.gz.sig +0 -0
  57. metadata +66 -46
  58. metadata.gz.sig +0 -0
  59. data/lib/json_p3/environment.rb +0 -111
  60. data/lib/json_p3/filter.rb +0 -459
  61. data/lib/json_p3/function.rb +0 -10
  62. data/lib/json_p3/function_extensions/count.rb +0 -15
  63. data/lib/json_p3/function_extensions/length.rb +0 -17
  64. data/lib/json_p3/function_extensions/match.rb +0 -62
  65. data/lib/json_p3/function_extensions/pattern.rb +0 -39
  66. data/lib/json_p3/function_extensions/search.rb +0 -44
  67. data/lib/json_p3/function_extensions/value.rb +0 -15
  68. data/lib/json_p3/lexer.rb +0 -419
  69. data/lib/json_p3/node.rb +0 -44
  70. data/lib/json_p3/parser.rb +0 -553
  71. data/lib/json_p3/path.rb +0 -72
  72. data/lib/json_p3/segment.rb +0 -158
  73. data/lib/json_p3/selector.rb +0 -306
  74. data/lib/json_p3/serialize.rb +0 -13
  75. data/lib/json_p3/token.rb +0 -36
  76. data/lib/json_p3/unescape.rb +0 -112
data/lib/json_p3/patch.rb CHANGED
@@ -1,329 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "English"
4
- require_relative "errors"
5
4
 
6
5
  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 }
6
+ # A JSON Patch containing zero or more patch operations.
7
+ class Patch
8
+ def self.apply!(ops, value)
9
+ new(ops).apply!(value)
322
10
  end
323
- end
324
11
 
325
- # A JSON Patch containing zero or more patch operations.
326
- class JSONPatch
327
12
  # @param ops [Array<Op | Hash<String, untyped>>?]
328
13
  def initialize(ops = nil)
329
14
  @ops = []
@@ -384,8 +69,8 @@ module JSONP3
384
69
  end
385
70
 
386
71
  # Apply this patch to JSON-like value _value_.
387
- def apply(value)
388
- @ops.each_with_index { |op, i| value = op.apply(value, i) }
72
+ def apply!(value)
73
+ @ops.each_with_index { |op, i| value = op.apply!(value, i) }
389
74
  value
390
75
  end
391
76
 
@@ -418,22 +103,22 @@ module JSONP3
418
103
  when "test"
419
104
  test(op_pointer(obj, "path", "test", i), op_value(obj, "value", "test", i))
420
105
  else
421
- raise JSONPatchError,
106
+ raise Patch::Error,
422
107
  "expected 'op' to be one of 'add', 'remove', 'replace', 'move', 'copy' or 'test' (#{obj["op"]}:#{i})"
423
108
  end
424
109
  end
425
110
  end
426
111
 
427
112
  def op_pointer(obj, key, op, index)
428
- raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
113
+ raise Patch::Error, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
429
114
 
430
- JSONP3::JSONPointer.new(obj[key])
431
- rescue JSONPointerError
432
- raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
115
+ JSONP3::Pointer.new(obj[key])
116
+ rescue Pointer::Error
117
+ raise Patch::Error, "#{$ERROR_INFO} (#{op}:#{index})"
433
118
  end
434
119
 
435
120
  def op_value(obj, key, op, index)
436
- raise JSONPatchError, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
121
+ raise Patch::Error, "missing property '#{key}' (#{op}:#{index})" unless obj.key?(key)
437
122
 
438
123
  obj[key]
439
124
  end
@@ -441,9 +126,9 @@ module JSONP3
441
126
  def ensure_pointer(pointer, op, index)
442
127
  return pointer unless pointer.is_a?(String)
443
128
 
444
- JSONP3::JSONPointer.new(pointer)
445
- rescue JSONPointerError
446
- raise JSONPatchError, "#{$ERROR_INFO} (#{op}:#{index})"
129
+ JSONP3::Pointer.new(pointer)
130
+ rescue Pointer::Error
131
+ raise Patch::Error, "#{$ERROR_INFO} (#{op}:#{index})"
447
132
  end
448
133
  end
449
134
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lexer"
4
+ require_relative "parser"
5
+ require_relative "query"
6
+ require_relative "function"
7
+ require_relative "function_extensions/length"
8
+ require_relative "function_extensions/value"
9
+ require_relative "function_extensions/count"
10
+ require_relative "function_extensions/match"
11
+ require_relative "function_extensions/search"
12
+
13
+ module JSONP3
14
+ module Path
15
+ # JSONPath configuration
16
+ #
17
+ # Configure an environment by inheriting from `Environment` and setting one
18
+ # or more constants and/or overriding {setup_function_extensions}.
19
+ class Environment
20
+ # The maximum integer allowed when selecting array items by index.
21
+ MAX_INT_INDEX = (2**53) - 1
22
+
23
+ # The minimum integer allowed when selecting array items by index.
24
+ MIN_INT_INDEX = -(2**53) + 1
25
+
26
+ # The maximum number of arrays and hashes the descendent segment will traverse
27
+ # before raising a {JSONPathRecursionError}.
28
+ MAX_RECURSION_DEPTH = 100
29
+
30
+ # One of the available implementations of the _name selector_.
31
+ #
32
+ # - {NameSelector} (the default) will select values from hashes using string keys.
33
+ # - {SymbolNameSelector} will select values from hashes using string or symbol keys.
34
+ #
35
+ # Implement your own name selector by inheriting from {NameSelector} and overriding
36
+ # `#resolve`.
37
+ NAME_SELECTOR = NameSelector
38
+
39
+ # An implementation of the _index selector_. The default implementation will
40
+ # select values from arrays only. Implement your own by inheriting from
41
+ # {IndexSelector} and overriding `#resolve`.
42
+ INDEX_SELECTOR = IndexSelector
43
+
44
+ attr_accessor :function_extensions
45
+
46
+ def initialize
47
+ @function_extensions = {}
48
+ setup_function_extensions
49
+ end
50
+
51
+ # Prepare JSONPath expression _query_ for repeated application.
52
+ # @param query [String]
53
+ # @return [Query]
54
+ def compile(query)
55
+ tokens = JSONP3::Path.tokenize(query)
56
+ Query.new(self, Parser.new(self, query, tokens).parse)
57
+ end
58
+
59
+ # Apply JSONPath expression _query_ to _value_.
60
+ # @param query [String] the JSONPath expression
61
+ # @param value [JSON-like data] the target JSON "document"
62
+ # @return [Array<JSONPath>]
63
+ def find(query, value)
64
+ compile(query).find(value)
65
+ end
66
+
67
+ # Apply JSONPath expression _query_ to _value_.
68
+ # @param query [String] the JSONPath expression
69
+ # @param value [JSON-like data] the target JSON "document"
70
+ # @return [Enumerable<JSONPath>]
71
+ def find_enum(query, value)
72
+ compile(query).find_enum(value)
73
+ end
74
+
75
+ # Apply JSONPath expression _query_ to _value_ an return the first
76
+ # available node.
77
+ # @param query [String] the JSONPath expression
78
+ # @param value [JSON-like data] the target JSON "document"
79
+ # @return [JSONPathNode | nil]
80
+ def match(path, value)
81
+ find_enum(path, value).first
82
+ end
83
+
84
+ # Apply JSONPath expression _query_ to _value_ an return `true` if there's at
85
+ # least one node, or nil if there were no matches.
86
+ # @param query [String] the JSONPath expression
87
+ # @param value [JSON-like data] the target JSON "document"
88
+ # @return [bool]
89
+ def match?(path, value)
90
+ !find_enum(path, value).first.nil?
91
+ end
92
+
93
+ # Apply JSONPath expression _query_ to _value_ an return the first
94
+ # available node, or nil if there were no matches.
95
+ # @param query [String] the JSONPath expression
96
+ # @param value [JSON-like data] the target JSON "document"
97
+ # @return [JSONPathNode | nil]
98
+ def first(path, value)
99
+ find_enum(path, value).first
100
+ end
101
+
102
+ # Override this function to configure JSONPath function extensions.
103
+ # By default, only the standard functions described in RFC 9535 are enabled.
104
+ def setup_function_extensions
105
+ @function_extensions["length"] = Length.new
106
+ @function_extensions["count"] = Count.new
107
+ @function_extensions["value"] = Value.new
108
+ @function_extensions["match"] = Match.new
109
+ @function_extensions["search"] = Search.new
110
+ end
111
+ end
112
+ end
113
+ end