philiprehberger-json_path 0.2.0 → 0.3.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
- data/CHANGELOG.md +5 -0
- data/README.md +16 -0
- data/lib/philiprehberger/json_path/version.rb +1 -1
- data/lib/philiprehberger/json_path.rb +171 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5f622166e4beba5e485f24b63a4286e28b6745c120a1271cd3e663cfe87150f
|
|
4
|
+
data.tar.gz: 71dd674e321a36c295037a9672ab8c92f0eb87e6709a9ec7bbc98299abc14343
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff4e5a102c7c9ab0d59add298fbd4b9b2d4f9feb2d88274cdebdbb7358cdcb97aeccd99c62e6d5e38990902a1840224e4c042cb3e03c5751cb21a27f14a95410
|
|
7
|
+
data.tar.gz: 1bb735639d90eb6827d01b6b5a37ad5b9c63d9dcece71dfd45ef124bae7cca27de0dab19335184e0351e3f86c1905603f2f39aa8b48325183542dfa60adac16e
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `JsonPath.paths(data, expression)` returns the canonical JSONPath strings of every match (complement to `.values`)
|
|
14
|
+
|
|
10
15
|
## [0.2.0] - 2026-04-03
|
|
11
16
|
|
|
12
17
|
### Added
|
data/README.md
CHANGED
|
@@ -55,6 +55,21 @@ Philiprehberger::JsonPath.exists?(data, '$.store.books')
|
|
|
55
55
|
# => true
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
### Matched paths
|
|
59
|
+
|
|
60
|
+
Use `paths` to get canonical JSONPath strings for every match, rather than the values themselves. Each returned path re-evaluates back to the single element it identifies, making it handy for diffing, patching, or reporting.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Philiprehberger::JsonPath.paths(data, '$.store.books[*].title')
|
|
64
|
+
# => ["$.store.books[0].title", "$.store.books[1].title", "$.store.books[2].title"]
|
|
65
|
+
|
|
66
|
+
Philiprehberger::JsonPath.paths(data, '$.store.books[?(@.price>22)]')
|
|
67
|
+
# => ["$.store.books[0]", "$.store.books[1]"]
|
|
68
|
+
|
|
69
|
+
Philiprehberger::JsonPath.paths(data, '$.missing')
|
|
70
|
+
# => []
|
|
71
|
+
```
|
|
72
|
+
|
|
58
73
|
### Recursive Descent
|
|
59
74
|
|
|
60
75
|
```ruby
|
|
@@ -136,6 +151,7 @@ Philiprehberger::JsonPath.query(data, '$.groups[?(@.members.length > 0)].name')
|
|
|
136
151
|
| `JsonPath.first(data, path)` | Return the first match or nil |
|
|
137
152
|
| `JsonPath.count(data, path)` | Return the number of matches |
|
|
138
153
|
| `JsonPath.exists?(data, path)` | Check if any match exists |
|
|
154
|
+
| `JsonPath.paths(data, path)` | Return canonical JSONPath strings for each match |
|
|
139
155
|
|
|
140
156
|
## Development
|
|
141
157
|
|
|
@@ -52,6 +52,20 @@ module Philiprehberger
|
|
|
52
52
|
!query(data, path).empty?
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Return the canonical JSONPath strings for every match of an expression
|
|
56
|
+
#
|
|
57
|
+
# Each returned path is an unambiguous JSONPath that, when re-evaluated,
|
|
58
|
+
# resolves to the single element it identifies. Paths are returned in
|
|
59
|
+
# document order. Returns an empty array when no matches exist.
|
|
60
|
+
#
|
|
61
|
+
# @param data [Hash, Array] the data structure to query
|
|
62
|
+
# @param path [String] JSONPath expression
|
|
63
|
+
# @return [Array<String>] canonical JSONPath strings for each match
|
|
64
|
+
def self.paths(data, path)
|
|
65
|
+
tokens = tokenize(path)
|
|
66
|
+
evaluate_with_paths(data, tokens).map { |_node, p| p }
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
class << self
|
|
56
70
|
private
|
|
57
71
|
|
|
@@ -281,6 +295,163 @@ module Philiprehberger
|
|
|
281
295
|
end
|
|
282
296
|
end
|
|
283
297
|
|
|
298
|
+
SAFE_KEY = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
299
|
+
|
|
300
|
+
def evaluate_with_paths(data, tokens)
|
|
301
|
+
results = [[data, '$']]
|
|
302
|
+
|
|
303
|
+
tokens.each do |token|
|
|
304
|
+
results = results.flat_map { |node, path| apply_token_with_paths(node, path, token) }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
results
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def apply_token_with_paths(node, path, token)
|
|
311
|
+
case token[:type]
|
|
312
|
+
when :key
|
|
313
|
+
apply_key_with_paths(node, path, token[:value])
|
|
314
|
+
when :index
|
|
315
|
+
apply_index_with_paths(node, path, token[:value])
|
|
316
|
+
when :wildcard
|
|
317
|
+
apply_wildcard_with_paths(node, path)
|
|
318
|
+
when :slice
|
|
319
|
+
apply_slice_with_paths(node, path, token[:start], token[:end])
|
|
320
|
+
when :filter
|
|
321
|
+
apply_filter_with_paths(node, path, token[:key], token[:op], token[:value])
|
|
322
|
+
when :filter_exists
|
|
323
|
+
apply_filter_exists_with_paths(node, path, token[:key])
|
|
324
|
+
when :filter_not_exists
|
|
325
|
+
apply_filter_not_exists_with_paths(node, path, token[:key])
|
|
326
|
+
when :filter_length
|
|
327
|
+
apply_filter_length_with_paths(node, path, token[:key_path], token[:op], token[:value])
|
|
328
|
+
when :recursive
|
|
329
|
+
apply_recursive_with_paths(node, path, token[:value])
|
|
330
|
+
else
|
|
331
|
+
[]
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def key_segment(key)
|
|
336
|
+
str = key.to_s
|
|
337
|
+
SAFE_KEY.match?(str) ? ".#{str}" : "['#{str.gsub("'", "\\\\'")}']"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def apply_key_with_paths(node, path, key)
|
|
341
|
+
return [] unless node.is_a?(Hash)
|
|
342
|
+
|
|
343
|
+
sym_key = key.to_sym
|
|
344
|
+
if node.key?(key)
|
|
345
|
+
[[node[key], path + key_segment(key)]]
|
|
346
|
+
elsif node.key?(sym_key)
|
|
347
|
+
[[node[sym_key], path + key_segment(key)]]
|
|
348
|
+
else
|
|
349
|
+
[]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def apply_index_with_paths(node, path, index)
|
|
354
|
+
return [] unless node.is_a?(Array)
|
|
355
|
+
return [] if index >= node.length || index < -node.length
|
|
356
|
+
|
|
357
|
+
normalized = index.negative? ? node.length + index : index
|
|
358
|
+
[[node[index], "#{path}[#{normalized}]"]]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def apply_wildcard_with_paths(node, path)
|
|
362
|
+
case node
|
|
363
|
+
when Array
|
|
364
|
+
node.each_with_index.map { |v, i| [v, "#{path}[#{i}]"] }
|
|
365
|
+
when Hash
|
|
366
|
+
node.map { |k, v| [v, path + key_segment(k)] }
|
|
367
|
+
else
|
|
368
|
+
[]
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def apply_slice_with_paths(node, path, start_idx, end_idx)
|
|
373
|
+
return [] unless node.is_a?(Array)
|
|
374
|
+
|
|
375
|
+
end_idx = node.length if end_idx.nil?
|
|
376
|
+
range = (start_idx...end_idx)
|
|
377
|
+
indices = range.to_a.select { |i| i >= 0 && i < node.length }
|
|
378
|
+
indices.map { |i| [node[i], "#{path}[#{i}]"] }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def apply_filter_with_paths(node, path, key, op, value)
|
|
382
|
+
return [] unless node.is_a?(Array)
|
|
383
|
+
|
|
384
|
+
matches = node.each_with_index.select do |item, _i|
|
|
385
|
+
next false unless item.is_a?(Hash)
|
|
386
|
+
|
|
387
|
+
actual = item[key] || item[key.to_sym]
|
|
388
|
+
next false if actual.nil? && !item.key?(key) && !item.key?(key.to_sym)
|
|
389
|
+
|
|
390
|
+
compare(actual, op, value)
|
|
391
|
+
end
|
|
392
|
+
matches.map { |item, i| [item, "#{path}[#{i}]"] }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def apply_filter_exists_with_paths(node, path, key)
|
|
396
|
+
return [] unless node.is_a?(Array)
|
|
397
|
+
|
|
398
|
+
matches = node.each_with_index.select do |item, _i|
|
|
399
|
+
next false unless item.is_a?(Hash)
|
|
400
|
+
|
|
401
|
+
item.key?(key) || item.key?(key.to_sym)
|
|
402
|
+
end
|
|
403
|
+
matches.map { |item, i| [item, "#{path}[#{i}]"] }
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def apply_filter_not_exists_with_paths(node, path, key)
|
|
407
|
+
return [] unless node.is_a?(Array)
|
|
408
|
+
|
|
409
|
+
matches = node.each_with_index.reject do |item, _i|
|
|
410
|
+
next false unless item.is_a?(Hash)
|
|
411
|
+
|
|
412
|
+
item.key?(key) || item.key?(key.to_sym)
|
|
413
|
+
end
|
|
414
|
+
matches.map { |item, i| [item, "#{path}[#{i}]"] }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def apply_filter_length_with_paths(node, path, key_path, op, value)
|
|
418
|
+
return [] unless node.is_a?(Array)
|
|
419
|
+
|
|
420
|
+
matches = node.each_with_index.select do |item, _i|
|
|
421
|
+
next false unless item.is_a?(Hash)
|
|
422
|
+
|
|
423
|
+
resolved = resolve_key_path(item, key_path)
|
|
424
|
+
next false if resolved.nil?
|
|
425
|
+
|
|
426
|
+
length = resolved.respond_to?(:length) ? resolved.length : nil
|
|
427
|
+
next false if length.nil?
|
|
428
|
+
|
|
429
|
+
compare(length, op, value)
|
|
430
|
+
end
|
|
431
|
+
matches.map { |item, i| [item, "#{path}[#{i}]"] }
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def apply_recursive_with_paths(node, path, key)
|
|
435
|
+
results = []
|
|
436
|
+
collect_recursive_with_paths(node, path, key, results)
|
|
437
|
+
results
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def collect_recursive_with_paths(node, path, key, results)
|
|
441
|
+
case node
|
|
442
|
+
when Hash
|
|
443
|
+
node.each do |k, v|
|
|
444
|
+
child_path = path + key_segment(k)
|
|
445
|
+
results << [v, child_path] if k.to_s == key
|
|
446
|
+
collect_recursive_with_paths(v, child_path, key, results)
|
|
447
|
+
end
|
|
448
|
+
when Array
|
|
449
|
+
node.each_with_index do |item, i|
|
|
450
|
+
collect_recursive_with_paths(item, "#{path}[#{i}]", key, results)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
284
455
|
def compare(actual, op, value)
|
|
285
456
|
case op
|
|
286
457
|
when '==' then actual == value
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-json_path
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- philiprehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Evaluate JSONPath expressions against Ruby hashes and arrays. Supports
|
|
14
14
|
dot notation, array indexing, wildcards, slices, filter expressions, recursive descent,
|