json_p3 0.3.2 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: afd5de1be45132d6071616dc7aa21425dc3d9beaed525315f017b343b434a42f
4
- data.tar.gz: 19f0fc51ac0f8680e47808beda7f206ea9d911b33ebebaccb5803cd7bcd012c0
3
+ metadata.gz: c045dc201d7af17b4292488327d4bd01fbba11454df29c9ec78e344a1c6f3ea3
4
+ data.tar.gz: c10613058ceafbf844d39a0114be75255ae453aa0f78d0d422fac864d749f364
5
5
  SHA512:
6
- metadata.gz: 9c6458e2fc86fa09988edbc032ad434cca4125f1d6a368e2f8e37e79e65b80fb404d12a3ec9dfb42ef007dbfc2977a4492814c9a8d689900921e2cfa885b6c11
7
- data.tar.gz: 54580931a9a37165dcbb10f8a240c8142a8de013cbc9e76776f59a5f60b43743f98db65daae7d85968d61a6eda1983df26f9ae8d8aaf1571b3a45106236a8ed1
6
+ metadata.gz: c04571664ae937194618544cb6aac078d91519fa6cd750e355f20d4c7a9ed9056880a9ba91fbc856475baec508ee2e779939ef6ccfeff9d9963a32f9ccf52ad2
7
+ data.tar.gz: 6232640619d2d65acebd920a1a3d19b77bc194b4b639b46cea9e44abb23d852f09e5eb85c004e5dc6e1aec3dd21ad2c36dabe7a59677cedeb8b776a256d375bf
checksums.yaml.gz.sig CHANGED
Binary file
data/.rubocop.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  require:
2
2
  - rubocop-minitest
3
3
  - rubocop-rake
4
+
5
+ plugins:
4
6
  - rubocop-performance
5
7
 
6
8
  AllCops:
@@ -20,7 +22,7 @@ Metrics/ClassLength:
20
22
  Max: 500
21
23
 
22
24
  Metrics/CyclomaticComplexity:
23
- Max: 15
25
+ Max: 20
24
26
 
25
27
  Metrics/MethodLength:
26
28
  Max: 50
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.4.1] - 2025-03-18
2
+
3
+ - Fixed `JSONPathSyntaxError` claiming "unbalanced parentheses" when the query has balanced brackets.
4
+
5
+ ## [0.4.0] - 2025-02-10
6
+
7
+ - Added `JSONP3.find_enum`, `JSONP3::JSONPathEnvironment.find_enum` and `JSONP3::JSONPath.find_enum`. `find_enum` is like `find`, but returns an Enumerable (usually an Enumerator) of `JSONPathNode` instances instead of a `JSONPathNodeList`. `find_enum` can be more efficient for some combinations of query and data, especially for large data and recursive queries.
8
+ - Added `JSONP3.match`, `JSONP3.match?`, `JSONP3.first` and equivalent methods for `JSONPathEnvironment` and `JSONPath`.
9
+
1
10
  ## [0.3.2] - 2025-01-29
2
11
 
3
12
  - Fix normalized string representations of node locations as returned by `JSONPathNode.path`.
data/README.md CHANGED
@@ -16,7 +16,7 @@ We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> s
16
16
  <img alt="Gem Version" src="https://img.shields.io/gem/v/json_p3?style=flat-square">
17
17
  </a>
18
18
  <a href="https://github.com/jg-rp/ruby-json-p3">
19
- <img alt="Static Badge" src="https://img.shields.io/badge/Ruby-3.1%20%7C%203.2%20%7C%203.3-CC342D?style=flat-square">
19
+ <img alt="Static Badge" src="https://img.shields.io/badge/Ruby-3.1%20%7C%203.2%20%7C%203.3%20%7C%203.4-CC342D?style=flat-square">
20
20
  </a>
21
21
  </p>
22
22
 
@@ -36,7 +36,7 @@ We follow <a href="https://datatracker.ietf.org/doc/html/rfc9535">RFC 9535</a> s
36
36
  Add `'json_p3'` to your Gemfile:
37
37
 
38
38
  ```
39
- gem 'json_p3', '~> 0.2.1'
39
+ gem 'json_p3', '~> 0.4.0'
40
40
  ```
41
41
 
42
42
  Or
@@ -47,7 +47,7 @@ gem install json_p3
47
47
 
48
48
  ### Checksum
49
49
 
50
- JSON P3 is cryptographically signed. To be sure the gem you install hasnt been tampered with, add my public key (if you havent already) as a trusted certificate:
50
+ JSON P3 is cryptographically signed. To be sure the gem you install hasn't been tampered with, add my public key (if you haven't already) as a trusted certificate:
51
51
 
52
52
  ```
53
53
  gem cert --add <(curl -Ls https://raw.githubusercontent.com/jg-rp/ruby-json-p3/refs/heads/main/certs/jgrp.pem)
@@ -192,6 +192,23 @@ end
192
192
  # {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
193
193
  ```
194
194
 
195
+ ### find_enum
196
+
197
+ `find_enum(query, value) -> Enumerable<JSONPathNode>`
198
+
199
+ `find_enum` is an alternative to `find` which returns an enumerable (usually an enumerator) of `JSONPathNode` instances instead of an array. Depending on the query and the data the query is applied to, `find_enum` can be more efficient than `find`, especially for large data and queries using recursive descent segments.
200
+
201
+ ```ruby
202
+ # ... continued from above
203
+
204
+ JSONP3.find_enum("$.users[?@.score > 85]", data).each do |node|
205
+ puts "#{node.value} at #{node.path}"
206
+ end
207
+
208
+ # {"name"=>"Sue", "score"=>100} at $['users'][0]
209
+ # {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
210
+ ```
211
+
195
212
  ### compile
196
213
 
197
214
  `compile(query) -> JSONPath`
@@ -238,14 +255,30 @@ end
238
255
  # {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
239
256
  ```
240
257
 
258
+ ### match / first
259
+
260
+ `match(query, value) -> JSONPathNode | nil`
261
+
262
+ `match` (alias `first`) returns a node for the first available match when applying _query_ to _value_, or `nil` if there were no matches.
263
+
264
+ ### match?
265
+
266
+ `match?(query, value) -> bool`
267
+
268
+ `match?` returns `true` if there was at least one match from applying _query_ to _value_, or `false` otherwise.
269
+
241
270
  ### JSONPathEnvironment
242
271
 
243
- The `find` and `compile` methods described above are convenience methods equivalent to
272
+ The `find`, `find_enum` and `compile` methods described above are convenience methods equivalent to:
244
273
 
245
274
  ```
246
275
  JSONP3::DEFAULT_ENVIRONMENT.find(query, data)
247
276
  ```
248
277
 
278
+ ```
279
+ JSONP3::DEFAULT_ENVIRONMENT.find_enum(query, data)
280
+ ```
281
+
249
282
  and
250
283
 
251
284
  ```
@@ -63,6 +63,41 @@ module JSONP3
63
63
  compile(query).find(value)
64
64
  end
65
65
 
66
+ # Apply JSONPath expression _query_ to _value_.
67
+ # @param query [String] the JSONPath expression
68
+ # @param value [JSON-like data] the target JSON "document"
69
+ # @return [Enumerable<JSONPath>]
70
+ def find_enum(query, value)
71
+ compile(query).find_enum(value)
72
+ end
73
+
74
+ # Apply JSONPath expression _query_ to _value_ an return the first
75
+ # available node.
76
+ # @param query [String] the JSONPath expression
77
+ # @param value [JSON-like data] the target JSON "document"
78
+ # @return [JSONPathNode | nil]
79
+ def match(path, value)
80
+ find_enum(path, value).first
81
+ end
82
+
83
+ # Apply JSONPath expression _query_ to _value_ an return `true` if there's at
84
+ # least one node, or nil if there were no matches.
85
+ # @param query [String] the JSONPath expression
86
+ # @param value [JSON-like data] the target JSON "document"
87
+ # @return [bool]
88
+ def match?(path, value)
89
+ !find_enum(path, value).first.nil?
90
+ end
91
+
92
+ # Apply JSONPath expression _query_ to _value_ an return the first
93
+ # available node, or nil if there were no matches.
94
+ # @param query [String] the JSONPath expression
95
+ # @param value [JSON-like data] the target JSON "document"
96
+ # @return [JSONPathNode | nil]
97
+ def first(path, value)
98
+ find_enum(path, value).first
99
+ end
100
+
66
101
  # Override this function to configure JSONPath function extensions.
67
102
  # By default, only the standard functions described in RFC 9535 are enabled.
68
103
  def setup_function_extensions
@@ -390,7 +390,7 @@ module JSONP3 # rubocop:disable Style/Documentation
390
390
  # @param args [Array<Object>]
391
391
  # @return [Array<Object>]
392
392
  def unpack_node_lists(func, args)
393
- unpacked_args = []
393
+ unpacked_args = [] # : Array[Object]
394
394
  args.each_with_index do |arg, i|
395
395
  unless arg.is_a?(JSONPathNodeList) && func.class::ARG_TYPES[i] != :nodes_expression
396
396
  unpacked_args << arg
@@ -44,7 +44,7 @@ module JSONP3
44
44
  private
45
45
 
46
46
  def full_match(pattern)
47
- parts = []
47
+ parts = [] # : Array[String]
48
48
  explicit_caret = pattern.start_with?("^")
49
49
  explicit_dollar = pattern.end_with?("$")
50
50
 
data/lib/json_p3/lexer.rb CHANGED
@@ -20,6 +20,12 @@ module JSONP3 # rubocop:disable Style/Documentation
20
20
  tokens.last)
21
21
  end
22
22
 
23
+ unless lexer.bracket_stack.empty?
24
+ ch, index = *lexer.bracket_stack.last
25
+ msg = "unbalanced brackets"
26
+ raise JSONPathSyntaxError.new(msg, Token.new(:token_error, ch, index, query, message: msg))
27
+ end
28
+
23
29
  tokens
24
30
  end
25
31
 
@@ -33,11 +39,12 @@ module JSONP3 # rubocop:disable Style/Documentation
33
39
  S_ESCAPES = Set["b", "f", "n", "r", "t", "u", "/", "\\"].freeze
34
40
 
35
41
  # @dynamic tokens
36
- attr_reader :tokens
42
+ attr_reader :tokens, :bracket_stack
37
43
 
38
44
  def initialize(query)
39
45
  @filter_depth = 0
40
- @paren_stack = []
46
+ @func_call_stack = []
47
+ @bracket_stack = []
41
48
  @tokens = []
42
49
  @start = 0
43
50
  @query = query.freeze
@@ -135,6 +142,7 @@ module JSONP3 # rubocop:disable Style/Documentation
135
142
  emit(:token_double_dot, "..")
136
143
  :lex_descendant_segment
137
144
  when "["
145
+ @bracket_stack << ["[", @start]
138
146
  emit(:token_lbracket, "[")
139
147
  :lex_inside_bracketed_segment
140
148
  else
@@ -157,6 +165,7 @@ module JSONP3 # rubocop:disable Style/Documentation
157
165
  emit(:token_wild, "*")
158
166
  :lex_segment
159
167
  when "["
168
+ @bracket_stack << ["[", @start]
160
169
  emit(:token_lbracket, "[")
161
170
  :lex_inside_bracketed_segment
162
171
  else
@@ -208,6 +217,13 @@ module JSONP3 # rubocop:disable Style/Documentation
208
217
 
209
218
  case c
210
219
  when "]"
220
+ if @bracket_stack.empty? || @bracket_stack.last.first != "["
221
+ backup
222
+ error "unbalanced brackets"
223
+ return nil
224
+ end
225
+
226
+ @bracket_stack.pop
211
227
  emit(:token_rbracket, "]")
212
228
  return :lex_segment
213
229
  when ""
@@ -251,17 +267,13 @@ module JSONP3 # rubocop:disable Style/Documentation
251
267
  return nil
252
268
  when "]"
253
269
  @filter_depth -= 1
254
- if @paren_stack.length == 1
255
- error "unbalanced parentheses"
256
- return nil
257
- end
258
270
  backup
259
271
  return :lex_inside_bracketed_segment
260
272
  when ","
261
273
  emit(:token_comma, ",")
262
274
  # If we have unbalanced parens, we are inside a function call and a
263
275
  # comma separates arguments. Otherwise a comma separates selectors.
264
- next if @paren_stack.length.positive?
276
+ next if @func_call_stack.length.positive?
265
277
 
266
278
  @filter_depth -= 1
267
279
  return :lex_inside_bracketed_segment
@@ -270,17 +282,25 @@ module JSONP3 # rubocop:disable Style/Documentation
270
282
  when '"'
271
283
  return :lex_double_quoted_string_inside_filter_expression
272
284
  when "("
285
+ @bracket_stack << ["(", @start]
273
286
  emit(:token_lparen, "(")
274
287
  # Are we in a function call? If so, a function argument contains parens.
275
- @paren_stack[-1] += 1 if @paren_stack.length.positive?
288
+ @func_call_stack[-1] += 1 if @func_call_stack.length.positive?
276
289
  when ")"
290
+ if @bracket_stack.empty? || @bracket_stack.last.first != "("
291
+ backup
292
+ error "unbalanced brackets"
293
+ return nil
294
+ end
295
+
296
+ @bracket_stack.pop
277
297
  emit(:token_rparen, ")")
278
298
  # Are we closing a function call or a parenthesized expression?
279
- if @paren_stack.length.positive?
280
- if @paren_stack[-1] == 1
281
- @paren_stack.pop
299
+ if @func_call_stack.length.positive?
300
+ if @func_call_stack[-1] == 1
301
+ @func_call_stack.pop
282
302
  else
283
- @paren_stack[-1] -= 1
303
+ @func_call_stack[-1] -= 1
284
304
  end
285
305
  end
286
306
  when "$"
@@ -359,8 +379,9 @@ module JSONP3 # rubocop:disable Style/Documentation
359
379
  end
360
380
  # Function name
361
381
  # Keep track of parentheses for this function call.
362
- @paren_stack << 1
382
+ @func_call_stack << 1
363
383
  emit :token_function
384
+ @bracket_stack << ["(", @start]
364
385
  self.next
365
386
  ignore # move past LPAREN
366
387
  else
@@ -245,7 +245,7 @@ module JSONP3
245
245
  FilterSelector.new(@env, token, FilterExpression.new(token, expression))
246
246
  end
247
247
 
248
- def parse_filter_expression(stream, precedence = Precedence::LOWEST) # rubocop:disable Metrics/CyclomaticComplexity
248
+ def parse_filter_expression(stream, precedence = Precedence::LOWEST)
249
249
  left = case stream.peek.type
250
250
  when :token_double_quote_string, :token_single_quote_string
251
251
  token = stream.next
@@ -477,7 +477,7 @@ module JSONP3
477
477
  expression.token)
478
478
  end
479
479
 
480
- def validate_function_extension_signature(token, args) # rubocop:disable Metrics/CyclomaticComplexity
480
+ def validate_function_extension_signature(token, args)
481
481
  func = @env.function_extensions.fetch(token.value)
482
482
  count = func.class::ARG_TYPES.length
483
483
 
data/lib/json_p3/path.rb CHANGED
@@ -20,11 +20,41 @@ module JSONP3
20
20
  def find(root)
21
21
  nodes = [JSONPathNode.new(root, [], root)]
22
22
  @segments.each { |segment| nodes = segment.resolve(nodes) }
23
- JSONPathNodeList.new(nodes)
23
+ JSONPathNodeList.new(nodes) # TODO: use JSONPathNodeList internally?
24
24
  end
25
25
 
26
26
  alias apply find
27
27
 
28
+ # Apply this JSONPath expression to JSON-like value _root_.
29
+ # @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
30
+ # @return [Enumerable<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
31
+ def find_enum(root)
32
+ nodes = [JSONPathNode.new(root, [], root)] # : Enumerable[JSONPathNode]
33
+ @segments.each { |segment| nodes = segment.resolve_enum(nodes) }
34
+ nodes
35
+ end
36
+
37
+ # Return the first node from applying this JSONPath expression to JSON-like value _root_.
38
+ # @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
39
+ # @return [JSONPathNode | nil] the first available node or nil if there were no matches.
40
+ def match(root)
41
+ find_enum(root).first
42
+ end
43
+
44
+ # Return `true` if this query results in at least one node, or `false` otherwise.
45
+ # @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
46
+ # @return [bool] `true` if this query results in at least one node, or `false` otherwise.
47
+ def match?(root)
48
+ !find_enum(root).first.nil?
49
+ end
50
+
51
+ # Return the first node from applying this JSONPath expression to JSON-like value _root_.
52
+ # @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
53
+ # @return [JSONPathNode | nil] the first available node or nil if there were no matches.
54
+ def first(root)
55
+ find_enum(root).first
56
+ end
57
+
28
58
  # Return _true_ if this JSONPath expression is a singular query.
29
59
  def singular?
30
60
  @segments.each do |segment|
@@ -13,9 +13,16 @@ module JSONP3
13
13
  end
14
14
 
15
15
  # Select the children of each node in _nodes_.
16
+ # @return [Array<JSONPathNode>]
16
17
  def resolve(_nodes)
17
18
  raise "segments must implement resolve(nodes)"
18
19
  end
20
+
21
+ # Select the children of each node in _nodes_.
22
+ # @return [Enumerable<JSONPathNode>]
23
+ def resolve_enum(_nodes)
24
+ raise "segments must implement resolve_enum(nodes)"
25
+ end
19
26
  end
20
27
 
21
28
  # The child selection segment.
@@ -30,6 +37,18 @@ module JSONP3
30
37
  rv
31
38
  end
32
39
 
40
+ def resolve_enum(nodes)
41
+ Enumerator.new do |yielder|
42
+ nodes.each do |node|
43
+ @selectors.each do |selector|
44
+ selector.resolve(node).each do |item|
45
+ yielder << item
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
33
52
  def to_s
34
53
  "[#{@selectors.map(&:to_s).join(", ")}]"
35
54
  end
@@ -61,6 +80,20 @@ module JSONP3
61
80
  rv
62
81
  end
63
82
 
83
+ def resolve_enum(nodes)
84
+ Enumerator.new do |yielder|
85
+ nodes.each do |node|
86
+ visit_enum(node).each do |descendant|
87
+ @selectors.each do |selector|
88
+ selector.resolve(descendant).each do |item|
89
+ yielder << item
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
64
97
  def to_s
65
98
  "..[#{@selectors.map(&:to_s).join(", ")}]"
66
99
  end
@@ -98,5 +131,28 @@ module JSONP3
98
131
 
99
132
  rv
100
133
  end
134
+
135
+ def visit_enum(node, depth = 1)
136
+ raise JSONPathRecursionError.new("recursion limit exceeded", @token) if depth > @env.class::MAX_RECURSION_DEPTH
137
+
138
+ Enumerator.new do |yielder|
139
+ yielder << node
140
+ if node.value.is_a? Array
141
+ node.value.each_with_index do |value, i|
142
+ child = JSONPathNode.new(value, [node.location, i], node.root)
143
+ visit_enum(child, depth + 1).each do |item|
144
+ yielder << item
145
+ end
146
+ end
147
+ elsif node.value.is_a? Hash
148
+ node.value.each do |key, value|
149
+ child = JSONPathNode.new(value, [node.location, key], node.root)
150
+ visit_enum(child, depth + 1).each do |item|
151
+ yielder << item
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
101
157
  end
102
158
  end
@@ -17,6 +17,12 @@ module JSONP3
17
17
  raise "selectors must implement resolve(node)"
18
18
  end
19
19
 
20
+ # Apply this selector to _node_.
21
+ # @return [Enumerable<JSONPathNode>]
22
+ def resolve_enum(node)
23
+ resolve(node)
24
+ end
25
+
20
26
  # Return true if this selector is a singular selector.
21
27
  def singular?
22
28
  false
@@ -145,6 +151,24 @@ module JSONP3
145
151
  end
146
152
  end
147
153
 
154
+ def resolve_enum(node)
155
+ if node.value.is_a? Hash
156
+ Enumerator.new do |yielder|
157
+ node.value.each do |k, v|
158
+ yielder << node.new_child(v, k)
159
+ end
160
+ end
161
+ elsif node.value.is_a? Array
162
+ Enumerator.new do |yielder|
163
+ node.value.each.with_index do |e, i|
164
+ yielder << node.new_child(e, i)
165
+ end
166
+ end
167
+ else
168
+ []
169
+ end
170
+ end
171
+
148
172
  def to_s
149
173
  "*"
150
174
  end
@@ -178,24 +202,23 @@ module JSONP3
178
202
  length = node.value.length
179
203
  return [] if length.zero? || @step.zero?
180
204
 
181
- norm_start = normalized_start(length)
182
- norm_stop = normalized_stop(length)
205
+ normalized_start = if @start.nil?
206
+ @step.negative? ? length - 1 : 0
207
+ elsif @start&.negative?
208
+ [length + (@start || raise), 0].max
209
+ else
210
+ [@start || raise, length - 1].min
211
+ end
183
212
 
184
- nodes = []
213
+ normalized_stop = if @stop.nil?
214
+ @step.negative? ? -1 : length
215
+ elsif @stop&.negative?
216
+ [length + (@stop || raise), -1].max
217
+ else
218
+ [@stop || raise, length].min
219
+ end
185
220
 
186
- if @step.positive?
187
-
188
- for i in (norm_start...norm_stop).step(@step) # rubocop:disable Style/For
189
- nodes << node.new_child(node.value[i], i)
190
- end
191
- else
192
- i = norm_start
193
- while i > norm_stop
194
- nodes << node.new_child(node.value[i], i)
195
- i += @step
196
- end
197
- end
198
- nodes
221
+ (normalized_start...normalized_stop).step(@step).map { |i| node.new_child(node.value[i], i) }
199
222
  end
200
223
 
201
224
  def to_s
@@ -218,24 +241,6 @@ module JSONP3
218
241
  def hash
219
242
  [@start, @stop, @step, @token].hash
220
243
  end
221
-
222
- private
223
-
224
- def normalized_start(length)
225
- # NOTE: trying to please the type checker :(
226
- return @step.negative? ? length - 1 : 0 if @start.nil?
227
- return [length + (@start || raise), 0].max if @start&.negative?
228
-
229
- [@start || raise, length - 1].min
230
- end
231
-
232
- def normalized_stop(length)
233
- # NOTE: trying to please the type checker :(
234
- return @step.negative? ? -1 : length if @stop.nil?
235
- return [length + (@stop || raise), -1].max if @stop&.negative?
236
-
237
- [@stop || raise, length].min
238
- end
239
244
  end
240
245
 
241
246
  # Select array elements or hash values according to a filter expression.
@@ -266,6 +271,22 @@ module JSONP3
266
271
  nodes
267
272
  end
268
273
 
274
+ def resolve_enum(node)
275
+ Enumerator.new do |yielder|
276
+ if node.value.is_a?(Array)
277
+ node.value.each_with_index do |e, i|
278
+ context = FilterContext.new(@env, e, node.root)
279
+ yielder << node.new_child(e, i) if @expression.evaluate(context)
280
+ end
281
+ elsif node.value.is_a?(Hash)
282
+ node.value.each_pair do |k, v|
283
+ context = FilterContext.new(@env, v, node.root)
284
+ yielder << node.new_child(v, k) if @expression.evaluate(context)
285
+ end
286
+ end
287
+ end
288
+ end
289
+
269
290
  def to_s
270
291
  "?#{@expression}"
271
292
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONP3
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/json_p3.rb CHANGED
@@ -13,10 +13,26 @@ module JSONP3
13
13
  DefaultEnvironment.find(path, data)
14
14
  end
15
15
 
16
+ def self.find_enum(path, data)
17
+ DefaultEnvironment.find_enum(path, data)
18
+ end
19
+
16
20
  def self.compile(path)
17
21
  DefaultEnvironment.compile(path)
18
22
  end
19
23
 
24
+ def self.match(path, data)
25
+ DefaultEnvironment.match(path, data)
26
+ end
27
+
28
+ def self.match?(path, data)
29
+ DefaultEnvironment.match?(path, data)
30
+ end
31
+
32
+ def self.first(path, data)
33
+ DefaultEnvironment.first(path, data)
34
+ end
35
+
20
36
  def self.resolve(pointer, value, default: JSONPointer::UNDEFINED)
21
37
  JSONPointer.new(pointer).resolve(value, default: default)
22
38
  end
@@ -26,4 +26,8 @@ Benchmark.ips do |x|
26
26
  x.report("just find:") do
27
27
  COMPILED_QUERIES.map { |p, d| p.find(d) }
28
28
  end
29
+
30
+ x.report("just find (enum):") do
31
+ COMPILED_QUERIES.map { |p, d| p.find_enum(d).to_a }
32
+ end
29
33
  end
@@ -15,4 +15,12 @@ Benchmark.bm(15) do |x|
15
15
  x.report("shallow:") do
16
16
  JSONP3.find("$.features..properties", DATA)
17
17
  end
18
+
19
+ x.report("enum deep:") do
20
+ JSONP3.find_enum("$.features..properties.BLOCK_NUM", DATA).to_a
21
+ end
22
+
23
+ x.report("enum shallow:") do
24
+ JSONP3.find_enum("$.features..properties", DATA).to_a
25
+ end
18
26
  end
data/sig/json_p3.rbs CHANGED
@@ -3,9 +3,17 @@ module JSONP3
3
3
  DefaultEnvironment: JSONPathEnvironment
4
4
 
5
5
  def self.find: (String path, untyped data) -> JSONPathNodeList
6
+
7
+ def self.find_enum: (String path, untyped data) -> Enumerable[JSONPathNode]
6
8
 
7
9
  def self.compile: (String path) -> JSONPath
8
10
 
11
+ def self.match: (String path, untyped data) -> (JSONPathNode | nil)
12
+
13
+ def self.first: (String path, untyped data) -> (JSONPathNode | nil)
14
+
15
+ def self.match?: (String path, untyped data) -> bool
16
+
9
17
  def self.resolve: (String pointer, untyped value, ?default: untyped) -> untyped
10
18
 
11
19
  def self.apply: (Array[Op | Hash[String, untyped]] ops, untyped value) -> untyped
@@ -65,6 +73,14 @@ module JSONP3
65
73
  # @param value [JSON-like data]
66
74
  # @return [Array<JSONPath>]
67
75
  def find: (String query, untyped value) -> JSONPathNodeList
76
+
77
+ def find_enum: (String query, untyped value) -> Enumerable[JSONPathNode]
78
+
79
+ def match: (String query, untyped value) -> (JSONPathNode | nil)
80
+
81
+ def match?: (String query, untyped value) -> bool
82
+
83
+ def first: (String query, untyped value) -> (JSONPathNode | nil)
68
84
 
69
85
  def setup_function_extensions: () -> void
70
86
  end
@@ -150,6 +166,16 @@ module JSONP3
150
166
 
151
167
  private
152
168
 
169
+ class Precedence
170
+ LOWEST: 1
171
+
172
+ LOGICAL_OR: 3
173
+
174
+ LOGICAL_AND: 4
175
+
176
+ PREFIX: 7
177
+ end
178
+
153
179
  def to_canonical_string: (Expression expression, Integer parent_precedence) -> String
154
180
  end
155
181
 
@@ -406,6 +432,8 @@ module JSONP3
406
432
 
407
433
  @paren_stack: Array[Integer]
408
434
 
435
+ @bracket_stack: Array[Array[untyped]]
436
+
409
437
  @tokens: Array[Token]
410
438
 
411
439
  @start: Integer
@@ -425,6 +453,9 @@ module JSONP3
425
453
  # @dynamic tokens
426
454
  attr_reader tokens: Array[Token]
427
455
 
456
+ # @dynamic tokens
457
+ attr_reader bracket_stack: Array[Array[untyped]]
458
+
428
459
  def initialize: (String query) -> void
429
460
 
430
461
  def run: () -> void
@@ -626,7 +657,15 @@ module JSONP3
626
657
  # Apply this JSONPath expression to JSON-like value _root_.
627
658
  # @param root [Array, Hash, String, Integer] the root JSON-like value to apply this query to.
628
659
  # @return [Array<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
629
- def find: (untyped root) -> untyped
660
+ def find: (untyped root) -> JSONPathNodeList
661
+
662
+ def find_enum: (untyped root) -> Enumerable[JSONPathNode]
663
+
664
+ def match: (untyped root) -> (JSONPathNode | nil)
665
+
666
+ def match?: (untyped root) -> bool
667
+
668
+ def first: (untyped root) -> (JSONPathNode | nil)
630
669
 
631
670
  alias apply find
632
671
 
@@ -657,11 +696,16 @@ module JSONP3
657
696
 
658
697
  # Select the children of each node in _nodes_.
659
698
  def resolve: (Array[JSONPathNode] _nodes) -> Array[JSONPathNode]
699
+
700
+ # Select the children of each node in _nodes_.
701
+ def resolve_enum: (Enumerable[JSONPathNode] _nodes) -> Enumerable[JSONPathNode]
660
702
  end
661
703
 
662
704
  # The child selection segment.
663
705
  class ChildSegment < Segment
664
706
  def resolve: (Array[JSONPathNode] nodes) -> Array[JSONPathNode]
707
+
708
+ def resolve_enum: (Enumerable[JSONPathNode] nodes) -> Enumerable[JSONPathNode]
665
709
 
666
710
  def to_s: () -> ::String
667
711
 
@@ -675,6 +719,8 @@ module JSONP3
675
719
  # The recursive descent segment
676
720
  class RecursiveDescentSegment < Segment
677
721
  def resolve: (Array[JSONPathNode] nodes) -> Array[JSONPathNode]
722
+
723
+ def resolve_enum: (Enumerable[JSONPathNode] nodes) -> Enumerable[JSONPathNode]
678
724
 
679
725
  def to_s: () -> ::String
680
726
 
@@ -685,6 +731,7 @@ module JSONP3
685
731
  def hash: () -> Integer
686
732
 
687
733
  def visit: (JSONPathNode node, ?::Integer depth) -> Array[JSONPathNode]
734
+ def visit_enum: (JSONPathNode node, ?::Integer depth) -> Enumerable[JSONPathNode]
688
735
  end
689
736
  end
690
737
 
@@ -703,6 +750,7 @@ module JSONP3
703
750
  # Apply this selector to _node_.
704
751
  # @return [Array<JSONPathNode>]
705
752
  def resolve: (JSONPathNode _node) -> Array[JSONPathNode]
753
+ def resolve_enum: (JSONPathNode _node) -> Enumerable[JSONPathNode]
706
754
 
707
755
  # Return true if this selector is a singular selector.
708
756
  def singular?: () -> false
@@ -718,6 +766,7 @@ module JSONP3
718
766
  def initialize: (JSONPathEnvironment env, Token token, String name) -> void
719
767
 
720
768
  def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
769
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
721
770
 
722
771
  def singular?: () -> true
723
772
 
@@ -741,6 +790,7 @@ module JSONP3
741
790
  def initialize: (JSONPathEnvironment env, Token token, String name) -> void
742
791
 
743
792
  def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
793
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
744
794
 
745
795
  def singular?: () -> true
746
796
 
@@ -762,7 +812,8 @@ module JSONP3
762
812
 
763
813
  def initialize: (JSONPathEnvironment env, Token token, Integer index) -> void
764
814
 
765
- def resolve: (JSONPathNode node) -> Array[JSONPathNode]
815
+ def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
816
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
766
817
 
767
818
  def singular?: () -> true
768
819
 
@@ -781,7 +832,8 @@ module JSONP3
781
832
 
782
833
  # The wildcard selector selects all elements from an array or values from a hash.
783
834
  class WildcardSelector < Selector
784
- def resolve: (JSONPathNode node) -> Array[JSONPathNode]
835
+ def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
836
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
785
837
 
786
838
  def to_s: () -> "*"
787
839
 
@@ -811,7 +863,8 @@ module JSONP3
811
863
 
812
864
  def initialize: (JSONPathEnvironment env, Token token, (Integer | nil) start, (Integer | nil) stop, (Integer | nil) step) -> void
813
865
 
814
- def resolve: (JSONPathNode node) -> Array[JSONPathNode]
866
+ def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
867
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
815
868
 
816
869
  def to_s: () -> ::String
817
870
 
@@ -820,12 +873,6 @@ module JSONP3
820
873
  alias eql? ==
821
874
 
822
875
  def hash: () -> Integer
823
-
824
- private
825
-
826
- def normalized_start: (Integer length) -> Integer
827
-
828
- def normalized_stop: (Integer length) -> Integer
829
876
  end
830
877
 
831
878
  # Select array elements or hash values according to a filter expression.
@@ -837,7 +884,8 @@ module JSONP3
837
884
 
838
885
  def initialize: (JSONPathEnvironment env, Token token, Expression expression) -> void
839
886
 
840
- def resolve: (JSONPathNode node) -> Array[JSONPathNode]
887
+ def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
888
+ def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
841
889
 
842
890
  def to_s: () -> ::String
843
891
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_p3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Prior
@@ -36,7 +36,7 @@ cert_chain:
36
36
  6dM18fnfBc3yA4KI7AO8UAmRkTscMYV6f/K4YZR6ZYCNWRpY7rkg+arhf05aoSQf
37
37
  vn9bO1bzwdnG
38
38
  -----END CERTIFICATE-----
39
- date: 2025-01-29 00:00:00.000000000 Z
39
+ date: 2025-03-18 00:00:00.000000000 Z
40
40
  dependencies: []
41
41
  description: JSONPath following RFC 9535
42
42
  email:
metadata.gz.sig CHANGED
Binary file