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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +9 -0
- data/README.md +37 -4
- data/lib/json_p3/environment.rb +35 -0
- data/lib/json_p3/filter.rb +1 -1
- data/lib/json_p3/function_extensions/match.rb +1 -1
- data/lib/json_p3/lexer.rb +34 -13
- data/lib/json_p3/parser.rb +2 -2
- data/lib/json_p3/path.rb +31 -1
- data/lib/json_p3/segment.rb +56 -0
- data/lib/json_p3/selector.rb +55 -34
- data/lib/json_p3/version.rb +1 -1
- data/lib/json_p3.rb +16 -0
- data/performance/benchmark_ips.rb +4 -0
- data/performance/benchmark_small_citylots.rb +8 -0
- data/sig/json_p3.rbs +59 -11
- data.tar.gz.sig +0 -0
- metadata +2 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c045dc201d7af17b4292488327d4bd01fbba11454df29c9ec78e344a1c6f3ea3
|
4
|
+
data.tar.gz: c10613058ceafbf844d39a0114be75255ae453aa0f78d0d422fac864d749f364
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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:
|
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.
|
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 hasn
|
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
|
```
|
data/lib/json_p3/environment.rb
CHANGED
@@ -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
|
data/lib/json_p3/filter.rb
CHANGED
@@ -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
|
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
|
-
@
|
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 @
|
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
|
-
@
|
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 @
|
280
|
-
if @
|
281
|
-
@
|
299
|
+
if @func_call_stack.length.positive?
|
300
|
+
if @func_call_stack[-1] == 1
|
301
|
+
@func_call_stack.pop
|
282
302
|
else
|
283
|
-
@
|
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
|
-
@
|
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
|
data/lib/json_p3/parser.rb
CHANGED
@@ -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)
|
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)
|
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|
|
data/lib/json_p3/segment.rb
CHANGED
@@ -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
|
data/lib/json_p3/selector.rb
CHANGED
@@ -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
|
-
|
182
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/json_p3/version.rb
CHANGED
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
|
@@ -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) ->
|
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.
|
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-
|
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
|