janeway-jsonpath 0.5.0 → 0.6.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/README.md +83 -5
- data/bin/janeway +77 -28
- data/lib/janeway/ast/child_segment.rb +8 -7
- data/lib/janeway/ast/filter_selector.rb +0 -2
- data/lib/janeway/ast/function.rb +2 -0
- data/lib/janeway/ast/selector.rb +17 -18
- data/lib/janeway/ast.rb +31 -0
- data/lib/janeway/enumerator.rb +152 -5
- data/lib/janeway/interpreter.rb +11 -1
- data/lib/janeway/interpreters/array_slice_selector_delete_if.rb +57 -0
- data/lib/janeway/interpreters/array_slice_selector_deleter.rb +1 -1
- data/lib/janeway/interpreters/child_segment_delete_if.rb +20 -0
- data/lib/janeway/interpreters/child_segment_deleter.rb +1 -1
- data/lib/janeway/interpreters/filter_selector_delete_if.rb +73 -0
- data/lib/janeway/interpreters/index_selector_delete_if.rb +40 -0
- data/lib/janeway/interpreters/iteration_helper.rb +45 -0
- data/lib/janeway/interpreters/name_selector_delete_if.rb +42 -0
- data/lib/janeway/interpreters/root_node_delete_if.rb +34 -0
- data/lib/janeway/interpreters/tree_constructor.rb +31 -1
- data/lib/janeway/interpreters/wildcard_selector_delete_if.rb +61 -0
- data/lib/janeway/interpreters/yielder.rb +7 -33
- data/lib/janeway/lexer.rb +40 -40
- data/lib/janeway/parser.rb +2 -0
- data/lib/janeway/query.rb +40 -0
- data/lib/janeway/version.rb +2 -1
- data/lib/janeway.rb +50 -37
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 33fc9254c0443121404ced6b9e60c15133111299b8f7052f012fe45d7ad4d281
|
4
|
+
data.tar.gz: c5a9eea01a8a332343b47328c64991a52fc50292969247433650914c8af727bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c101d0e02380fc56bd4abc83b5012965e3281aabc3bcce10e7f55c6cff61ba5b825f0d582f95c076234e01d59f07a8db19e6b8984c85e923c780d6f362adc2c
|
7
|
+
data.tar.gz: e2a762c773653c01415dee8f988b95be53d45da8bb3d1ed6f20817eb333fce00b3c960f9268a2212c929a27582d4c0cdb8e480e3948b2e193974f452b32e526f
|
data/README.md
CHANGED
@@ -126,7 +126,7 @@ Returns all values that match the query.
|
|
126
126
|
# Returns every book in the store cheaper than $10
|
127
127
|
```
|
128
128
|
|
129
|
-
Alternatively,
|
129
|
+
Alternatively, parse the query once, and share it between threads or ractors with different data sources:
|
130
130
|
|
131
131
|
```ruby
|
132
132
|
# Create ractors with their own data sources
|
@@ -140,7 +140,7 @@ Alternatively, compile the query once, and share it between threads or ractors w
|
|
140
140
|
end
|
141
141
|
|
142
142
|
# Construct JSONPath query object and send it to each ractor
|
143
|
-
query = Janeway.
|
143
|
+
query = Janeway.parse('$..book[? @.price<10]')
|
144
144
|
ractors.each { |ractor| ractor.send(query).take }
|
145
145
|
```
|
146
146
|
|
@@ -192,7 +192,7 @@ For example, iterating over an array may yield index values 1, 2 and 3.
|
|
192
192
|
Deleting the value at index 1 would change the index for the remaining values.
|
193
193
|
Next iteration might yield index 2, but since 1 was deleted it is now really at index 1.
|
194
194
|
|
195
|
-
To avoid having to deal with such problems, use the
|
195
|
+
To avoid having to deal with such problems, use the `#delete` method instead.
|
196
196
|
|
197
197
|
|
198
198
|
Lastly, the `#each` iterator's fourth yield parameter is the [normalized path](https://www.rfc-editor.org/rfc/rfc9535.html#name-normalized-paths) to the matched value.
|
@@ -205,8 +205,10 @@ This is a jsonpath query string that uniquely points to the matched value.
|
|
205
205
|
paths << path
|
206
206
|
end
|
207
207
|
paths
|
208
|
-
#
|
209
|
-
#
|
208
|
+
# [
|
209
|
+
# "$['birds']", "$['dogs']", "$['birds'][0]", "$['birds'][1]",
|
210
|
+
# "$['birds'][2]", "$['dogs'][0]", "$['dogs'][1]", "$['dogs'][2]"
|
211
|
+
# ]
|
210
212
|
```
|
211
213
|
|
212
214
|
##### #delete
|
@@ -222,6 +224,82 @@ The `#delete` method deletes matched values from the input.
|
|
222
224
|
# dog list is now []
|
223
225
|
```
|
224
226
|
|
227
|
+
##### #delete_if
|
228
|
+
|
229
|
+
The `#delete_if` method yields matched values to a block, and deletes them if the block returns a truthy result.
|
230
|
+
This allows values to be deleted based on conditions that can't be tested by a JSONPath query:
|
231
|
+
```ruby
|
232
|
+
# delete any book from the store json data that is not in the database
|
233
|
+
Janeway.enum_for('$.store.book.*', data).delete_if do |book|
|
234
|
+
results = db.query('SELECT * FROM books WHERE author=? AND title=?', book['author'], book['title'])
|
235
|
+
results.size == 0
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
##### #replace
|
240
|
+
|
241
|
+
Replaces every value matched by the query with the given value.
|
242
|
+
The replacement does not need to be the same type (eg. you can replace a string value with Hash or nil.)
|
243
|
+
|
244
|
+
Alternatively, provide a block which receives the value and returns a replacement value.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
# Set every price to nil
|
248
|
+
Janeway.enum_for('$..price', data).replace(nil)
|
249
|
+
|
250
|
+
# Suppose the prices were serialized as strings, eg. "price" => "9.99"
|
251
|
+
# Convert them to floating point values:
|
252
|
+
Janeway.enum_for('$..price', data).replace { |price| price.to_f }
|
253
|
+
|
254
|
+
# Same thing, but using more terse ruby:
|
255
|
+
Janeway.enum_for('$..price', data).replace(&:to_f)
|
256
|
+
|
257
|
+
# Convert price to hash:
|
258
|
+
Janeway.enum_for('$..price', data).replace do |price|
|
259
|
+
{
|
260
|
+
'price' => price.to_f,
|
261
|
+
'currency' => 'CAD',
|
262
|
+
}
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
##### #insert
|
267
|
+
|
268
|
+
Adds the given value to the input data, at the place specified by the JSONPath query.
|
269
|
+
|
270
|
+
The query must be a singular query, meaning it can only be made of of name selectors (hash keys) and index selectors (array indexes.)
|
271
|
+
The normalized paths which are yielded to `#each` are usable here.
|
272
|
+
Examples of singular queries:
|
273
|
+
```
|
274
|
+
$.store.book.0.price
|
275
|
+
$['store']['book'][0]
|
276
|
+
```
|
277
|
+
Examples of queries that are valid JSONPath but are not useable here:
|
278
|
+
```
|
279
|
+
$.store.book.*
|
280
|
+
$.store.book[1:2]
|
281
|
+
$.store.book[? @.price >= 8.99]
|
282
|
+
```
|
283
|
+
|
284
|
+
Additionally, some other restrictions apply:
|
285
|
+
* The "parent" node must exist, eg. for `$.a[1].name`, the path `$.a[1]` must exist and be a Hash
|
286
|
+
* Cannot create array index `n` unless the array contains exactly `n-1` elements
|
287
|
+
* If query path already exists, the block is called if provided. Otherwise an exception is raised.
|
288
|
+
|
289
|
+
Here is an example of adding a new book to the store:
|
290
|
+
```ruby
|
291
|
+
# Add a new book to the store
|
292
|
+
book_count = data['store']['book'].size
|
293
|
+
Janeway.enum_for("$.store.book[#{book_count}]", data).insert do
|
294
|
+
{ "category": "fiction",
|
295
|
+
"author": "Kameron Hurley",
|
296
|
+
"title": "The Light Brigade",
|
297
|
+
"price": 33.11
|
298
|
+
}
|
299
|
+
end
|
300
|
+
```
|
301
|
+
|
302
|
+
#### Ruby Enumerable module methods
|
225
303
|
|
226
304
|
The `Janeway.enum_for` and `Janeway::Query#enum_for` methods return an enumerator, so you can use the usual ruby enumerator methods, such as:
|
227
305
|
|
data/bin/janeway
CHANGED
@@ -15,25 +15,30 @@ HELP = <<~HELP_TEXT.freeze
|
|
15
15
|
#{SCRIPT_NAME} [QUERY] [FILENAME]
|
16
16
|
|
17
17
|
Purpose:
|
18
|
-
Print
|
18
|
+
Print values from the input data that match the given JSONPath query.
|
19
|
+
|
20
|
+
Alternatively, use one of the action flags to modify values from the input JSON specified by the query.
|
19
21
|
|
20
22
|
QUERY is a JSONPath query. Quote it with single quotes to avoid shell errors.
|
21
23
|
|
22
|
-
FILENAME is the path to a JSON file to use as input.
|
23
|
-
Alternately,
|
24
|
-
If
|
24
|
+
FILENAME is the path to a JSON file to use as input data.
|
25
|
+
Alternately, JSON can be provided on STDIN.
|
26
|
+
If data is not provided then the query is just checked for correctness.
|
25
27
|
|
26
28
|
For an introduction to JSONPath, see https://goessner.net/articles/JsonPath/
|
27
29
|
For the complete reference, see https://www.rfc-editor.org/info/rfc9535
|
28
30
|
|
29
31
|
Examples:
|
30
|
-
#{SCRIPT_NAME} '$.store.book[*].author'
|
31
|
-
cat
|
32
|
+
#{SCRIPT_NAME} '$.store.book[*].author' data.json
|
33
|
+
cat data.json | #{SCRIPT_NAME} '$.store.book[*].author'
|
32
34
|
|
33
35
|
HELP_TEXT
|
34
36
|
|
35
37
|
# Command-line options
|
36
|
-
Options = Struct.new(
|
38
|
+
Options = Struct.new(
|
39
|
+
:query, :query_file, :action, :value, :value_given, :data,
|
40
|
+
:compact_output, :verbose, keyword_init: true
|
41
|
+
)
|
37
42
|
|
38
43
|
# Parse the command-line arguments.
|
39
44
|
# This includes both bare words and hyphenated options.
|
@@ -44,9 +49,12 @@ def parse_args(argv)
|
|
44
49
|
argv << '--help' if argv.empty?
|
45
50
|
options = parse_options(argv)
|
46
51
|
|
47
|
-
#
|
52
|
+
# Get jsonpath query and input json
|
48
53
|
options.query = read_query(options.query_file, argv)
|
49
|
-
options.
|
54
|
+
options.data = read_input(argv.first)
|
55
|
+
if %i[insert replace].include?(options.action) && !options.value_given
|
56
|
+
abort("Need value for #{options.action}, use -v or -V")
|
57
|
+
end
|
50
58
|
options
|
51
59
|
end
|
52
60
|
|
@@ -54,14 +62,29 @@ end
|
|
54
62
|
#
|
55
63
|
# @param argv [Array<String>]
|
56
64
|
def parse_options(argv)
|
57
|
-
options = Options.new
|
65
|
+
options = Options.new(action: :search)
|
58
66
|
op = OptionParser.new do |opts|
|
59
67
|
opts.banner = HELP
|
60
68
|
opts.separator('Options:')
|
61
69
|
|
62
70
|
opts.on('-q', '--query FILE', 'Read jsonpath query from file') { |o| options.query_file = o }
|
63
|
-
opts.on('-c', '--compact', '
|
64
|
-
opts.on('-
|
71
|
+
opts.on('-c', '--compact', 'Print result in compact json format') { options.compact_output = true }
|
72
|
+
opts.on('-v', '--value STRING', 'VALUE for insert or replace (str/number/null/true/false)') do |o|
|
73
|
+
options.value = read_value(o)
|
74
|
+
options.value_given = true # must track this separately since value may be false / nil
|
75
|
+
end
|
76
|
+
desc =
|
77
|
+
'JSON file containing VALUE for insert or replace ' \
|
78
|
+
'(bare literals are valid JSON, eg. 9, "string", null)'
|
79
|
+
opts.on('-V', '--value-file FILENAME', desc) do |o|
|
80
|
+
options.value = read_value_file(o)
|
81
|
+
options.value_given = true
|
82
|
+
end
|
83
|
+
opts.separator('Actions:')
|
84
|
+
opts.on('-d', '--delete', 'Print data, with matches deleted') { options.action = :delete }
|
85
|
+
opts.on('-r', '--replace', 'Print data, with matches replaced by VALUE') { options.action = :replace }
|
86
|
+
opts.on('-i', '--insert', 'Print data, with VALUE inserted at query location') { options.action = :insert }
|
87
|
+
opts.on('-p', '--paths', 'List normalized path for each match') { options.action = :paths }
|
65
88
|
opts.on('--version', 'Show version number') { abort(Janeway::VERSION) }
|
66
89
|
opts.on('-h', '--help', 'Show this help message') { abort(opts.to_s) }
|
67
90
|
end
|
@@ -99,43 +122,69 @@ def read_input(path)
|
|
99
122
|
parse_json(json)
|
100
123
|
end
|
101
124
|
|
125
|
+
# Read insert/replacement value from file
|
126
|
+
# @return [Array,Hash,String,Numeric,nil]
|
127
|
+
def read_value_file(path)
|
128
|
+
abort("File not readable: #{path}") unless File.exist?(path)
|
129
|
+
|
130
|
+
parse_json File.read(path)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Read insert/replacement value from command-line argument
|
134
|
+
# @return [Integer, Float, String]
|
135
|
+
def read_value(value_str)
|
136
|
+
case value_str
|
137
|
+
when 'null' then nil
|
138
|
+
when 'true' then true
|
139
|
+
when 'false' then false
|
140
|
+
when /^\d+$/ then value_str.to_i # integer
|
141
|
+
when /^\d+\.\d+$/ then value_str.to_f # float
|
142
|
+
else value_str # string
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
102
146
|
# Parse JSON, and abort if it is invalid
|
103
147
|
# @param json [String]
|
104
148
|
# @return [Hash, Array] un-serialized json, as ruby objects
|
105
149
|
def parse_json(json)
|
106
150
|
JSON.parse(json)
|
107
151
|
rescue JSON::JSONError => e
|
108
|
-
# JSON error messages may include the entire
|
152
|
+
# JSON error messages may include the entire data, so limit how much is printed.
|
109
153
|
msg = e.message[0..256]
|
110
154
|
msg += "\n..." if e.message.length > 256
|
111
|
-
abort "Input is not valid JSON: #{msg}"
|
155
|
+
abort "Input data is not valid JSON: #{msg}"
|
112
156
|
end
|
113
157
|
|
114
158
|
# Just pares the query ane then exit.
|
115
159
|
# Useful for testing whether a query is valid.
|
116
160
|
# @param query [String] jsonpath
|
117
161
|
def parse_query_and_exit(query)
|
118
|
-
Janeway.
|
162
|
+
Janeway.parse(query)
|
119
163
|
puts 'Query is valid. Provide input json to run the query.'
|
120
164
|
exit(0)
|
121
165
|
end
|
122
166
|
|
123
167
|
# @param options [Options]
|
124
168
|
def main(options)
|
125
|
-
parse_query_and_exit(options.query) unless options.
|
126
|
-
|
127
|
-
enum = Janeway.enum_for(options.query, options.
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
169
|
+
parse_query_and_exit(options.query) unless options.data
|
170
|
+
|
171
|
+
enum = Janeway.enum_for(options.query, options.data)
|
172
|
+
results =
|
173
|
+
case options.action
|
174
|
+
when :search then enum.search
|
175
|
+
when :paths then enum.map { |_, _, _, path| path }
|
176
|
+
when :replace
|
177
|
+
enum.replace(options.value)
|
178
|
+
options.data
|
179
|
+
when :insert
|
180
|
+
enum.insert(options.value)
|
181
|
+
options.data
|
182
|
+
when :delete
|
183
|
+
enum.delete
|
184
|
+
options.data
|
132
185
|
else
|
133
|
-
|
186
|
+
raise "Unknown action: #{options.action}"
|
134
187
|
end
|
135
|
-
exit(0)
|
136
|
-
else
|
137
|
-
results = enum.search
|
138
|
-
end
|
139
188
|
|
140
189
|
if options.compact_output
|
141
190
|
puts JSON.generate(results)
|
@@ -149,7 +198,7 @@ begin
|
|
149
198
|
main(options)
|
150
199
|
rescue Janeway::Error => e
|
151
200
|
warn "Error: #{e.message}\nQuery: #{e.query}\n"
|
152
|
-
warn " #{' ' * e.location.col}^" if e.location
|
201
|
+
warn " #{' ' * e.location.col}^" if e.location # point to the character at the location index
|
153
202
|
exit(1)
|
154
203
|
rescue Interrupt, Errno::EPIPE
|
155
204
|
abort("\n")
|
@@ -2,16 +2,17 @@
|
|
2
2
|
|
3
3
|
require 'forwardable'
|
4
4
|
|
5
|
-
# A set of selectors within brackets, as a comma-separated list.
|
6
|
-
# https://www.rfc-editor.org/rfc/rfc9535.html#child-segment
|
7
|
-
#
|
8
|
-
# @example
|
9
|
-
# $[*, *]
|
10
|
-
# $[1, 2, 3]
|
11
|
-
# $[name1, [1:10]]
|
12
5
|
module Janeway
|
13
6
|
module AST
|
14
7
|
# Represent a union of 2 or more selectors.
|
8
|
+
#
|
9
|
+
# A set of selectors within brackets, as a comma-separated list.
|
10
|
+
# https://www.rfc-editor.org/rfc/rfc9535.html#child-segment
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# $[*, *]
|
14
|
+
# $[1, 2, 3]
|
15
|
+
# $[name1, [1:10]]
|
15
16
|
class ChildSegment < Janeway::AST::Expression
|
16
17
|
extend Forwardable
|
17
18
|
def_delegators :@value, :size, :first, :last, :each, :map, :empty?
|
data/lib/janeway/ast/function.rb
CHANGED
data/lib/janeway/ast/selector.rb
CHANGED
@@ -2,27 +2,26 @@
|
|
2
2
|
|
3
3
|
require_relative 'expression'
|
4
4
|
|
5
|
-
# https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base/blob/main/draft-ietf-jsonpath-base.md#selectors
|
6
|
-
# Selectors:
|
7
|
-
#
|
8
|
-
# A name selector, e.g. 'name', selects a named child of an object.
|
9
|
-
#
|
10
|
-
# An index selector, e.g. 3, selects an indexed child of an array.
|
11
|
-
#
|
12
|
-
# A wildcard * ({{wildcard-selector}}) in the expression [*] selects all
|
13
|
-
# children of a node and in the expression ..[*] selects all descendants of a
|
14
|
-
# node.
|
15
|
-
#
|
16
|
-
# An array slice start:end:step ({{slice}}) selects a series of elements from
|
17
|
-
# an array, giving a start position, an end position, and an optional step
|
18
|
-
# value that moves the position from the start to the end.
|
19
|
-
#
|
20
|
-
# Filter expressions ?<logical-expr> select certain children of an object or array, as in:
|
21
|
-
#
|
22
|
-
# $.store.book[?@.price < 10].title
|
23
5
|
module Janeway
|
24
6
|
module AST
|
25
7
|
# Represent a selector, which is an expression that filters nodes from a list based on a predicate.
|
8
|
+
#
|
9
|
+
# https://www.rfc-editor.org/rfc/rfc9535.html#name-selectors
|
10
|
+
# Selectors:
|
11
|
+
#
|
12
|
+
# A name selector, e.g. 'name', selects a named child of an object.
|
13
|
+
#
|
14
|
+
# An index selector, e.g. 3, selects an indexed child of an array.
|
15
|
+
#
|
16
|
+
# A wildcard * in the expression [*] selects all children of a node and in
|
17
|
+
# the expression ..[*] selects all descendants of a node.
|
18
|
+
#
|
19
|
+
# An array slice start:end:step selects a series of elements from an array,
|
20
|
+
# giving a start position, an end position, and an optional step value that
|
21
|
+
# moves the position from the start to the end.
|
22
|
+
#
|
23
|
+
# Filter expressions ?<logical-expr> select certain children of an object or array, as in:
|
24
|
+
# $.store.book[?@.price < 10].title
|
26
25
|
class Selector < Janeway::AST::Expression
|
27
26
|
# Subsequent expression that modifies the result of this selector list.
|
28
27
|
attr_accessor :next
|
data/lib/janeway/ast.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Janeway
|
4
|
+
# Abstract Syntax Tree
|
5
|
+
module AST
|
6
|
+
# These are the limits of what javascript's Number type can represent
|
7
|
+
INTEGER_MIN = -9_007_199_254_740_991
|
8
|
+
INTEGER_MAX = 9_007_199_254_740_991
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
require_relative 'ast/array_slice_selector'
|
13
|
+
require_relative 'ast/binary_operator'
|
14
|
+
require_relative 'ast/boolean'
|
15
|
+
require_relative 'ast/child_segment'
|
16
|
+
require_relative 'ast/current_node'
|
17
|
+
require_relative 'ast/descendant_segment'
|
18
|
+
require_relative 'ast/error'
|
19
|
+
require_relative 'ast/expression'
|
20
|
+
require_relative 'ast/filter_selector'
|
21
|
+
require_relative 'ast/function'
|
22
|
+
require_relative 'ast/helpers'
|
23
|
+
require_relative 'ast/index_selector'
|
24
|
+
require_relative 'ast/name_selector'
|
25
|
+
require_relative 'ast/null'
|
26
|
+
require_relative 'ast/number'
|
27
|
+
require_relative 'ast/root_node'
|
28
|
+
require_relative 'ast/selector'
|
29
|
+
require_relative 'ast/string_type'
|
30
|
+
require_relative 'ast/unary_operator'
|
31
|
+
require_relative 'ast/wildcard_selector'
|
data/lib/janeway/enumerator.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'interpreter'
|
4
|
+
|
3
5
|
module Janeway
|
4
|
-
# Enumerator combines a
|
5
|
-
# It provides
|
6
|
+
# Enumerator combines a JSONPath query with input.
|
7
|
+
# It provides methods for running queries on the input and enumerating over the results.
|
6
8
|
class Enumerator
|
7
9
|
include Enumerable
|
8
10
|
|
9
|
-
# @
|
11
|
+
# @return [Janeway::Query]
|
12
|
+
attr_reader :query
|
13
|
+
|
14
|
+
# @param query [Janeway::Query]
|
15
|
+
# @param input [Array, Hash]
|
10
16
|
def initialize(query, input)
|
11
17
|
@query = query
|
12
18
|
@input = input
|
@@ -34,10 +40,151 @@ module Janeway
|
|
34
40
|
Janeway::Interpreter.new(@query, as: :iterator, &block).interpret(@input)
|
35
41
|
end
|
36
42
|
|
37
|
-
# Delete
|
38
|
-
# @return [Array
|
43
|
+
# Delete values from the input that are matched by the JSONPath query.
|
44
|
+
# @return [Array] deleted values
|
39
45
|
def delete
|
40
46
|
Janeway::Interpreter.new(@query, as: :deleter).interpret(@input)
|
41
47
|
end
|
48
|
+
|
49
|
+
# Delete values from the input that are matched by the JSONPath query and also return a truthy value
|
50
|
+
# from the block.
|
51
|
+
# @return [Array] deleted values
|
52
|
+
def delete_if(&block)
|
53
|
+
Janeway::Interpreter.new(@query, as: :delete_if, &block).interpret(@input)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Assign the given value at every query match.
|
57
|
+
# @param replacement [Object]
|
58
|
+
# @return [void]
|
59
|
+
def replace(replacement = :no_replacement_value_was_given, &block)
|
60
|
+
if replacement != :no_replacement_value_was_given && block_given?
|
61
|
+
raise Janeway::Error.new('#replace needs either replacement value or block, not both', @query)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Avoid infinite loop when the replacement data would also be matched by the query
|
65
|
+
previous = []
|
66
|
+
each do |_, parent, key|
|
67
|
+
# Update the previous match
|
68
|
+
unless previous.empty?
|
69
|
+
prev_parent, prev_key = *previous
|
70
|
+
prev_parent[prev_key] = block_given? ? yield(prev_parent[prev_key]) : replacement
|
71
|
+
end
|
72
|
+
|
73
|
+
# Record this match to be updated later
|
74
|
+
previous = [parent, key]
|
75
|
+
end
|
76
|
+
return if previous.empty?
|
77
|
+
|
78
|
+
# Update the final match
|
79
|
+
prev_parent, prev_key = *previous
|
80
|
+
prev_parent[prev_key] = block_given? ? yield(prev_parent[prev_key]) : replacement
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Insert `value` into the input at a location specified by a singular query.
|
85
|
+
#
|
86
|
+
# This has restrictions:
|
87
|
+
# * Only for singular queries
|
88
|
+
# (no wildcards, filter expressions, child segments, etc.)
|
89
|
+
# * The "parent" node must exist
|
90
|
+
# eg. for $.a[1].name, the path $.a[1] must exist and be a Hash
|
91
|
+
# * Cannot create array index N unless the array
|
92
|
+
# already has exactly N-1 elements
|
93
|
+
# * If query path already exists, the block is called if provided.
|
94
|
+
# Otherwise an exception is raised.
|
95
|
+
#
|
96
|
+
# Optionally, pass in a block to be called when there is already a value at the
|
97
|
+
# specified array index / hash key. The parent object and the array index
|
98
|
+
# or hash key will be yielded to the block.
|
99
|
+
#
|
100
|
+
# @yieldparam [Array, Hash] parent object
|
101
|
+
# @yieldparam [Intgeer, String] array index or hash key
|
102
|
+
def insert(value, &block)
|
103
|
+
# Query must point to a single target object
|
104
|
+
unless @query.singular_query?
|
105
|
+
msg = 'Janeway::Query#insert may only be used with a singular query'
|
106
|
+
raise Janeway::Error.new(msg, @query)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Find parent of new value
|
110
|
+
parent, key, parent_path = find_parent
|
111
|
+
|
112
|
+
# Insert new value into parent
|
113
|
+
case parent
|
114
|
+
when Hash then insert_into_hash(parent, key, value, parent_path, &block)
|
115
|
+
when Array then insert_into_array(parent, key, value, parent_path, &block)
|
116
|
+
else
|
117
|
+
raise Error.new("cannot insert into basic type: #{parent.inspect}", @query.to_s)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Find the 'parent' of the object pointed to by the query. For singular query only.
|
124
|
+
#
|
125
|
+
# @!visibility private
|
126
|
+
# @return [Object, Object, String] parent object (Hash / Array), key/index (String / Integer), path to parent
|
127
|
+
def find_parent
|
128
|
+
# Make a Query that points to the target's parent
|
129
|
+
parent_query = @query.dup
|
130
|
+
selector = parent_query.pop # returns a name or index selector
|
131
|
+
|
132
|
+
# Find parent element, give up if parent does not exist
|
133
|
+
results = Interpreter.new(parent_query).interpret(@input)
|
134
|
+
case results.size
|
135
|
+
when 0 then raise "no parent found for #{@query}, cannot insert value"
|
136
|
+
when 1 then parent = results.first
|
137
|
+
else raise "query #{parent_query} matched multiple elements!" # not possible for singular query
|
138
|
+
end
|
139
|
+
|
140
|
+
[parent, selector.value, parent_query.to_s]
|
141
|
+
end
|
142
|
+
|
143
|
+
# Insert value into hash at the given key
|
144
|
+
#
|
145
|
+
# @!visibility private
|
146
|
+
# @param hash [Hash]
|
147
|
+
# @param key [String]
|
148
|
+
# @param value [Object]
|
149
|
+
# @param path [String] jsonpath singular query to parent
|
150
|
+
# @yieldparam [Hash] parent object
|
151
|
+
# @yieldparam [String] hash key
|
152
|
+
def insert_into_hash(hash, key, value, path, &block)
|
153
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
154
|
+
raise Error.new("cannot use #{key.inspect} as hash key", @query.to_s)
|
155
|
+
end
|
156
|
+
|
157
|
+
if hash.key?(key)
|
158
|
+
raise Error.new("hash at #{path} already has key #{key.inspect}", @query.to_s) unless block_given?
|
159
|
+
|
160
|
+
yield hash, key
|
161
|
+
end
|
162
|
+
|
163
|
+
hash[key] = value
|
164
|
+
end
|
165
|
+
|
166
|
+
# Insert value into array at the given index
|
167
|
+
#
|
168
|
+
# @!visibility private
|
169
|
+
# @param array [Array]
|
170
|
+
# @param index [Integer]
|
171
|
+
# @param value [Object]
|
172
|
+
# @param path [String] jsonpath singular query to parent
|
173
|
+
# @yieldparam [Array] parent object
|
174
|
+
# @yieldparam [Integer] array index
|
175
|
+
def insert_into_array(array, index, value, path, &block)
|
176
|
+
raise Error.new("cannot use #{index.inspect} as array index", @query.to_s) unless index.is_a?(Integer)
|
177
|
+
|
178
|
+
if index < array.size
|
179
|
+
raise Error.new("array at #{path} already has index #{index}", @query.to_s) unless block_given?
|
180
|
+
|
181
|
+
yield array, index
|
182
|
+
end
|
183
|
+
if index > array.size
|
184
|
+
raise Error.new("cannot add index #{index} because array at #{path} is too small", @query.to_s)
|
185
|
+
end
|
186
|
+
|
187
|
+
array << value
|
188
|
+
end
|
42
189
|
end
|
43
190
|
end
|
data/lib/janeway/interpreter.rb
CHANGED
@@ -11,6 +11,7 @@ module Janeway
|
|
11
11
|
# It should be created for a single query and then discarded.
|
12
12
|
class Interpreter
|
13
13
|
attr_reader :jsonpath, :output
|
14
|
+
|
14
15
|
include Interpreters
|
15
16
|
|
16
17
|
# Interpret a query on the given input, return result
|
@@ -25,7 +26,7 @@ module Janeway
|
|
25
26
|
# @param query [Query] abstract syntax tree of the jsonpath query
|
26
27
|
def initialize(query, as: :finder, &block)
|
27
28
|
raise ArgumentError, "expect Query, got #{query.inspect}" unless query.is_a?(Query)
|
28
|
-
unless %i[finder iterator deleter].include?(as)
|
29
|
+
unless %i[finder iterator deleter delete_if].include?(as)
|
29
30
|
raise ArgumentError, "invalid interpreter type: #{as.inspect}"
|
30
31
|
end
|
31
32
|
|
@@ -80,6 +81,7 @@ module Janeway
|
|
80
81
|
case @type
|
81
82
|
when :iterator then interpreters.push(Yielder.new(&block))
|
82
83
|
when :deleter then interpreters.push make_deleter(interpreters.pop)
|
84
|
+
when :delete_if then interpreters.push make_delete_if(interpreters.pop, &block)
|
83
85
|
end
|
84
86
|
|
85
87
|
# Link interpreters together
|
@@ -114,6 +116,14 @@ module Janeway
|
|
114
116
|
TreeConstructor.ast_node_to_deleter(interpreter.node)
|
115
117
|
end
|
116
118
|
|
119
|
+
# Make a DeleteIf that will delete the results matched by a Selector,
|
120
|
+
# after yielding to a block for approval.
|
121
|
+
#
|
122
|
+
# @param interpreter [Interpreters::Base] interpeter subclass
|
123
|
+
def make_delete_if(interpreter, &block)
|
124
|
+
TreeConstructor.ast_node_to_delete_if(interpreter.node, &block)
|
125
|
+
end
|
126
|
+
|
117
127
|
# Return an Interpreter::Error with the specified message, include the query.
|
118
128
|
#
|
119
129
|
# @param msg [String] error message
|