philiprehberger-json_path 0.3.0 → 0.5.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 +10 -0
- data/README.md +20 -0
- data/lib/philiprehberger/json_path/version.rb +1 -1
- data/lib/philiprehberger/json_path.rb +82 -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: f593dd25616efcfc78e8efbda2f1909d8ba63d19e1b144c9d9708c7a9e736fde
|
|
4
|
+
data.tar.gz: 83c8f2bc4b1dfdc7da601cb21739ae2b2ff5c22964d0cefeae04cb6bcae4d09a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63eaa887ffe7f5a8e440b06e78217c9f19391dafd3705ac0fb29a68fbd6362599532626f2c47f9848d76e83370bc3a38503942a8e48d3fbf4cd332adef9813e4
|
|
7
|
+
data.tar.gz: 86c79db3e059ae57cd8192fa63168aa58b34a6a187041a3ce7905a58276a30fd0c10b7289da3724ec122d49283011c555f9b557b3e916bee3f91a168f05217fe
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-05-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `JsonPath.update(data, path, &block)` — apply a block to every JSONPath match, replacing each with the block's return value. Mutates `data` in place; returns it for chaining. Raises when the path resolves to the root document.
|
|
14
|
+
|
|
15
|
+
## [0.4.0] - 2026-04-19
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `JsonPath.last(data, path)` — last matching value; symmetric with existing `.first`; returns `nil` on no match
|
|
19
|
+
|
|
10
20
|
## [0.3.0] - 2026-04-17
|
|
11
21
|
|
|
12
22
|
### Added
|
data/README.md
CHANGED
|
@@ -48,6 +48,10 @@ Philiprehberger::JsonPath.values(data, '$.store.books[*].title')
|
|
|
48
48
|
Philiprehberger::JsonPath.first(data, '$.store.books[0].title')
|
|
49
49
|
# => "Ruby"
|
|
50
50
|
|
|
51
|
+
items = { "items" => [{ "id" => 1 }, { "id" => 2 }, { "id" => 3 }] }
|
|
52
|
+
Philiprehberger::JsonPath.first(items, "$.items[*].id") # => 1
|
|
53
|
+
Philiprehberger::JsonPath.last(items, "$.items[*].id") # => 3
|
|
54
|
+
|
|
51
55
|
Philiprehberger::JsonPath.count(data, '$.store.books[*]')
|
|
52
56
|
# => 3
|
|
53
57
|
|
|
@@ -126,6 +130,20 @@ Philiprehberger::JsonPath.query(data, '$.groups[?(@.members.length > 0)].name')
|
|
|
126
130
|
# => ["team1"]
|
|
127
131
|
```
|
|
128
132
|
|
|
133
|
+
### In-place Updates
|
|
134
|
+
|
|
135
|
+
Apply a transformation to every match. Mutates the data and returns it for chaining.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
data = { 'users' => [{ 'name' => 'alice' }, { 'name' => 'bob' }] }
|
|
139
|
+
|
|
140
|
+
Philiprehberger::JsonPath.update(data, '$.users[*].name', &:upcase)
|
|
141
|
+
# data["users"].map { |u| u["name"] } => ["ALICE", "BOB"]
|
|
142
|
+
|
|
143
|
+
# Increment every numeric price in a nested store
|
|
144
|
+
Philiprehberger::JsonPath.update(catalog, '$..price') { |p| p * 1.10 }
|
|
145
|
+
```
|
|
146
|
+
|
|
129
147
|
### Supported Syntax
|
|
130
148
|
|
|
131
149
|
| Syntax | Description |
|
|
@@ -149,9 +167,11 @@ Philiprehberger::JsonPath.query(data, '$.groups[?(@.members.length > 0)].name')
|
|
|
149
167
|
| `JsonPath.query(data, path)` | Return all matches as an array |
|
|
150
168
|
| `JsonPath.values(data, path)` | Alias for `query` |
|
|
151
169
|
| `JsonPath.first(data, path)` | Return the first match or nil |
|
|
170
|
+
| `JsonPath.last(data, path)` | Return the last match or nil |
|
|
152
171
|
| `JsonPath.count(data, path)` | Return the number of matches |
|
|
153
172
|
| `JsonPath.exists?(data, path)` | Check if any match exists |
|
|
154
173
|
| `JsonPath.paths(data, path)` | Return canonical JSONPath strings for each match |
|
|
174
|
+
| `JsonPath.update(data, path, &block)` | Replace every matched value with the block's return; mutates `data` in place |
|
|
155
175
|
|
|
156
176
|
## Development
|
|
157
177
|
|
|
@@ -34,6 +34,15 @@ module Philiprehberger
|
|
|
34
34
|
query(data, path).first
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Query data and return the last match
|
|
38
|
+
#
|
|
39
|
+
# @param data [Hash, Array] the data structure to query
|
|
40
|
+
# @param path [String] JSONPath expression
|
|
41
|
+
# @return [Object, nil] the last matching value or nil
|
|
42
|
+
def self.last(data, path)
|
|
43
|
+
query(data, path).last
|
|
44
|
+
end
|
|
45
|
+
|
|
37
46
|
# Return the number of matches for a JSONPath expression
|
|
38
47
|
#
|
|
39
48
|
# @param data [Hash, Array] the data structure to query
|
|
@@ -66,6 +75,79 @@ module Philiprehberger
|
|
|
66
75
|
evaluate_with_paths(data, tokens).map { |_node, p| p }
|
|
67
76
|
end
|
|
68
77
|
|
|
78
|
+
# Apply a transformation to every JSONPath match in `data`.
|
|
79
|
+
#
|
|
80
|
+
# Mutates `data` in place and returns it for chaining. Each match is
|
|
81
|
+
# replaced with the block's return value. Skips matches that resolve
|
|
82
|
+
# to the document root (`"$"`). Use `Marshal.load(Marshal.dump(data))`
|
|
83
|
+
# first if you need to preserve the original.
|
|
84
|
+
#
|
|
85
|
+
# @param data [Hash, Array] the data to transform
|
|
86
|
+
# @param path [String] JSONPath expression
|
|
87
|
+
# @yield [value] each matched value
|
|
88
|
+
# @yieldreturn [Object] the replacement value
|
|
89
|
+
# @return [Hash, Array] the mutated data
|
|
90
|
+
# @raise [Error] when the path resolves to the root document
|
|
91
|
+
def self.update(data, path, &block)
|
|
92
|
+
raise Error, 'block required for update' unless block
|
|
93
|
+
|
|
94
|
+
paths(data, path).each do |canonical|
|
|
95
|
+
raise Error, 'Cannot update root document' if canonical == '$'
|
|
96
|
+
|
|
97
|
+
mutate_at(data, canonical, &block)
|
|
98
|
+
end
|
|
99
|
+
data
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @api private
|
|
103
|
+
def self.mutate_at(data, canonical, &block)
|
|
104
|
+
segments = canonical_segments(canonical)
|
|
105
|
+
*parent_path, last = segments
|
|
106
|
+
parent = parent_path.reduce(data) { |node, seg| descend(node, seg) }
|
|
107
|
+
|
|
108
|
+
if parent.is_a?(Hash)
|
|
109
|
+
key = parent.key?(last) ? last : last.to_sym
|
|
110
|
+
parent[key] = block.call(parent[key])
|
|
111
|
+
elsif parent.is_a?(Array)
|
|
112
|
+
idx = Integer(last)
|
|
113
|
+
parent[idx] = block.call(parent[idx])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
def self.canonical_segments(canonical)
|
|
119
|
+
remaining = canonical[1..]
|
|
120
|
+
segments = []
|
|
121
|
+
|
|
122
|
+
until remaining.empty?
|
|
123
|
+
case remaining
|
|
124
|
+
when /\A\.([A-Za-z_][A-Za-z0-9_]*)/
|
|
125
|
+
segments << ::Regexp.last_match(1)
|
|
126
|
+
remaining = remaining[::Regexp.last_match(0).length..]
|
|
127
|
+
when /\A\['((?:\\'|[^'])*)'\]/
|
|
128
|
+
segments << ::Regexp.last_match(1).gsub("\\'", "'")
|
|
129
|
+
remaining = remaining[::Regexp.last_match(0).length..]
|
|
130
|
+
when /\A\[(\d+)\]/
|
|
131
|
+
segments << ::Regexp.last_match(1)
|
|
132
|
+
remaining = remaining[::Regexp.last_match(0).length..]
|
|
133
|
+
else
|
|
134
|
+
raise Error, "Unparseable canonical path segment: #{remaining}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
segments
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @api private
|
|
142
|
+
def self.descend(node, segment)
|
|
143
|
+
if node.is_a?(Hash)
|
|
144
|
+
node.key?(segment) ? node[segment] : node[segment.to_sym]
|
|
145
|
+
elsif node.is_a?(Array)
|
|
146
|
+
node[Integer(segment)]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
private_class_method :mutate_at, :canonical_segments, :descend
|
|
150
|
+
|
|
69
151
|
class << self
|
|
70
152
|
private
|
|
71
153
|
|
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.5.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-
|
|
11
|
+
date: 2026-05-01 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,
|