json_p3 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +11 -0
- data/README.md +36 -3
- data/lib/json_p3/environment.rb +35 -0
- data/lib/json_p3/filter.rb +37 -4
- data/lib/json_p3/function_extensions/match.rb +1 -1
- data/lib/json_p3/lexer.rb +1 -1
- data/lib/json_p3/node.rb +3 -1
- 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 +56 -35
- data/lib/json_p3/serialize.rb +13 -0
- 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 +67 -18
- data.tar.gz.sig +0 -0
- metadata +5 -3
- 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: db1ec71125b99bc877ba9856434a65e3f8e1c11a48b9566443c3b1a8b6860169
|
4
|
+
data.tar.gz: ad176fdda64b4f741e9f86ab48d4c8e3ccd767187fc0fc0fe28a4e6967a071d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3b153a665f534891e1ed1976ca6c180cd1c2427786b9f7b74fcea1953d7337135db669b3541d5eaba7fd2829dbd6c94e6c36236a4a6c6221d1bd5c0ac45f5a04
|
7
|
+
data.tar.gz: 52303daa1cd27c3f3ed7187949f6b9a2cf2c95483e61cd8cf7ced438f5511ad9067f39496b1fe53a85932a3de2be15ebfa2c791fdbc9002aceb0c85395c04839
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/.rubocop.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.6
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## [0.4.0] - 2025-02-10
|
2
|
+
|
3
|
+
- 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.
|
4
|
+
- Added `JSONP3.match`, `JSONP3.match?`, `JSONP3.first` and equivalent methods for `JSONPathEnvironment` and `JSONPath`.
|
5
|
+
|
6
|
+
## [0.3.2] - 2025-01-29
|
7
|
+
|
8
|
+
- Fix normalized string representations of node locations as returned by `JSONPathNode.path`.
|
9
|
+
- Fix canonical string representations of instances of `JSONPath`, as returned by `to_s`.
|
10
|
+
- Fixed filter queries with multiple bracketed segments. Previously we were failing to tokenize queries like `$[?@[0][0]]`. See [#15](https://github.com/jg-rp/ruby-json-p3/issues/15).
|
11
|
+
|
1
12
|
## [0.3.1] - 2024-12-05
|
2
13
|
|
3
14
|
- Fix JSON Patch `move` and `copy` operations when using the special JSON Pointer token `-`.
|
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
|
|
@@ -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
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "json"
|
4
3
|
require_relative "function"
|
4
|
+
require_relative "serialize"
|
5
5
|
|
6
6
|
module JSONP3 # rubocop:disable Style/Documentation
|
7
7
|
# Base class for all filter expression nodes.
|
@@ -33,7 +33,7 @@ module JSONP3 # rubocop:disable Style/Documentation
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def to_s
|
36
|
-
@expression
|
36
|
+
to_canonical_string(@expression, Precedence::LOWEST)
|
37
37
|
end
|
38
38
|
|
39
39
|
def ==(other)
|
@@ -47,6 +47,39 @@ module JSONP3 # rubocop:disable Style/Documentation
|
|
47
47
|
def hash
|
48
48
|
[@expression, @token].hash
|
49
49
|
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
class Precedence
|
54
|
+
LOWEST = 1
|
55
|
+
LOGICAL_OR = 3
|
56
|
+
LOGICAL_AND = 4
|
57
|
+
PREFIX = 7
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_canonical_string(expression, parent_precedence)
|
61
|
+
if expression.instance_of? LogicalAndExpression
|
62
|
+
left = to_canonical_string(expression.left, Precedence::LOGICAL_AND)
|
63
|
+
right = to_canonical_string(expression.right, Precedence::LOGICAL_AND)
|
64
|
+
expr = "#{left} && #{right}"
|
65
|
+
return parent_precedence >= Precedence::LOGICAL_AND ? "(#{expr})" : expr
|
66
|
+
end
|
67
|
+
|
68
|
+
if expression.instance_of? LogicalOrExpression
|
69
|
+
left = to_canonical_string(expression.left, Precedence::LOGICAL_OR)
|
70
|
+
right = to_canonical_string(expression.right, Precedence::LOGICAL_OR)
|
71
|
+
expr = "#{left} || #{right}"
|
72
|
+
return parent_precedence >= Precedence::LOGICAL_OR ? "(#{expr})" : expr
|
73
|
+
end
|
74
|
+
|
75
|
+
if expression.instance_of? LogicalNotExpression
|
76
|
+
operand = to_canonical_string(expression.expression, Precedence::PREFIX)
|
77
|
+
expr = "!#{operand}"
|
78
|
+
return parent_precedence > Precedence::PREFIX ? `(#{expr})` : expr
|
79
|
+
end
|
80
|
+
|
81
|
+
expression.to_s
|
82
|
+
end
|
50
83
|
end
|
51
84
|
|
52
85
|
# Base class for expression literals.
|
@@ -85,7 +118,7 @@ module JSONP3 # rubocop:disable Style/Documentation
|
|
85
118
|
# A double or single quoted string literal.
|
86
119
|
class StringLiteral < FilterExpressionLiteral
|
87
120
|
def to_s
|
88
|
-
|
121
|
+
JSONP3.canonical_string(@value)
|
89
122
|
end
|
90
123
|
end
|
91
124
|
|
@@ -357,7 +390,7 @@ module JSONP3 # rubocop:disable Style/Documentation
|
|
357
390
|
# @param args [Array<Object>]
|
358
391
|
# @return [Array<Object>]
|
359
392
|
def unpack_node_lists(func, args)
|
360
|
-
unpacked_args = []
|
393
|
+
unpacked_args = [] # : Array[Object]
|
361
394
|
args.each_with_index do |arg, i|
|
362
395
|
unless arg.is_a?(JSONPathNodeList) && func.class::ARG_TYPES[i] != :nodes_expression
|
363
396
|
unpacked_args << arg
|
data/lib/json_p3/lexer.rb
CHANGED
@@ -209,7 +209,7 @@ module JSONP3 # rubocop:disable Style/Documentation
|
|
209
209
|
case c
|
210
210
|
when "]"
|
211
211
|
emit(:token_rbracket, "]")
|
212
|
-
return
|
212
|
+
return :lex_segment
|
213
213
|
when ""
|
214
214
|
error "unclosed bracketed selection"
|
215
215
|
return nil
|
data/lib/json_p3/node.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "serialize"
|
4
|
+
|
3
5
|
module JSONP3
|
4
6
|
# A JSON-like value and its location.
|
5
7
|
class JSONPathNode
|
@@ -19,7 +21,7 @@ module JSONP3
|
|
19
21
|
# Return the normalized path to this node.
|
20
22
|
# @return [String] the normalized path.
|
21
23
|
def path
|
22
|
-
segments = @location.flatten.map { |i| i.is_a?(String) ? "[
|
24
|
+
segments = @location.flatten.map { |i| i.is_a?(String) ? "[#{JSONP3.canonical_string(i)}]" : "[#{i}]" }
|
23
25
|
"$#{segments.join}"
|
24
26
|
end
|
25
27
|
|
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
|
@@ -46,7 +52,7 @@ module JSONP3
|
|
46
52
|
end
|
47
53
|
|
48
54
|
def to_s
|
49
|
-
@name
|
55
|
+
JSONP3.canonical_string(@name)
|
50
56
|
end
|
51
57
|
|
52
58
|
def ==(other)
|
@@ -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
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module JSONP3 # rubocop:disable Style/Documentation
|
6
|
+
TRANS = { "\\\"" => "\"", "'" => "\\'" }.freeze
|
7
|
+
|
8
|
+
# Return _value_ formatted as a canonical string literal.
|
9
|
+
# @param value [String]
|
10
|
+
def self.canonical_string(value)
|
11
|
+
"'#{(JSON.dump(value)[1..-2] || raise).gsub(/('|\\")/, TRANS)}'"
|
12
|
+
end
|
13
|
+
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
|
@@ -126,7 +142,7 @@ module JSONP3
|
|
126
142
|
|
127
143
|
def initialize: (Token token) -> void
|
128
144
|
|
129
|
-
# Evaluate the filter
|
145
|
+
# Evaluate the filter expression in the given context.
|
130
146
|
def evaluate: (FilterContext _context) -> untyped
|
131
147
|
end
|
132
148
|
|
@@ -147,6 +163,20 @@ module JSONP3
|
|
147
163
|
alias eql? ==
|
148
164
|
|
149
165
|
def hash: () -> Integer
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
class Precedence
|
170
|
+
LOWEST: 1
|
171
|
+
|
172
|
+
LOGICAL_OR: 3
|
173
|
+
|
174
|
+
LOGICAL_AND: 4
|
175
|
+
|
176
|
+
PREFIX: 7
|
177
|
+
end
|
178
|
+
|
179
|
+
def to_canonical_string: (Expression expression, Integer parent_precedence) -> String
|
150
180
|
end
|
151
181
|
|
152
182
|
# Base class for expression literals.
|
@@ -394,7 +424,7 @@ module JSONP3
|
|
394
424
|
# @return [Array<Token>]
|
395
425
|
def self.tokenize: (String query) -> Array[Token]
|
396
426
|
|
397
|
-
# JSONPath query
|
427
|
+
# JSONPath query expression lexical scanner.
|
398
428
|
#
|
399
429
|
# @see tokenize
|
400
430
|
class Lexer
|
@@ -428,7 +458,7 @@ module JSONP3
|
|
428
458
|
# Generate a new token with the given type.
|
429
459
|
# @param token_type [Symbol] one of the constants defined on the _Token_ class.
|
430
460
|
# @param value [String | nil] a the token's value, if it is known, otherwise the
|
431
|
-
# value will be sliced from @query. This is a performance
|
461
|
+
# value will be sliced from @query. This is a performance optimization.
|
432
462
|
def emit: (token_t token_type, ?untyped value) -> void
|
433
463
|
|
434
464
|
def next: () -> String
|
@@ -488,7 +518,7 @@ module JSONP3
|
|
488
518
|
|
489
519
|
# @param value [JSON-like] the value at this node.
|
490
520
|
# @param location [Array<String | Integer | Array<String | Integer>>] the sequence of
|
491
|
-
# names and/or
|
521
|
+
# names and/or indices leading to _value_ in _root_.
|
492
522
|
# @param root [JSON-like] the root value containing _value_ at _location_.
|
493
523
|
def initialize: (untyped value, Array[location_element] location, untyped root) -> void
|
494
524
|
|
@@ -498,7 +528,7 @@ module JSONP3
|
|
498
528
|
|
499
529
|
# Return a new node that is a child of this node.
|
500
530
|
# @param value the JSON-like value at the new node.
|
501
|
-
# @param key [Integer, String] the array index or hash key
|
531
|
+
# @param key [Integer, String] the array index or hash key associated with _value_.
|
502
532
|
def new_child: (untyped value, String | Integer key) -> JSONPathNode
|
503
533
|
|
504
534
|
def to_s: () -> ::String
|
@@ -622,7 +652,15 @@ module JSONP3
|
|
622
652
|
# Apply this JSONPath expression to JSON-like value _root_.
|
623
653
|
# @param root [Array, Hash, String, Integer] the root JSON-like value to apply this query to.
|
624
654
|
# @return [Array<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
|
625
|
-
def find: (untyped root) ->
|
655
|
+
def find: (untyped root) -> JSONPathNodeList
|
656
|
+
|
657
|
+
def find_enum: (untyped root) -> Enumerable[JSONPathNode]
|
658
|
+
|
659
|
+
def match: (untyped root) -> (JSONPathNode | nil)
|
660
|
+
|
661
|
+
def match?: (untyped root) -> bool
|
662
|
+
|
663
|
+
def first: (untyped root) -> (JSONPathNode | nil)
|
626
664
|
|
627
665
|
alias apply find
|
628
666
|
|
@@ -653,11 +691,16 @@ module JSONP3
|
|
653
691
|
|
654
692
|
# Select the children of each node in _nodes_.
|
655
693
|
def resolve: (Array[JSONPathNode] _nodes) -> Array[JSONPathNode]
|
694
|
+
|
695
|
+
# Select the children of each node in _nodes_.
|
696
|
+
def resolve_enum: (Enumerable[JSONPathNode] _nodes) -> Enumerable[JSONPathNode]
|
656
697
|
end
|
657
698
|
|
658
699
|
# The child selection segment.
|
659
700
|
class ChildSegment < Segment
|
660
701
|
def resolve: (Array[JSONPathNode] nodes) -> Array[JSONPathNode]
|
702
|
+
|
703
|
+
def resolve_enum: (Enumerable[JSONPathNode] nodes) -> Enumerable[JSONPathNode]
|
661
704
|
|
662
705
|
def to_s: () -> ::String
|
663
706
|
|
@@ -671,6 +714,8 @@ module JSONP3
|
|
671
714
|
# The recursive descent segment
|
672
715
|
class RecursiveDescentSegment < Segment
|
673
716
|
def resolve: (Array[JSONPathNode] nodes) -> Array[JSONPathNode]
|
717
|
+
|
718
|
+
def resolve_enum: (Enumerable[JSONPathNode] nodes) -> Enumerable[JSONPathNode]
|
674
719
|
|
675
720
|
def to_s: () -> ::String
|
676
721
|
|
@@ -681,6 +726,7 @@ module JSONP3
|
|
681
726
|
def hash: () -> Integer
|
682
727
|
|
683
728
|
def visit: (JSONPathNode node, ?::Integer depth) -> Array[JSONPathNode]
|
729
|
+
def visit_enum: (JSONPathNode node, ?::Integer depth) -> Enumerable[JSONPathNode]
|
684
730
|
end
|
685
731
|
end
|
686
732
|
|
@@ -699,6 +745,7 @@ module JSONP3
|
|
699
745
|
# Apply this selector to _node_.
|
700
746
|
# @return [Array<JSONPathNode>]
|
701
747
|
def resolve: (JSONPathNode _node) -> Array[JSONPathNode]
|
748
|
+
def resolve_enum: (JSONPathNode _node) -> Enumerable[JSONPathNode]
|
702
749
|
|
703
750
|
# Return true if this selector is a singular selector.
|
704
751
|
def singular?: () -> false
|
@@ -714,6 +761,7 @@ module JSONP3
|
|
714
761
|
def initialize: (JSONPathEnvironment env, Token token, String name) -> void
|
715
762
|
|
716
763
|
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
764
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
717
765
|
|
718
766
|
def singular?: () -> true
|
719
767
|
|
@@ -737,6 +785,7 @@ module JSONP3
|
|
737
785
|
def initialize: (JSONPathEnvironment env, Token token, String name) -> void
|
738
786
|
|
739
787
|
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
788
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
740
789
|
|
741
790
|
def singular?: () -> true
|
742
791
|
|
@@ -758,7 +807,8 @@ module JSONP3
|
|
758
807
|
|
759
808
|
def initialize: (JSONPathEnvironment env, Token token, Integer index) -> void
|
760
809
|
|
761
|
-
def resolve: (JSONPathNode node) -> Array[JSONPathNode]
|
810
|
+
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
811
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
762
812
|
|
763
813
|
def singular?: () -> true
|
764
814
|
|
@@ -777,7 +827,8 @@ module JSONP3
|
|
777
827
|
|
778
828
|
# The wildcard selector selects all elements from an array or values from a hash.
|
779
829
|
class WildcardSelector < Selector
|
780
|
-
def resolve: (JSONPathNode node) -> Array[JSONPathNode]
|
830
|
+
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
831
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
781
832
|
|
782
833
|
def to_s: () -> "*"
|
783
834
|
|
@@ -807,7 +858,8 @@ module JSONP3
|
|
807
858
|
|
808
859
|
def initialize: (JSONPathEnvironment env, Token token, (Integer | nil) start, (Integer | nil) stop, (Integer | nil) step) -> void
|
809
860
|
|
810
|
-
def resolve: (JSONPathNode node) -> Array[JSONPathNode]
|
861
|
+
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
862
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
811
863
|
|
812
864
|
def to_s: () -> ::String
|
813
865
|
|
@@ -816,12 +868,6 @@ module JSONP3
|
|
816
868
|
alias eql? ==
|
817
869
|
|
818
870
|
def hash: () -> Integer
|
819
|
-
|
820
|
-
private
|
821
|
-
|
822
|
-
def normalized_start: (Integer length) -> Integer
|
823
|
-
|
824
|
-
def normalized_stop: (Integer length) -> Integer
|
825
871
|
end
|
826
872
|
|
827
873
|
# Select array elements or hash values according to a filter expression.
|
@@ -833,7 +879,8 @@ module JSONP3
|
|
833
879
|
|
834
880
|
def initialize: (JSONPathEnvironment env, Token token, Expression expression) -> void
|
835
881
|
|
836
|
-
def resolve: (JSONPathNode node) -> Array[JSONPathNode]
|
882
|
+
def resolve: (JSONPathNode node) -> ::Array[JSONPathNode]
|
883
|
+
def resolve_enum: (JSONPathNode node) -> Enumerable[JSONPathNode]
|
837
884
|
|
838
885
|
def to_s: () -> ::String
|
839
886
|
|
@@ -926,7 +973,7 @@ module JSONP3
|
|
926
973
|
# @param value [String]
|
927
974
|
# @param quote [String] one of '"' or "'".
|
928
975
|
# @param token [Token]
|
929
|
-
# @return [String] A new string without escape
|
976
|
+
# @return [String] A new string without escape sequences.
|
930
977
|
def self.unescape_string: (String value, String quote, Token token) -> String
|
931
978
|
|
932
979
|
def self.decode_hex_char: (untyped value, untyped index, Token token) -> ::Array[untyped]
|
@@ -938,6 +985,8 @@ module JSONP3
|
|
938
985
|
def self.low_surrogate?: (untyped code_point) -> untyped
|
939
986
|
|
940
987
|
def self.code_point_to_string: (untyped code_point, Token token) -> untyped
|
988
|
+
|
989
|
+
def self.canonical_string: (untyped String) -> ::String
|
941
990
|
end
|
942
991
|
|
943
992
|
module JSONP3
|
@@ -1162,7 +1211,7 @@ module JSONP3
|
|
1162
1211
|
class OpRemove < Op
|
1163
1212
|
@pointer: JSONPointer
|
1164
1213
|
|
1165
|
-
# @param
|
1214
|
+
# @param pointer [JSONPointer]
|
1166
1215
|
def initialize: (JSONPointer pointer) -> void
|
1167
1216
|
|
1168
1217
|
def name: () -> "remove"
|
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.0
|
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:
|
39
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
40
40
|
dependencies: []
|
41
41
|
description: JSONPath following RFC 9535
|
42
42
|
email:
|
@@ -46,6 +46,7 @@ extensions: []
|
|
46
46
|
extra_rdoc_files: []
|
47
47
|
files:
|
48
48
|
- ".rubocop.yml"
|
49
|
+
- ".ruby-version"
|
49
50
|
- ".yardopts"
|
50
51
|
- CHANGELOG.md
|
51
52
|
- LICENCE
|
@@ -73,6 +74,7 @@ files:
|
|
73
74
|
- lib/json_p3/pointer.rb
|
74
75
|
- lib/json_p3/segment.rb
|
75
76
|
- lib/json_p3/selector.rb
|
77
|
+
- lib/json_p3/serialize.rb
|
76
78
|
- lib/json_p3/token.rb
|
77
79
|
- lib/json_p3/unescape.rb
|
78
80
|
- lib/json_p3/version.rb
|
@@ -106,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
108
|
- !ruby/object:Gem::Version
|
107
109
|
version: '0'
|
108
110
|
requirements: []
|
109
|
-
rubygems_version: 3.
|
111
|
+
rubygems_version: 3.5.22
|
110
112
|
signing_key:
|
111
113
|
specification_version: 4
|
112
114
|
summary: 'JSONPath: Query Expressions for JSON in Ruby'
|
metadata.gz.sig
CHANGED
Binary file
|