jsonpath 1.0.7 → 1.1.2
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/.github/workflows/test.yml +31 -0
- data/.gitignore +2 -0
- data/Gemfile +1 -1
- data/README.md +133 -37
- data/jsonpath.gemspec +1 -0
- data/lib/jsonpath/dig.rb +57 -0
- data/lib/jsonpath/enumerable.rb +9 -19
- data/lib/jsonpath/parser.rb +10 -5
- data/lib/jsonpath/version.rb +1 -1
- data/lib/jsonpath.rb +46 -4
- data/test/test_jsonpath.rb +272 -11
- data/test/test_readme.rb +117 -0
- metadata +23 -7
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab3e08744fe7f8ba3b64670fee8f75a0fa4c9b339de8257f64cdc7ea24f26965
|
4
|
+
data.tar.gz: 447ce47a7c5df36c2fa290da5aafc77304d5e8db9318cfcac2b97dd9ae8c9c8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b458262f098ceea595a91a981136141d0ed3f7ef9782d8a205f619be1bb60a7234af11229527ed9fc9ef3baafec323ad33850d87b4dc389ac482c14bb4aa05f
|
7
|
+
data.tar.gz: d1756aa04d5e07de855439aa0a4895078ab5c3c2c90c4c9ec755ba1da2ffb3fe11e59d1819e03f6a82e8670fd810b1c895da150a9d24176ddf4a86a54705fcbf
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: test
|
2
|
+
on:
|
3
|
+
- push
|
4
|
+
- pull_request
|
5
|
+
jobs:
|
6
|
+
test:
|
7
|
+
strategy:
|
8
|
+
fail-fast: false
|
9
|
+
matrix:
|
10
|
+
ruby-version:
|
11
|
+
- '2.5'
|
12
|
+
- '2.6'
|
13
|
+
- '2.7'
|
14
|
+
- ruby-head
|
15
|
+
- jruby-head
|
16
|
+
- truffleruby-head
|
17
|
+
runs-on:
|
18
|
+
- ubuntu-latest
|
19
|
+
|
20
|
+
runs-on: ${{ matrix.runs-on }}
|
21
|
+
|
22
|
+
steps:
|
23
|
+
|
24
|
+
- uses: actions/checkout@v2
|
25
|
+
|
26
|
+
- uses: ruby/setup-ruby@v1
|
27
|
+
with:
|
28
|
+
ruby-version: ${{ matrix.ruby-version }}
|
29
|
+
bundler-cache: true
|
30
|
+
|
31
|
+
- run: bundle exec rake test
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,8 +4,8 @@ This is an implementation of http://goessner.net/articles/JsonPath/.
|
|
4
4
|
|
5
5
|
## What is JsonPath?
|
6
6
|
|
7
|
-
JsonPath is a way of addressing elements within a JSON object. Similar to xpath
|
8
|
-
traverse a json object and manipulate or access it.
|
7
|
+
JsonPath is a way of addressing elements within a JSON object. Similar to xpath
|
8
|
+
of yore, JsonPath lets you traverse a json object and manipulate or access it.
|
9
9
|
|
10
10
|
## Usage
|
11
11
|
|
@@ -15,8 +15,8 @@ There is stand-alone usage through the binary `jsonpath`
|
|
15
15
|
|
16
16
|
jsonpath [expression] (file|string)
|
17
17
|
|
18
|
-
If you omit the second argument, it will read stdin, assuming one valid JSON
|
19
|
-
per line. Expression must be a valid jsonpath expression.
|
18
|
+
If you omit the second argument, it will read stdin, assuming one valid JSON
|
19
|
+
object per line. Expression must be a valid jsonpath expression.
|
20
20
|
|
21
21
|
### Library
|
22
22
|
|
@@ -40,8 +40,8 @@ json = <<-HERE_DOC
|
|
40
40
|
HERE_DOC
|
41
41
|
```
|
42
42
|
|
43
|
-
Now that we have a JSON object, let's get all the prices present in the object.
|
44
|
-
in the following way.
|
43
|
+
Now that we have a JSON object, let's get all the prices present in the object.
|
44
|
+
We create an object for the path in the following way.
|
45
45
|
|
46
46
|
```ruby
|
47
47
|
path = JsonPath.new('$..price')
|
@@ -54,14 +54,15 @@ path.on(json)
|
|
54
54
|
# => [19.95, 8.95, 12.99, 8.99, 22.99]
|
55
55
|
```
|
56
56
|
|
57
|
-
Or on some other object ...
|
57
|
+
Or reuse it later on some other object (thread safe) ...
|
58
58
|
|
59
59
|
```ruby
|
60
60
|
path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}')
|
61
61
|
# => [18.88]
|
62
62
|
```
|
63
63
|
|
64
|
-
You can also just combine this into one mega-call with the convenient
|
64
|
+
You can also just combine this into one mega-call with the convenient
|
65
|
+
`JsonPath.on` method.
|
65
66
|
|
66
67
|
```ruby
|
67
68
|
JsonPath.on(json, '$..author')
|
@@ -73,29 +74,36 @@ Of course the full JsonPath syntax is supported, such as array slices
|
|
73
74
|
```ruby
|
74
75
|
JsonPath.new('$..book[::2]').on(json)
|
75
76
|
# => [
|
76
|
-
# {"price"=>8.95, "category"=>"reference", "
|
77
|
-
# {"price"=>8.99, "category"=>"fiction", "
|
77
|
+
# {"price" => 8.95, "category" => "reference", "title" => "Sayings of the Century", "author" => "Nigel Rees"},
|
78
|
+
# {"price" => 8.99, "category" => "fiction", "isbn" => "0-553-21311-3", "title" => "Moby Dick", "author" => "Herman Melville","color" => "blue"},
|
78
79
|
# ]
|
79
80
|
```
|
80
81
|
|
81
|
-
...and evals
|
82
|
+
...and evals, including those with conditional operators
|
82
83
|
|
83
84
|
```ruby
|
84
|
-
JsonPath.new(
|
85
|
+
JsonPath.new("$..price[?(@ < 10)]").on(json)
|
85
86
|
# => [8.95, 8.99]
|
87
|
+
|
88
|
+
JsonPath.new("$..book[?(@['price'] == 8.95 || @['price'] == 8.99)].title").on(json)
|
89
|
+
# => ["Sayings of the Century", "Moby Dick"]
|
90
|
+
|
91
|
+
JsonPath.new("$..book[?(@['price'] == 8.95 && @['price'] == 8.99)].title").on(json)
|
92
|
+
# => []
|
86
93
|
```
|
87
94
|
|
88
|
-
There is a convenience method, `#first` that gives you the first element for a
|
95
|
+
There is a convenience method, `#first` that gives you the first element for a
|
96
|
+
JSON object and path.
|
89
97
|
|
90
98
|
```ruby
|
91
|
-
JsonPath.new('$..color').first(
|
99
|
+
JsonPath.new('$..color').first(json)
|
92
100
|
# => "red"
|
93
101
|
```
|
94
102
|
|
95
103
|
As well, we can directly create an `Enumerable` at any time using `#[]`.
|
96
104
|
|
97
105
|
```ruby
|
98
|
-
enum = JsonPath.new('$..color')[
|
106
|
+
enum = JsonPath.new('$..color')[json]
|
99
107
|
# => #<JsonPath::Enumerable:...>
|
100
108
|
enum.first
|
101
109
|
# => "red"
|
@@ -103,29 +111,77 @@ enum.any?{ |c| c == 'red' }
|
|
103
111
|
# => true
|
104
112
|
```
|
105
113
|
|
106
|
-
|
114
|
+
For more usage examples and variations on paths, please visit the tests. There
|
115
|
+
are some more complex ones as well.
|
116
|
+
|
117
|
+
### Querying ruby data structures
|
118
|
+
|
119
|
+
If you have ruby hashes with symbolized keys as input, you
|
120
|
+
can use `:use_symbols` to make JsonPath work fine on them too:
|
107
121
|
|
108
|
-
|
122
|
+
```ruby
|
123
|
+
book = { title: "Sayings of the Century" }
|
124
|
+
|
125
|
+
JsonPath.new('$.title').on(book)
|
126
|
+
# => []
|
127
|
+
|
128
|
+
JsonPath.new('$.title', use_symbols: true).on(book)
|
129
|
+
# => ["Sayings of the Century"]
|
130
|
+
```
|
109
131
|
|
110
|
-
|
132
|
+
JsonPath also recognizes objects responding to `dig` (introduced
|
133
|
+
in ruby 2.3), and therefore works out of the box with Struct,
|
134
|
+
OpenStruct, and other Hash-like structures:
|
111
135
|
|
112
136
|
```ruby
|
113
|
-
|
114
|
-
|
115
|
-
end
|
137
|
+
book_class = Struct.new(:title)
|
138
|
+
book = book_class.new("Sayings of the Century")
|
116
139
|
|
117
|
-
|
118
|
-
|
119
|
-
|
140
|
+
JsonPath.new('$.title').on(book)
|
141
|
+
# => ["Sayings of the Century"]
|
142
|
+
```
|
143
|
+
|
144
|
+
JsonPath is able to query pure ruby objects and uses `__send__`
|
145
|
+
on them. The option is enabled by default in JsonPath 1.x, but
|
146
|
+
we encourage to enable it explicitly:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
book_class = Class.new{ attr_accessor :title }
|
150
|
+
book = book_class.new
|
151
|
+
book.title = "Sayings of the Century"
|
120
152
|
|
121
|
-
|
122
|
-
|
123
|
-
|
153
|
+
JsonPath.new('$.title', allow_send: true).on(book)
|
154
|
+
# => ["Sayings of the Century"]
|
155
|
+
```
|
156
|
+
|
157
|
+
### Other available options
|
158
|
+
|
159
|
+
By default, JsonPath does not return null values on unexisting paths.
|
160
|
+
This can be changed using the `:default_path_leaf_to_null` option
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
JsonPath.new('$..book[*].isbn').on(json)
|
164
|
+
# => ["0-553-21311-3", "0-395-19395-8"]
|
165
|
+
|
166
|
+
JsonPath.new('$..book[*].isbn', default_path_leaf_to_null: true).on(json)
|
167
|
+
# => [nil, nil, "0-553-21311-3", "0-395-19395-8"]
|
168
|
+
```
|
169
|
+
|
170
|
+
When JsonPath returns a Hash, you can ask to symbolize its keys
|
171
|
+
using the `:symbolize_keys` option
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
JsonPath.new('$..book[0]').on(json)
|
175
|
+
# => [{"category" => "reference", ...}]
|
176
|
+
|
177
|
+
JsonPath.new('$..book[0]', symbolize_keys: true).on(json)
|
178
|
+
# => [{category: "reference", ...}]
|
124
179
|
```
|
125
180
|
|
126
181
|
### Selecting Values
|
127
182
|
|
128
|
-
It's possible to select results once a query has been defined after the query. For
|
183
|
+
It's possible to select results once a query has been defined after the query. For
|
184
|
+
example given this JSON data:
|
129
185
|
|
130
186
|
```bash
|
131
187
|
{
|
@@ -168,15 +224,10 @@ It's possible to select results once a query has been defined after the query. F
|
|
168
224
|
]
|
169
225
|
```
|
170
226
|
|
171
|
-
### Running an individual test
|
172
|
-
|
173
|
-
```ruby
|
174
|
-
ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6
|
175
|
-
```
|
176
|
-
|
177
227
|
### Manipulation
|
178
228
|
|
179
|
-
If you'd like to do substitution in a json object, you can use `#gsub`
|
229
|
+
If you'd like to do substitution in a json object, you can use `#gsub`
|
230
|
+
or `#gsub!` to modify the object in place.
|
180
231
|
|
181
232
|
```ruby
|
182
233
|
JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash
|
@@ -188,7 +239,9 @@ The result will be
|
|
188
239
|
{'candy' => 'big turks'}
|
189
240
|
```
|
190
241
|
|
191
|
-
If you'd like to remove all nil keys, you can use `#compact` and `#compact!`.
|
242
|
+
If you'd like to remove all nil keys, you can use `#compact` and `#compact!`.
|
243
|
+
To remove all keys under a certain path, use `#delete` or `#delete!`. You can
|
244
|
+
even chain these methods together as follows:
|
192
245
|
|
193
246
|
```ruby
|
194
247
|
json = '{"candy":"lollipop","noncandy":null,"other":"things"}'
|
@@ -200,6 +253,49 @@ o = JsonPath.for(json).
|
|
200
253
|
# => {"candy" => "big turks"}
|
201
254
|
```
|
202
255
|
|
256
|
+
### Fetch all paths
|
257
|
+
|
258
|
+
To fetch all possible paths in given json, you can use `fetch_all_paths` method.
|
259
|
+
|
260
|
+
data:
|
261
|
+
|
262
|
+
```bash
|
263
|
+
{
|
264
|
+
"store": {
|
265
|
+
"book": [
|
266
|
+
{
|
267
|
+
"category": "reference",
|
268
|
+
"author": "Nigel Rees"
|
269
|
+
},
|
270
|
+
{
|
271
|
+
"category": "fiction",
|
272
|
+
"author": "Evelyn Waugh"
|
273
|
+
}
|
274
|
+
]
|
275
|
+
}
|
276
|
+
```
|
277
|
+
|
278
|
+
... and this query:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
JsonPath.fetch_all_path(data)
|
282
|
+
```
|
283
|
+
|
284
|
+
... the result will be:
|
285
|
+
|
286
|
+
```bash
|
287
|
+
["$", "$.store", "$.store.book", "$.store.book[0].category", "$.store.book[0].author", "$.store.book[0]", "$.store.book[1].category", "$.store.book[1].author", "$.store.book[1]"]
|
288
|
+
```
|
289
|
+
|
290
|
+
|
291
|
+
|
203
292
|
# Contributions
|
204
293
|
|
205
|
-
Please feel free to submit an Issue or a Pull Request any time you feel like
|
294
|
+
Please feel free to submit an Issue or a Pull Request any time you feel like
|
295
|
+
you would like to contribute. Thank you!
|
296
|
+
|
297
|
+
## Running an individual test
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6
|
301
|
+
```
|
data/jsonpath.gemspec
CHANGED
data/lib/jsonpath/dig.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class JsonPath
|
4
|
+
module Dig
|
5
|
+
|
6
|
+
# Similar to what Hash#dig or Array#dig
|
7
|
+
def dig(context, *keys)
|
8
|
+
keys.inject(context){|memo,k|
|
9
|
+
dig_one(memo, k)
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns a hash mapping each key from keys
|
14
|
+
# to its dig value on context.
|
15
|
+
def dig_as_hash(context, keys)
|
16
|
+
keys.each_with_object({}) do |k, memo|
|
17
|
+
memo[k] = dig_one(context, k)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Dig the value of k on context.
|
22
|
+
def dig_one(context, k)
|
23
|
+
case context
|
24
|
+
when Hash
|
25
|
+
context[@options[:use_symbols] ? k.to_sym : k]
|
26
|
+
when Array
|
27
|
+
context[k.to_i]
|
28
|
+
else
|
29
|
+
if context.respond_to?(:dig)
|
30
|
+
context.dig(k)
|
31
|
+
elsif @options[:allow_send]
|
32
|
+
context.__send__(k)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Yields the block if context has a diggable
|
38
|
+
# value for k
|
39
|
+
def yield_if_diggable(context, k, &blk)
|
40
|
+
case context
|
41
|
+
when Array
|
42
|
+
nil
|
43
|
+
when Hash
|
44
|
+
k = @options[:use_symbols] ? k.to_sym : k
|
45
|
+
return yield if context.key?(k) || @options[:default_path_leaf_to_null]
|
46
|
+
else
|
47
|
+
if context.respond_to?(:dig)
|
48
|
+
digged = dig_one(context, k)
|
49
|
+
yield if !digged.nil? || @options[:default_path_leaf_to_null]
|
50
|
+
elsif @options[:allow_send] && context.respond_to?(k.to_s) && !Object.respond_to?(k.to_s)
|
51
|
+
yield
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/lib/jsonpath/enumerable.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
class JsonPath
|
4
4
|
class Enumerable
|
5
5
|
include ::Enumerable
|
6
|
+
include Dig
|
6
7
|
|
7
8
|
def initialize(path, object, mode, options = {})
|
8
9
|
@path = path.path
|
@@ -12,12 +13,7 @@ class JsonPath
|
|
12
13
|
end
|
13
14
|
|
14
15
|
def each(context = @object, key = nil, pos = 0, &blk)
|
15
|
-
node =
|
16
|
-
if key
|
17
|
-
context.is_a?(Hash) || context.is_a?(Array) ? context[key] : context.__send__(key)
|
18
|
-
else
|
19
|
-
context
|
20
|
-
end
|
16
|
+
node = key ? dig_one(context, key) : context
|
21
17
|
@_current_node = node
|
22
18
|
return yield_value(blk, context, key) if pos == @path.size
|
23
19
|
|
@@ -47,11 +43,10 @@ class JsonPath
|
|
47
43
|
def filter_context(context, keys)
|
48
44
|
case context
|
49
45
|
when Hash
|
50
|
-
|
51
|
-
context.select { |k| keys.include?(k) }
|
46
|
+
dig_as_hash(context, keys)
|
52
47
|
when Array
|
53
48
|
context.each_with_object([]) do |c, memo|
|
54
|
-
memo << c
|
49
|
+
memo << dig_as_hash(c, keys)
|
55
50
|
end
|
56
51
|
end
|
57
52
|
end
|
@@ -61,16 +56,14 @@ class JsonPath
|
|
61
56
|
case sub_path[0]
|
62
57
|
when '\'', '"'
|
63
58
|
k = sub_path[1, sub_path.size - 2]
|
64
|
-
|
65
|
-
node[k] ||= nil if @options[:default_path_leaf_to_null]
|
66
|
-
each(node, k, pos + 1, &blk) if node.key?(k)
|
67
|
-
elsif node.respond_to?(k.to_s) && !Object.respond_to?(k.to_s)
|
59
|
+
yield_if_diggable(node, k) do
|
68
60
|
each(node, k, pos + 1, &blk)
|
69
61
|
end
|
70
62
|
when '?'
|
71
63
|
handle_question_mark(sub_path, node, pos, &blk)
|
72
64
|
else
|
73
65
|
next if node.is_a?(Array) && node.empty?
|
66
|
+
next if node.nil? # when default_path_leaf_to_null is true
|
74
67
|
|
75
68
|
array_args = sub_path.split(':')
|
76
69
|
if array_args[0] == '*'
|
@@ -130,7 +123,7 @@ class JsonPath
|
|
130
123
|
def yield_value(blk, context, key)
|
131
124
|
case @mode
|
132
125
|
when nil
|
133
|
-
blk.call(key ? context
|
126
|
+
blk.call(key ? dig_one(context, key) : context)
|
134
127
|
when :compact
|
135
128
|
if key && context[key].nil?
|
136
129
|
key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
|
@@ -158,16 +151,13 @@ class JsonPath
|
|
158
151
|
identifiers = /@?((?<!\d)\.(?!\d)(\w+))+/.match(exp)
|
159
152
|
if !identifiers.nil? && !@_current_node.methods.include?(identifiers[2].to_sym)
|
160
153
|
exp_to_eval = exp.dup
|
161
|
-
exp_to_eval[identifiers[0]] = identifiers[0].split('.').map do |el|
|
162
|
-
el == '@' ? '@' : "['#{el}']"
|
163
|
-
end.join
|
164
154
|
begin
|
165
|
-
return JsonPath::Parser.new(@_current_node).parse(exp_to_eval)
|
155
|
+
return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
|
166
156
|
rescue StandardError
|
167
157
|
return default
|
168
158
|
end
|
169
159
|
end
|
170
|
-
JsonPath::Parser.new(@_current_node).parse(exp)
|
160
|
+
JsonPath::Parser.new(@_current_node, @options).parse(exp)
|
171
161
|
end
|
172
162
|
end
|
173
163
|
end
|
data/lib/jsonpath/parser.rb
CHANGED
@@ -5,11 +5,14 @@ require 'strscan'
|
|
5
5
|
class JsonPath
|
6
6
|
# Parser parses and evaluates an expression passed to @_current_node.
|
7
7
|
class Parser
|
8
|
+
include Dig
|
9
|
+
|
8
10
|
REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/
|
9
11
|
|
10
|
-
def initialize(node)
|
12
|
+
def initialize(node, options)
|
11
13
|
@_current_node = node
|
12
14
|
@_expr_map = {}
|
15
|
+
@options = options
|
13
16
|
end
|
14
17
|
|
15
18
|
# parse will parse an expression in the following way.
|
@@ -65,7 +68,7 @@ class JsonPath
|
|
65
68
|
scanner = StringScanner.new(exp)
|
66
69
|
elements = []
|
67
70
|
until scanner.eos?
|
68
|
-
if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[
|
71
|
+
if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?]?/))
|
69
72
|
elements << t.gsub(/[\[\]'.]|\s+/, '')
|
70
73
|
elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
|
71
74
|
operator = t
|
@@ -91,7 +94,7 @@ class JsonPath
|
|
91
94
|
el = if elements.empty?
|
92
95
|
@_current_node
|
93
96
|
elsif @_current_node.is_a?(Hash)
|
94
|
-
@_current_node
|
97
|
+
dig(@_current_node, *elements)
|
95
98
|
else
|
96
99
|
elements.inject(@_current_node, &:__send__)
|
97
100
|
end
|
@@ -156,9 +159,11 @@ class JsonPath
|
|
156
159
|
res = bool_or_exp(top.shift)
|
157
160
|
top.each_with_index do |item, index|
|
158
161
|
if item == '&&'
|
159
|
-
|
162
|
+
next_value = bool_or_exp(top[index + 1])
|
163
|
+
res &&= next_value
|
160
164
|
elsif item == '||'
|
161
|
-
|
165
|
+
next_value = bool_or_exp(top[index + 1])
|
166
|
+
res ||= next_value
|
162
167
|
end
|
163
168
|
end
|
164
169
|
|
data/lib/jsonpath/version.rb
CHANGED
data/lib/jsonpath.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'strscan'
|
4
4
|
require 'multi_json'
|
5
5
|
require 'jsonpath/proxy'
|
6
|
+
require 'jsonpath/dig'
|
6
7
|
require 'jsonpath/enumerable'
|
7
8
|
require 'jsonpath/version'
|
8
9
|
require 'jsonpath/parser'
|
@@ -12,10 +13,18 @@ require 'jsonpath/parser'
|
|
12
13
|
class JsonPath
|
13
14
|
PATH_ALL = '$..*'
|
14
15
|
|
16
|
+
DEFAULT_OPTIONS = {
|
17
|
+
:default_path_leaf_to_null => false,
|
18
|
+
:symbolize_keys => false,
|
19
|
+
:use_symbols => false,
|
20
|
+
:allow_send => true,
|
21
|
+
:max_nesting => 100
|
22
|
+
}
|
23
|
+
|
15
24
|
attr_accessor :path
|
16
25
|
|
17
26
|
def initialize(path, opts = {})
|
18
|
-
@opts = opts
|
27
|
+
@opts = DEFAULT_OPTIONS.merge(opts)
|
19
28
|
scanner = StringScanner.new(path.strip)
|
20
29
|
@path = []
|
21
30
|
until scanner.eos?
|
@@ -78,13 +87,46 @@ class JsonPath
|
|
78
87
|
end
|
79
88
|
a
|
80
89
|
end
|
90
|
+
|
91
|
+
def self.fetch_all_path(obj)
|
92
|
+
all_paths = ['$']
|
93
|
+
find_path(obj, '$', all_paths, obj.class == Array)
|
94
|
+
return all_paths
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.find_path(obj, root_key, all_paths, is_array = false)
|
98
|
+
obj.each do |key, value|
|
99
|
+
table_params = { key: key, root_key: root_key}
|
100
|
+
is_loop = value.class == Array || value.class == Hash
|
101
|
+
if is_loop
|
102
|
+
path_exp = construct_path(table_params)
|
103
|
+
all_paths << path_exp
|
104
|
+
find_path(value, path_exp, all_paths, value.class == Array)
|
105
|
+
elsif is_array
|
106
|
+
table_params[:index] = obj.find_index(key)
|
107
|
+
path_exp = construct_path(table_params)
|
108
|
+
find_path(key, path_exp, all_paths, key.class == Array) if key.class == Hash || key.class == Array
|
109
|
+
all_paths << path_exp
|
110
|
+
else
|
111
|
+
all_paths << construct_path(table_params)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.construct_path(table_row)
|
117
|
+
if table_row[:index]
|
118
|
+
return table_row[:root_key] + '['+ table_row[:index].to_s + ']'
|
119
|
+
else
|
120
|
+
return table_row[:root_key] + '.'+ table_row[:key]
|
121
|
+
end
|
122
|
+
end
|
81
123
|
|
82
124
|
def first(obj_or_str, *args)
|
83
125
|
enum_on(obj_or_str).first(*args)
|
84
126
|
end
|
85
127
|
|
86
128
|
def enum_on(obj_or_str, mode = nil)
|
87
|
-
JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode,
|
129
|
+
JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str, @opts), mode,
|
88
130
|
@opts)
|
89
131
|
end
|
90
132
|
alias [] enum_on
|
@@ -99,8 +141,8 @@ class JsonPath
|
|
99
141
|
|
100
142
|
private
|
101
143
|
|
102
|
-
def self.process_object(obj_or_str)
|
103
|
-
obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str
|
144
|
+
def self.process_object(obj_or_str, opts = {})
|
145
|
+
obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str, max_nesting: opts[:max_nesting]) : obj_or_str
|
104
146
|
end
|
105
147
|
|
106
148
|
def deep_clone
|
data/test/test_jsonpath.rb
CHANGED
@@ -70,24 +70,60 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
70
70
|
assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] < 10)]").on(@object)
|
71
71
|
assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] == 9)]").on(@object)
|
72
72
|
assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] > 20)]").on(@object)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_not_equals_operator
|
76
|
+
expected =
|
77
|
+
[
|
78
|
+
@object['store']['book'][0],
|
79
|
+
@object['store']['book'][4],
|
80
|
+
@object['store']['book'][5],
|
81
|
+
@object['store']['book'][6]
|
82
|
+
]
|
83
|
+
assert_equal(expected, JsonPath.new("$..book[?(@['category'] != 'fiction')]").on(@object))
|
84
|
+
assert_equal(expected, JsonPath.new("$..book[?(@['category']!=fiction)]").on(@object))
|
85
|
+
assert_equal(expected, JsonPath.new("$..book[?(@.category!=fiction)]").on(@object))
|
86
|
+
assert_equal(expected, JsonPath.new("$..book[?(@.category != 'fiction')]").on(@object))
|
79
87
|
end
|
80
88
|
|
81
89
|
def test_or_operator
|
82
90
|
assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object)
|
91
|
+
result = ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]
|
92
|
+
assert_equal result, JsonPath.new("$..book[?(@.price==13 || @.price==9 || @.price==23)].title").on(@object)
|
93
|
+
assert_equal result, JsonPath.new("$..book[?(@.price==9 || @.price==23 || @.price==13)].title").on(@object)
|
94
|
+
assert_equal result, JsonPath.new("$..book[?(@.price==23 || @.price==13 || @.price==9)].title").on(@object)
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_or_operator_with_not_equals
|
98
|
+
# Should be the same regardless of key style ( @.key vs @['key'] )
|
99
|
+
result = ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien', 'Lukyanenko']
|
100
|
+
assert_equal result, JsonPath.new("$..book[?(@['title']=='Osennie Vizity' || @['author']!='Lukyanenko')].author").on(@object)
|
101
|
+
assert_equal result, JsonPath.new("$..book[?(@.title=='Osennie Vizity' || @.author != Lukyanenko )].author").on(@object)
|
102
|
+
assert_equal result, JsonPath.new("$..book[?(@.title=='Osennie Vizity' || @.author!=Lukyanenko )].author").on(@object)
|
83
103
|
end
|
84
104
|
|
85
105
|
def test_and_operator
|
86
106
|
assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object)
|
107
|
+
assert_equal [], JsonPath.new("$..book[?(@.price>=13 && @.category==fiction && @.title==no_match)]").on(@object)
|
108
|
+
assert_equal [], JsonPath.new("$..book[?(@.title==no_match && @.category==fiction && @.price==13)]").on(@object)
|
109
|
+
assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.title==no_match && @.category==fiction)]").on(@object)
|
110
|
+
assert_equal [], JsonPath.new("$..book[?(@.price==13 && @.bad_key_name==true && @.category==fiction)]").on(@object)
|
111
|
+
|
112
|
+
expected = [@object['store']['book'][1]]
|
113
|
+
assert_equal expected, JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object)
|
114
|
+
assert_equal expected, JsonPath.new("$..book[?(@.price < 23 && @.price > 9)]").on(@object)
|
115
|
+
|
116
|
+
expected = ['Sword of Honour', 'The Lord of the Rings']
|
117
|
+
assert_equal expected, JsonPath.new("$..book[?(@.price>=13 && @.category==fiction)].title").on(@object)
|
118
|
+
assert_equal ['The Lord of the Rings'], JsonPath.new("$..book[?(@.category==fiction && @.isbn && @.price>9)].title").on(@object)
|
119
|
+
assert_equal ['Sayings of the Century'], JsonPath.new("$..book[?(@['price'] == 9 && @.author=='Nigel Rees')].title").on(@object)
|
120
|
+
assert_equal ['Sayings of the Century'], JsonPath.new("$..book[?(@['price'] == 9 && @.tags..asdf)].title").on(@object)
|
87
121
|
end
|
88
122
|
|
89
|
-
def
|
90
|
-
|
123
|
+
def test_and_operator_with_not_equals
|
124
|
+
expected = ['Nigel Rees']
|
125
|
+
assert_equal expected, JsonPath.new("$..book[?(@['price']==9 && @['category']!=fiction)].author").on(@object)
|
126
|
+
assert_equal expected, JsonPath.new("$..book[?(@.price==9 && @.category!=fiction)].author").on(@object)
|
91
127
|
end
|
92
128
|
|
93
129
|
def test_nested_grouping
|
@@ -130,6 +166,50 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
130
166
|
assert_equal ['value'], JsonPath.new('$.b').on(object)
|
131
167
|
end
|
132
168
|
|
169
|
+
def test_works_on_object
|
170
|
+
klass = Class.new{
|
171
|
+
attr_reader :b
|
172
|
+
def initialize(b)
|
173
|
+
@b = b
|
174
|
+
end
|
175
|
+
}
|
176
|
+
object = klass.new("value")
|
177
|
+
|
178
|
+
assert_equal ["value"], JsonPath.new('$.b').on(object)
|
179
|
+
end
|
180
|
+
|
181
|
+
def test_works_on_object_can_be_disabled
|
182
|
+
klass = Class.new{
|
183
|
+
attr_reader :b
|
184
|
+
def initialize(b)
|
185
|
+
@b = b
|
186
|
+
end
|
187
|
+
}
|
188
|
+
object = klass.new("value")
|
189
|
+
|
190
|
+
assert_equal [], JsonPath.new('$.b', allow_send: false).on(object)
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_works_on_diggable
|
194
|
+
klass = Class.new{
|
195
|
+
attr_reader :h
|
196
|
+
def initialize(h)
|
197
|
+
@h = h
|
198
|
+
end
|
199
|
+
def dig(*keys)
|
200
|
+
@h.dig(*keys)
|
201
|
+
end
|
202
|
+
}
|
203
|
+
|
204
|
+
object = klass.new('a' => 'some', 'b' => 'value')
|
205
|
+
assert_equal ['value'], JsonPath.new('$.b').on(object)
|
206
|
+
|
207
|
+
object = {
|
208
|
+
"foo" => klass.new('a' => 'some', 'b' => 'value')
|
209
|
+
}
|
210
|
+
assert_equal ['value'], JsonPath.new('$.foo.b').on(object)
|
211
|
+
end
|
212
|
+
|
133
213
|
def test_works_on_non_hash_with_filters
|
134
214
|
klass = Struct.new(:a, :b)
|
135
215
|
first_object = klass.new('some', 'value')
|
@@ -138,6 +218,24 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
138
218
|
assert_equal ['other value'], JsonPath.new('$[?(@.a == "next")].b').on([first_object, second_object])
|
139
219
|
end
|
140
220
|
|
221
|
+
def test_works_on_hash_with_summary
|
222
|
+
object = {
|
223
|
+
"foo" => [{
|
224
|
+
"a" => "some",
|
225
|
+
"b" => "value"
|
226
|
+
}]
|
227
|
+
}
|
228
|
+
assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object)
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_works_on_non_hash_with_summary
|
232
|
+
klass = Struct.new(:a, :b)
|
233
|
+
object = {
|
234
|
+
"foo" => [klass.new("some", "value")]
|
235
|
+
}
|
236
|
+
assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object)
|
237
|
+
end
|
238
|
+
|
141
239
|
def test_recognize_array_with_evald_index
|
142
240
|
assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object)
|
143
241
|
end
|
@@ -508,11 +606,42 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
508
606
|
'name' => 'testname2'
|
509
607
|
}]
|
510
608
|
}
|
511
|
-
|
609
|
+
|
610
|
+
# These queries should be equivalent
|
611
|
+
expected = [{ 'isTrue' => true, 'name' => 'testname1' }]
|
612
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue)]').on(data)
|
613
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue==true)]').on(data)
|
614
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue == true)]').on(data)
|
615
|
+
|
616
|
+
# These queries should be equivalent
|
617
|
+
expected = [{ 'isTrue' => false, 'name' => 'testname2' }]
|
618
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue != true)]').on(data)
|
619
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue!=true)]').on(data)
|
620
|
+
assert_equal expected, JsonPath.new('$.data[?(@.isTrue==false)]').on(data)
|
621
|
+
end
|
622
|
+
|
623
|
+
def test_and_operator_with_boolean_parameter_value
|
624
|
+
data = {
|
625
|
+
'data' => [{
|
626
|
+
'hasProperty1' => true,
|
627
|
+
'hasProperty2' => false,
|
628
|
+
'name' => 'testname1'
|
629
|
+
}, {
|
630
|
+
'hasProperty1' => false,
|
631
|
+
'hasProperty2' => true,
|
632
|
+
'name' => 'testname2'
|
633
|
+
}, {
|
634
|
+
'hasProperty1' => true,
|
635
|
+
'hasProperty2' => true,
|
636
|
+
'name' => 'testname3'
|
637
|
+
}]
|
638
|
+
}
|
639
|
+
assert_equal ['testname3'], JsonPath.new('$.data[?(@.hasProperty1 && @.hasProperty2)].name').on(data)
|
512
640
|
end
|
513
641
|
|
514
642
|
def test_regex_simple
|
515
643
|
assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object)
|
644
|
+
assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@=~/asdf/)]').on(@object)
|
516
645
|
end
|
517
646
|
|
518
647
|
def test_regex_simple_miss
|
@@ -849,7 +978,34 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
849
978
|
assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]")
|
850
979
|
end
|
851
980
|
|
852
|
-
def
|
981
|
+
def test_selecting_multiple_keys_on_hash
|
982
|
+
json = '
|
983
|
+
{
|
984
|
+
"category": "reference",
|
985
|
+
"author": "Nigel Rees",
|
986
|
+
"title": "Sayings of the Century",
|
987
|
+
"price": 8.95
|
988
|
+
}
|
989
|
+
'.to_json
|
990
|
+
assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.(category,author)')
|
991
|
+
end
|
992
|
+
|
993
|
+
def test_selecting_multiple_keys_on_sub_hash
|
994
|
+
skip("Failing as the semantics of .(x,y) is unclear")
|
995
|
+
json = '
|
996
|
+
{
|
997
|
+
"book": {
|
998
|
+
"category": "reference",
|
999
|
+
"author": "Nigel Rees",
|
1000
|
+
"title": "Sayings of the Century",
|
1001
|
+
"price": 8.95
|
1002
|
+
}
|
1003
|
+
}
|
1004
|
+
'.to_json
|
1005
|
+
assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.book.(category,author)')
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def test_selecting_multiple_keys_on_array
|
853
1009
|
json = '
|
854
1010
|
{
|
855
1011
|
"store": {
|
@@ -874,7 +1030,7 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
874
1030
|
assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)')
|
875
1031
|
end
|
876
1032
|
|
877
|
-
def
|
1033
|
+
def test_selecting_multiple_keys_on_array_with_filter
|
878
1034
|
json = '
|
879
1035
|
{
|
880
1036
|
"store": {
|
@@ -925,6 +1081,32 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
925
1081
|
assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )")
|
926
1082
|
end
|
927
1083
|
|
1084
|
+
def test_use_symbol_opt
|
1085
|
+
json = {
|
1086
|
+
store: {
|
1087
|
+
book: [
|
1088
|
+
{
|
1089
|
+
category: "reference",
|
1090
|
+
author: "Nigel Rees",
|
1091
|
+
title: "Sayings of the Century",
|
1092
|
+
price: 8.95
|
1093
|
+
},
|
1094
|
+
{
|
1095
|
+
category: "fiction",
|
1096
|
+
author: "Evelyn Waugh",
|
1097
|
+
title: "Sword of Honour",
|
1098
|
+
price: 12.99
|
1099
|
+
}
|
1100
|
+
]
|
1101
|
+
}
|
1102
|
+
}
|
1103
|
+
on = ->(path){ JsonPath.on(json, path, use_symbols: true) }
|
1104
|
+
assert_equal ['reference', 'fiction'], on.("$.store.book[*].category")
|
1105
|
+
assert_equal ['reference', 'fiction'], on.("$..category")
|
1106
|
+
assert_equal ['reference'], on.("$.store.book[?(@['price'] == 8.95)].category")
|
1107
|
+
assert_equal [{'category' => 'reference'}], on.("$.store.book[?(@['price'] == 8.95)](category)")
|
1108
|
+
end
|
1109
|
+
|
928
1110
|
def test_object_method_send
|
929
1111
|
j = {height: 5, hash: "some_hash"}.to_json
|
930
1112
|
hs = JsonPath.new "$..send"
|
@@ -945,6 +1127,70 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
945
1127
|
assert_equal ['foo'], JsonPath.new('$.1').on(data.to_json)
|
946
1128
|
end
|
947
1129
|
|
1130
|
+
def test_behavior_on_null_and_missing
|
1131
|
+
data = {
|
1132
|
+
"foo" => nil,
|
1133
|
+
"bar" => {
|
1134
|
+
"baz" => nil
|
1135
|
+
},
|
1136
|
+
"bars" => [
|
1137
|
+
{ "foo" => 12 },
|
1138
|
+
{ "foo" => nil },
|
1139
|
+
{ }
|
1140
|
+
]
|
1141
|
+
}
|
1142
|
+
assert_equal [nil], JsonPath.new('$.foo').on(data)
|
1143
|
+
assert_equal [nil], JsonPath.new('$.bar.baz').on(data)
|
1144
|
+
assert_equal [], JsonPath.new('$.baz').on(data)
|
1145
|
+
assert_equal [], JsonPath.new('$.bar.foo').on(data)
|
1146
|
+
assert_equal [12, nil], JsonPath.new('$.bars[*].foo').on(data)
|
1147
|
+
end
|
1148
|
+
|
1149
|
+
def test_default_path_leaf_to_null_opt
|
1150
|
+
data = {
|
1151
|
+
"foo" => nil,
|
1152
|
+
"bar" => {
|
1153
|
+
"baz" => nil
|
1154
|
+
},
|
1155
|
+
"bars" => [
|
1156
|
+
{ "foo" => 12 },
|
1157
|
+
{ "foo" => nil },
|
1158
|
+
{ }
|
1159
|
+
]
|
1160
|
+
}
|
1161
|
+
assert_equal [nil], JsonPath.new('$.foo', default_path_leaf_to_null: true).on(data)
|
1162
|
+
assert_equal [nil], JsonPath.new('$.bar.baz', default_path_leaf_to_null: true).on(data)
|
1163
|
+
assert_equal [nil], JsonPath.new('$.baz', default_path_leaf_to_null: true).on(data)
|
1164
|
+
assert_equal [nil], JsonPath.new('$.bar.foo', default_path_leaf_to_null: true).on(data)
|
1165
|
+
assert_equal [12, nil, nil], JsonPath.new('$.bars[*].foo', default_path_leaf_to_null: true).on(data)
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
def test_raise_max_nesting_error
|
1169
|
+
json = {
|
1170
|
+
a: {
|
1171
|
+
b: {
|
1172
|
+
c: {
|
1173
|
+
}
|
1174
|
+
}
|
1175
|
+
}
|
1176
|
+
}.to_json
|
1177
|
+
|
1178
|
+
assert_raises(MultiJson::ParseError) { JsonPath.new('$.a', max_nesting: 1).on(json) }
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
def test_with_max_nesting_false
|
1182
|
+
json = {
|
1183
|
+
a: {
|
1184
|
+
b: {
|
1185
|
+
c: {
|
1186
|
+
}
|
1187
|
+
}
|
1188
|
+
}
|
1189
|
+
}.to_json
|
1190
|
+
|
1191
|
+
assert_equal [{}], JsonPath.new('$.a.b.c', max_nesting: false).on(json)
|
1192
|
+
end
|
1193
|
+
|
948
1194
|
def example_object
|
949
1195
|
{ 'store' => {
|
950
1196
|
'book' => [
|
@@ -999,4 +1245,19 @@ class TestJsonpath < MiniTest::Unit::TestCase
|
|
999
1245
|
'_links' => { 'self' => {} }
|
1000
1246
|
} }
|
1001
1247
|
end
|
1248
|
+
|
1249
|
+
def test_fetch_all_path
|
1250
|
+
data = {
|
1251
|
+
"foo" => nil,
|
1252
|
+
"bar" => {
|
1253
|
+
"baz" => nil
|
1254
|
+
},
|
1255
|
+
"bars" => [
|
1256
|
+
{ "foo" => 12 },
|
1257
|
+
{ "foo" => nil },
|
1258
|
+
{ }
|
1259
|
+
]
|
1260
|
+
}
|
1261
|
+
assert_equal ["$", "$.foo", "$.bar", "$.bar.baz", "$.bars", "$.bars[0].foo", "$.bars[0]", "$.bars[1].foo", "$.bars[1]", "$.bars[2]"], JsonPath.fetch_all_path(data)
|
1262
|
+
end
|
1002
1263
|
end
|
data/test/test_readme.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'phocus'
|
5
|
+
require 'jsonpath'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
class TestJsonpathReadme < MiniTest::Unit::TestCase
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@json = <<-HERE_DOC
|
12
|
+
{"store":
|
13
|
+
{"bicycle":
|
14
|
+
{"price":19.95, "color":"red"},
|
15
|
+
"book":[
|
16
|
+
{"price":8.95, "category":"reference", "title":"Sayings of the Century", "author":"Nigel Rees"},
|
17
|
+
{"price":12.99, "category":"fiction", "title":"Sword of Honour", "author":"Evelyn Waugh"},
|
18
|
+
{"price":8.99, "category":"fiction", "isbn":"0-553-21311-3", "title":"Moby Dick", "author":"Herman Melville","color":"blue"},
|
19
|
+
{"price":22.99, "category":"fiction", "isbn":"0-395-19395-8", "title":"The Lord of the Rings", "author":"Tolkien"}
|
20
|
+
]
|
21
|
+
}
|
22
|
+
}
|
23
|
+
HERE_DOC
|
24
|
+
end
|
25
|
+
attr_reader :json
|
26
|
+
|
27
|
+
def test_library_section
|
28
|
+
path = JsonPath.new('$..price')
|
29
|
+
assert_equal [19.95, 8.95, 12.99, 8.99, 22.99], path.on(json)
|
30
|
+
assert_equal [18.88], path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}')
|
31
|
+
assert_equal ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "Tolkien"], JsonPath.on(json, '$..author')
|
32
|
+
assert_equal [
|
33
|
+
{"price" => 8.95, "category" => "reference", "title" => "Sayings of the Century", "author" => "Nigel Rees"},
|
34
|
+
{"price" => 8.99, "category" => "fiction", "isbn" => "0-553-21311-3", "title" => "Moby Dick", "author" => "Herman Melville","color" => "blue"},
|
35
|
+
], JsonPath.new('$..book[::2]').on(json)
|
36
|
+
assert_equal [8.95, 8.99], JsonPath.new("$..price[?(@ < 10)]").on(json)
|
37
|
+
assert_equal ["Sayings of the Century", "Moby Dick"], JsonPath.new("$..book[?(@['price'] == 8.95 || @['price'] == 8.99)].title").on(json)
|
38
|
+
assert_equal [], JsonPath.new("$..book[?(@['price'] == 8.95 && @['price'] == 8.99)].title").on(json)
|
39
|
+
assert_equal "red", JsonPath.new('$..color').first(json)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_library_section_enumerable
|
43
|
+
enum = JsonPath.new('$..color')[json]
|
44
|
+
assert_equal "red", enum.first
|
45
|
+
assert enum.any?{ |c| c == 'red' }
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_ruby_structures_section
|
49
|
+
book = { title: "Sayings of the Century" }
|
50
|
+
assert_equal [], JsonPath.new('$.title').on(book)
|
51
|
+
assert_equal ["Sayings of the Century"], JsonPath.new('$.title', use_symbols: true).on(book)
|
52
|
+
|
53
|
+
book_class = Struct.new(:title)
|
54
|
+
book = book_class.new("Sayings of the Century")
|
55
|
+
assert_equal ["Sayings of the Century"], JsonPath.new('$.title').on(book)
|
56
|
+
|
57
|
+
book_class = Class.new{ attr_accessor :title }
|
58
|
+
book = book_class.new
|
59
|
+
book.title = "Sayings of the Century"
|
60
|
+
assert_equal ["Sayings of the Century"], JsonPath.new('$.title', allow_send: true).on(book)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_options_section
|
64
|
+
assert_equal ["0-553-21311-3", "0-395-19395-8"], JsonPath.new('$..book[*].isbn').on(json)
|
65
|
+
assert_equal [nil, nil, "0-553-21311-3", "0-395-19395-8"], JsonPath.new('$..book[*].isbn', default_path_leaf_to_null: true).on(json)
|
66
|
+
|
67
|
+
assert_equal ["price", "category", "title", "author"], JsonPath.new('$..book[0]').on(json).map(&:keys).flatten.uniq
|
68
|
+
assert_equal [:price, :category, :title, :author], JsonPath.new('$..book[0]').on(json, symbolize_keys: true).map(&:keys).flatten.uniq
|
69
|
+
end
|
70
|
+
|
71
|
+
def selecting_value_section
|
72
|
+
json = <<-HERE_DOC
|
73
|
+
{
|
74
|
+
"store": {
|
75
|
+
"book": [
|
76
|
+
{
|
77
|
+
"category": "reference",
|
78
|
+
"author": "Nigel Rees",
|
79
|
+
"title": "Sayings of the Century",
|
80
|
+
"price": 8.95
|
81
|
+
},
|
82
|
+
{
|
83
|
+
"category": "fiction",
|
84
|
+
"author": "Evelyn Waugh",
|
85
|
+
"title": "Sword of Honour",
|
86
|
+
"price": 12.99
|
87
|
+
}
|
88
|
+
]
|
89
|
+
}
|
90
|
+
HERE_DOC
|
91
|
+
got = JsonPath.on(json, "$.store.book[*](category,author)")
|
92
|
+
expected = [
|
93
|
+
{
|
94
|
+
"category" => "reference",
|
95
|
+
"author" => "Nigel Rees"
|
96
|
+
},
|
97
|
+
{
|
98
|
+
"category" => "fiction",
|
99
|
+
"author" => "Evelyn Waugh"
|
100
|
+
}
|
101
|
+
]
|
102
|
+
assert_equal expected, got
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_manipulation_section
|
106
|
+
assert_equal({"candy" => "big turks"}, JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash)
|
107
|
+
|
108
|
+
json = '{"candy":"lollipop","noncandy":null,"other":"things"}'
|
109
|
+
o = JsonPath.for(json).
|
110
|
+
gsub('$..candy') {|v| "big turks" }.
|
111
|
+
compact.
|
112
|
+
delete('$..other').
|
113
|
+
to_hash
|
114
|
+
assert_equal({"candy" => "big turks"}, o)
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonpath
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joshua Hull
|
8
8
|
- Gergely Brautigam
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-04-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: multi_json
|
@@ -81,6 +81,20 @@ dependencies:
|
|
81
81
|
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: racc
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
84
98
|
- !ruby/object:Gem::Dependency
|
85
99
|
name: rake
|
86
100
|
requirement: !ruby/object:Gem::Requirement
|
@@ -106,11 +120,11 @@ extra_rdoc_files:
|
|
106
120
|
- README.md
|
107
121
|
files:
|
108
122
|
- ".gemtest"
|
123
|
+
- ".github/workflows/test.yml"
|
109
124
|
- ".gitignore"
|
110
125
|
- ".rspec"
|
111
126
|
- ".rubocop.yml"
|
112
127
|
- ".rubocop_todo.yml"
|
113
|
-
- ".travis.yml"
|
114
128
|
- Gemfile
|
115
129
|
- LICENSE.md
|
116
130
|
- README.md
|
@@ -118,17 +132,19 @@ files:
|
|
118
132
|
- bin/jsonpath
|
119
133
|
- jsonpath.gemspec
|
120
134
|
- lib/jsonpath.rb
|
135
|
+
- lib/jsonpath/dig.rb
|
121
136
|
- lib/jsonpath/enumerable.rb
|
122
137
|
- lib/jsonpath/parser.rb
|
123
138
|
- lib/jsonpath/proxy.rb
|
124
139
|
- lib/jsonpath/version.rb
|
125
140
|
- test/test_jsonpath.rb
|
126
141
|
- test/test_jsonpath_bin.rb
|
142
|
+
- test/test_readme.rb
|
127
143
|
homepage: https://github.com/joshbuddy/jsonpath
|
128
144
|
licenses:
|
129
145
|
- MIT
|
130
146
|
metadata: {}
|
131
|
-
post_install_message:
|
147
|
+
post_install_message:
|
132
148
|
rdoc_options: []
|
133
149
|
require_paths:
|
134
150
|
- lib
|
@@ -143,8 +159,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
159
|
- !ruby/object:Gem::Version
|
144
160
|
version: '0'
|
145
161
|
requirements: []
|
146
|
-
rubygems_version: 3.
|
147
|
-
signing_key:
|
162
|
+
rubygems_version: 3.3.7
|
163
|
+
signing_key:
|
148
164
|
specification_version: 4
|
149
165
|
summary: Ruby implementation of http://goessner.net/articles/JsonPath/
|
150
166
|
test_files: []
|