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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5f622166e4beba5e485f24b63a4286e28b6745c120a1271cd3e663cfe87150f
4
- data.tar.gz: 71dd674e321a36c295037a9672ab8c92f0eb87e6709a9ec7bbc98299abc14343
3
+ metadata.gz: f593dd25616efcfc78e8efbda2f1909d8ba63d19e1b144c9d9708c7a9e736fde
4
+ data.tar.gz: 83c8f2bc4b1dfdc7da601cb21739ae2b2ff5c22964d0cefeae04cb6bcae4d09a
5
5
  SHA512:
6
- metadata.gz: ff4e5a102c7c9ab0d59add298fbd4b9b2d4f9feb2d88274cdebdbb7358cdcb97aeccd99c62e6d5e38990902a1840224e4c042cb3e03c5751cb21a27f14a95410
7
- data.tar.gz: 1bb735639d90eb6827d01b6b5a37ad5b9c63d9dcece71dfd45ef124bae7cca27de0dab19335184e0351e3f86c1905603f2f39aa8b48325183542dfa60adac16e
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module JsonPath
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -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.3.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-04-18 00:00:00.000000000 Z
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,