philiprehberger-json_path 0.1.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78a07561ecdca899e8715c239ebaaa0e7ed9892a557445bcceb03b3cbb7f61a6
4
- data.tar.gz: 3e418c526ccb1fff468d02485c7b76a071de30b84059c1205b9478e0d683c235
3
+ metadata.gz: a5f622166e4beba5e485f24b63a4286e28b6745c120a1271cd3e663cfe87150f
4
+ data.tar.gz: 71dd674e321a36c295037a9672ab8c92f0eb87e6709a9ec7bbc98299abc14343
5
5
  SHA512:
6
- metadata.gz: 40bd26003e395e6ca24f52cf469df2004096cecb5113365b65f5fab8153614bd8c0fa62acd49f11bfbc9da9971b56b6ba8aa839536796336a24ba0a47ea90b4d
7
- data.tar.gz: 1694b53c1f359b143eeb54a336c3e1612044408a7ebd9fb4d4b036ca6829777ace260a44c23f86d1b5ebfbade802a8be4508a5116aee0bc863dc805457945e92
6
+ metadata.gz: ff4e5a102c7c9ab0d59add298fbd4b9b2d4f9feb2d88274cdebdbb7358cdcb97aeccd99c62e6d5e38990902a1840224e4c042cb3e03c5751cb21a27f14a95410
7
+ data.tar.gz: 1bb735639d90eb6827d01b6b5a37ad5b9c63d9dcece71dfd45ef124bae7cca27de0dab19335184e0351e3f86c1905603f2f39aa8b48325183542dfa60adac16e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ 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
+
15
+ ## [0.2.0] - 2026-04-03
16
+
17
+ ### Added
18
+ - Recursive descent operator `..` for matching keys at any nesting depth
19
+ - `JsonPath.count(data, path)` method to return number of matches
20
+ - `JsonPath.values(data, path)` method as an alias for `query`
21
+ - Negation filter `!` support: `$[?(!@.key)]` matches items without a key
22
+ - Filter comparison with `@.key.length` for arrays and strings
23
+
24
+ ## [0.1.7] - 2026-03-31
25
+
26
+ ### Added
27
+ - Add GitHub issue templates, dependabot config, and PR template
28
+
10
29
  ## [0.1.6] - 2026-03-31
11
30
 
12
31
  ### Changed
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Gem Version](https://badge.fury.io/rb/philiprehberger-json_path.svg)](https://rubygems.org/gems/philiprehberger-json_path)
5
5
  [![Last updated](https://img.shields.io/github/last-commit/philiprehberger/rb-json-path)](https://github.com/philiprehberger/rb-json-path/commits/main)
6
6
 
7
- JSONPath expression evaluator with dot notation, wildcards, slices, and filters
7
+ JSONPath expression evaluator with dot notation, wildcards, slices, filters, and recursive descent
8
8
 
9
9
  ## Requirements
10
10
 
@@ -42,13 +42,44 @@ data = {
42
42
  Philiprehberger::JsonPath.query(data, '$.store.books[*].title')
43
43
  # => ["Ruby", "Python", "Go"]
44
44
 
45
+ Philiprehberger::JsonPath.values(data, '$.store.books[*].title')
46
+ # => ["Ruby", "Python", "Go"] (alias for query)
47
+
45
48
  Philiprehberger::JsonPath.first(data, '$.store.books[0].title')
46
49
  # => "Ruby"
47
50
 
51
+ Philiprehberger::JsonPath.count(data, '$.store.books[*]')
52
+ # => 3
53
+
48
54
  Philiprehberger::JsonPath.exists?(data, '$.store.books')
49
55
  # => true
50
56
  ```
51
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
+
73
+ ### Recursive Descent
74
+
75
+ ```ruby
76
+ Philiprehberger::JsonPath.query(data, '$..price')
77
+ # => [30, 25, 20]
78
+
79
+ Philiprehberger::JsonPath.query(data, '$..title')
80
+ # => ["Ruby", "Python", "Go"]
81
+ ```
82
+
52
83
  ### Array Indexing and Slicing
53
84
 
54
85
  ```ruby
@@ -72,6 +103,29 @@ Philiprehberger::JsonPath.query(data, "$.store.books[?(@.title=='Go')].price")
72
103
  # => [20]
73
104
  ```
74
105
 
106
+ ### Negation Filters
107
+
108
+ ```ruby
109
+ data = { 'items' => [{ 'name' => 'a', 'hidden' => true }, { 'name' => 'b' }] }
110
+
111
+ Philiprehberger::JsonPath.query(data, '$.items[?(!@.hidden)].name')
112
+ # => ["b"]
113
+ ```
114
+
115
+ ### Length Comparisons
116
+
117
+ ```ruby
118
+ data = {
119
+ 'groups' => [
120
+ { 'name' => 'team1', 'members' => ['Alice', 'Bob'] },
121
+ { 'name' => 'team2', 'members' => [] }
122
+ ]
123
+ }
124
+
125
+ Philiprehberger::JsonPath.query(data, '$.groups[?(@.members.length > 0)].name')
126
+ # => ["team1"]
127
+ ```
128
+
75
129
  ### Supported Syntax
76
130
 
77
131
  | Syntax | Description |
@@ -82,16 +136,22 @@ Philiprehberger::JsonPath.query(data, "$.store.books[?(@.title=='Go')].price")
82
136
  | `[n]` | Array index (supports negative) |
83
137
  | `[*]` | Wildcard (all elements) |
84
138
  | `[start:end]` | Array slice |
139
+ | `..key` | Recursive descent (match key at any depth) |
85
140
  | `[?(@.key>val)]` | Filter expression |
86
141
  | `[?(@.key)]` | Existence filter |
142
+ | `[?(!@.key)]` | Negation filter |
143
+ | `[?(@.key.length>n)]` | Length comparison filter |
87
144
 
88
145
  ## API
89
146
 
90
147
  | Method | Description |
91
148
  |--------|-------------|
92
149
  | `JsonPath.query(data, path)` | Return all matches as an array |
150
+ | `JsonPath.values(data, path)` | Alias for `query` |
93
151
  | `JsonPath.first(data, path)` | Return the first match or nil |
152
+ | `JsonPath.count(data, path)` | Return the number of matches |
94
153
  | `JsonPath.exists?(data, path)` | Check if any match exists |
154
+ | `JsonPath.paths(data, path)` | Return canonical JSONPath strings for each match |
95
155
 
96
156
  ## Development
97
157
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module JsonPath
5
- VERSION = '0.1.6'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -16,6 +16,15 @@ module Philiprehberger
16
16
  evaluate(data, tokens)
17
17
  end
18
18
 
19
+ # Alias for query (more discoverable name)
20
+ #
21
+ # @param data [Hash, Array] the data structure to query
22
+ # @param path [String] JSONPath expression
23
+ # @return [Array] all matching values
24
+ def self.values(data, path)
25
+ query(data, path)
26
+ end
27
+
19
28
  # Query data and return the first match
20
29
  #
21
30
  # @param data [Hash, Array] the data structure to query
@@ -25,6 +34,15 @@ module Philiprehberger
25
34
  query(data, path).first
26
35
  end
27
36
 
37
+ # Return the number of matches for a JSONPath expression
38
+ #
39
+ # @param data [Hash, Array] the data structure to query
40
+ # @param path [String] JSONPath expression
41
+ # @return [Integer] number of matches
42
+ def self.count(data, path)
43
+ query(data, path).size
44
+ end
45
+
28
46
  # Check if a JSONPath expression matches anything
29
47
  #
30
48
  # @param data [Hash, Array] the data structure to query
@@ -34,9 +52,39 @@ module Philiprehberger
34
52
  !query(data, path).empty?
35
53
  end
36
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
+
37
69
  class << self
38
70
  private
39
71
 
72
+ TOKEN_PATTERNS = [
73
+ [/\A\.\.(\w+)/, :recursive_pat],
74
+ [/\A\.(\w+)/, :key_pat],
75
+ [/\A\[(\d+)\]/, :index_pat],
76
+ [/\A\[\*\]/, :wildcard_pat],
77
+ [/\A\[(-?\d+):(-?\d+)\]/, :slice_both_pat],
78
+ [/\A\[:(-?\d+)\]/, :slice_end_pat],
79
+ [/\A\[(-?\d+):\]/, :slice_start_pat],
80
+ [/\A\[\?\(!@\.(\w+)\)\]/, :filter_not_exists_pat],
81
+ [/\A\[\?\(@\.([\w.]+)\.length\s*(==|!=|>|>=|<|<=)\s*([^\]]+)\)\]/, :filter_length_pat],
82
+ [/\A\[\?\(@\.(\w+)\s*(==|!=|>|>=|<|<=)\s*([^\]]+)\)\]/, :filter_pat],
83
+ [/\A\[\?\(@\.(\w+)\)\]/, :filter_exists_pat],
84
+ [/\A\['([^']+)'\]/, :bracket_single_pat],
85
+ [/\A\["([^"]+)"\]/, :bracket_double_pat]
86
+ ].freeze
87
+
40
88
  def tokenize(path)
41
89
  raise Error, 'Path must start with $' unless path.to_s.start_with?('$')
42
90
 
@@ -44,50 +92,44 @@ module Philiprehberger
44
92
  tokens = []
45
93
 
46
94
  until remaining.empty?
47
- case remaining
48
- when /\A\.(\w+)/
49
- tokens << { type: :key, value: Regexp.last_match(1) }
50
- remaining = remaining[Regexp.last_match(0).length..]
51
- when /\A\[(\d+)\]/
52
- tokens << { type: :index, value: Regexp.last_match(1).to_i }
53
- remaining = remaining[Regexp.last_match(0).length..]
54
- when /\A\[\*\]/
55
- tokens << { type: :wildcard }
56
- remaining = remaining[3..]
57
- when /\A\[(-?\d+):(-?\d+)\]/
58
- tokens << { type: :slice, start: Regexp.last_match(1).to_i, end: Regexp.last_match(2).to_i }
59
- remaining = remaining[Regexp.last_match(0).length..]
60
- when /\A\[:(-?\d+)\]/
61
- tokens << { type: :slice, start: 0, end: Regexp.last_match(1).to_i }
62
- remaining = remaining[Regexp.last_match(0).length..]
63
- when /\A\[(-?\d+):\]/
64
- tokens << { type: :slice, start: Regexp.last_match(1).to_i, end: nil }
65
- remaining = remaining[Regexp.last_match(0).length..]
66
- when /\A\[\?\(@\.(\w+)\s*(==|!=|>|>=|<|<=)\s*([^\]]+)\)\]/
67
- tokens << {
68
- type: :filter,
69
- key: Regexp.last_match(1),
70
- op: Regexp.last_match(2),
71
- value: parse_filter_value(Regexp.last_match(3).strip)
72
- }
73
- remaining = remaining[Regexp.last_match(0).length..]
74
- when /\A\[\?\(@\.(\w+)\)\]/
75
- tokens << { type: :filter_exists, key: Regexp.last_match(1) }
76
- remaining = remaining[Regexp.last_match(0).length..]
77
- when /\A\['([^']+)'\]/
78
- tokens << { type: :key, value: Regexp.last_match(1) }
79
- remaining = remaining[Regexp.last_match(0).length..]
80
- when /\A\["([^"]+)"\]/
81
- tokens << { type: :key, value: Regexp.last_match(1) }
82
- remaining = remaining[Regexp.last_match(0).length..]
83
- else
84
- raise Error, "Unexpected token at: #{remaining}"
85
- end
95
+ token, consumed = match_token(remaining)
96
+ raise Error, "Unexpected token at: #{remaining}" unless token
97
+
98
+ tokens << token
99
+ remaining = remaining[consumed..]
86
100
  end
87
101
 
88
102
  tokens
89
103
  end
90
104
 
105
+ def match_token(remaining)
106
+ TOKEN_PATTERNS.each do |pattern, kind|
107
+ m = pattern.match(remaining)
108
+ next unless m
109
+
110
+ return build_token(kind, m), m[0].length
111
+ end
112
+ nil
113
+ end
114
+
115
+ def build_token(kind, m)
116
+ case kind
117
+ when :recursive_pat then { type: :recursive, value: m[1] }
118
+ when :key_pat then { type: :key, value: m[1] }
119
+ when :index_pat then { type: :index, value: m[1].to_i }
120
+ when :wildcard_pat then { type: :wildcard }
121
+ when :slice_both_pat then { type: :slice, start: m[1].to_i, end: m[2].to_i }
122
+ when :slice_end_pat then { type: :slice, start: 0, end: m[1].to_i }
123
+ when :slice_start_pat then { type: :slice, start: m[1].to_i, end: nil }
124
+ when :filter_not_exists_pat then { type: :filter_not_exists, key: m[1] }
125
+ when :filter_length_pat then { type: :filter_length, key_path: m[1], op: m[2], value: parse_filter_value(m[3].strip) }
126
+ when :filter_pat then { type: :filter, key: m[1], op: m[2], value: parse_filter_value(m[3].strip) }
127
+ when :filter_exists_pat then { type: :filter_exists, key: m[1] }
128
+ when :bracket_single_pat then { type: :key, value: m[1] }
129
+ when :bracket_double_pat then { type: :key, value: m[1] }
130
+ end
131
+ end
132
+
91
133
  def parse_filter_value(str)
92
134
  case str
93
135
  when /\A'(.*)'\z/ then Regexp.last_match(1)
@@ -125,6 +167,12 @@ module Philiprehberger
125
167
  apply_filter(node, token[:key], token[:op], token[:value])
126
168
  when :filter_exists
127
169
  apply_filter_exists(node, token[:key])
170
+ when :filter_not_exists
171
+ apply_filter_not_exists(node, token[:key])
172
+ when :filter_length
173
+ apply_filter_length(node, token[:key_path], token[:op], token[:value])
174
+ when :recursive
175
+ apply_recursive(node, token[:value])
128
176
  else
129
177
  []
130
178
  end
@@ -191,6 +239,219 @@ module Philiprehberger
191
239
  end
192
240
  end
193
241
 
242
+ def apply_filter_not_exists(node, key)
243
+ return [] unless node.is_a?(Array)
244
+
245
+ node.reject do |item|
246
+ next false unless item.is_a?(Hash)
247
+
248
+ item.key?(key) || item.key?(key.to_sym)
249
+ end
250
+ end
251
+
252
+ def apply_filter_length(node, key_path, op, value)
253
+ return [] unless node.is_a?(Array)
254
+
255
+ node.select do |item|
256
+ next false unless item.is_a?(Hash)
257
+
258
+ resolved = resolve_key_path(item, key_path)
259
+ next false if resolved.nil?
260
+
261
+ length = resolved.respond_to?(:length) ? resolved.length : nil
262
+ next false if length.nil?
263
+
264
+ compare(length, op, value)
265
+ end
266
+ end
267
+
268
+ def resolve_key_path(node, key_path)
269
+ keys = key_path.split('.')
270
+ current = node
271
+ keys.each do |key|
272
+ return nil unless current.is_a?(Hash)
273
+
274
+ current = current[key] || current[key.to_sym]
275
+ return nil if current.nil?
276
+ end
277
+ current
278
+ end
279
+
280
+ def apply_recursive(node, key)
281
+ results = []
282
+ collect_recursive(node, key, results)
283
+ results
284
+ end
285
+
286
+ def collect_recursive(node, key, results)
287
+ case node
288
+ when Hash
289
+ node.each do |k, v|
290
+ results << v if k.to_s == key
291
+ collect_recursive(v, key, results)
292
+ end
293
+ when Array
294
+ node.each { |item| collect_recursive(item, key, results) }
295
+ end
296
+ end
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
+
194
455
  def compare(actual, op, value)
195
456
  case op
196
457
  when '==' then actual == value
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-json_path
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
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-03-31 00:00:00.000000000 Z
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
- dot notation, array indexing, wildcards, slices, and filter expressions for querying
15
- nested data.
14
+ dot notation, array indexing, wildcards, slices, filter expressions, recursive descent,
15
+ and length comparisons for querying nested data.
16
16
  email:
17
17
  - philiprehberger@users.noreply.github.com
18
18
  executables: []
@@ -24,11 +24,11 @@ files:
24
24
  - README.md
25
25
  - lib/philiprehberger/json_path.rb
26
26
  - lib/philiprehberger/json_path/version.rb
27
- homepage: https://github.com/philiprehberger/rb-json-path
27
+ homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-json_path
28
28
  licenses:
29
29
  - MIT
30
30
  metadata:
31
- homepage_uri: https://github.com/philiprehberger/rb-json-path
31
+ homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-json_path
32
32
  source_code_uri: https://github.com/philiprehberger/rb-json-path
33
33
  changelog_uri: https://github.com/philiprehberger/rb-json-path/blob/main/CHANGELOG.md
34
34
  bug_tracker_uri: https://github.com/philiprehberger/rb-json-path/issues
@@ -51,5 +51,6 @@ requirements: []
51
51
  rubygems_version: 3.5.22
52
52
  signing_key:
53
53
  specification_version: 4
54
- summary: JSONPath expression evaluator with dot notation, wildcards, slices, and filters
54
+ summary: JSONPath expression evaluator with dot notation, wildcards, slices, filters,
55
+ and recursive descent
55
56
  test_files: []