jsonpath 1.0.7 → 1.1.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: a31bb2922acb2b1a6e84a6e640ee148eb1f40f9d8113751c6903149e472e99d9
4
- data.tar.gz: ab119c28fe07e14bfab16e5e2905d59ccf7e8154d5ff817d14a9db92215f8bd2
3
+ metadata.gz: 27b1b5e54ebef03b9cde7f447c1dc6fb8e411b4a28f5039426c87782b250ab1a
4
+ data.tar.gz: ae08f25885125bde4f301d905b9c6446bf0e922516aabce101a4e90088fd6e68
5
5
  SHA512:
6
- metadata.gz: 4ede5f5c546e8ac46689b993ada2d6ed456ed9e9b1027023e421b8d27e99b3f785360a3fcf093a58be473a80ecca3485b709f23575be6b93fb71c2ae46f63966
7
- data.tar.gz: 021a544e520b944266c276b3b79ca53e0311df7ade212655282e4f3f03b9bca18aba5858148dc7de8ec3ca91be67dbd2f5554c4b6a73273c094d94dc31f5aa71
6
+ metadata.gz: e4e778c37086407f3fb101b23c7f0507c48f0714ad2ae671d14ce52a2397e18f0f7433b8fa0fe651e503285002b4db1b70dad91ae4de1cf51b948f9ec736818b
7
+ data.tar.gz: b0dac82fcb16862190ae1405680d4779d6b3208d6ad31e25e5b0eb800877ca8bf56bae2789bac11452dfd872593071334205208bcb3a7320e3a8c0b94ba206c9
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ doc/*
6
6
  .DS_Store
7
7
  .idea
8
8
  vendor
9
+ .tags
10
+ *.gem
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 of yore, JsonPath lets you
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 object
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. We create an object for the path
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 `JsonPath.on` method.
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", "author"=>"Nigel Rees", "title"=>"Sayings of the Century"},
77
- # {"price"=>8.99, "category"=>"fiction", "author"=>"Herman Melville", "title"=>"Moby Dick", "isbn"=>"0-553-21311-3"}
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('$..price[?(@ < 10)]').on(json)
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 JSON object and path.
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(object)
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')[object]
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
- ### More examples
114
+ For more usage examples and variations on paths, please visit the tests. There
115
+ are some more complex ones as well.
107
116
 
108
- For more usage examples and variations on paths, please visit the tests. There are some more complex ones as well.
117
+ ### Querying ruby data structures
109
118
 
110
- ### Conditional Operators Are Also Supported
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:
111
121
 
112
122
  ```ruby
113
- def test_or_operator
114
- assert_equal [@object['store']['book'][1], @object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] == 13 || @['price'] == 23)]").on(@object)
115
- end
123
+ book = { title: "Sayings of the Century" }
116
124
 
117
- def test_and_operator
118
- assert_equal [], JsonPath.new("$..book[?(@['price'] == 13 && @['price'] == 23)]").on(@object)
119
- end
125
+ JsonPath.new('$.title').on(book)
126
+ # => []
120
127
 
121
- def test_and_operator_with_more_results
122
- assert_equal [@object['store']['book'][1]], JsonPath.new("$..book[?(@['price'] < 23 && @['price'] > 9)]").on(@object)
123
- end
128
+ JsonPath.new('$.title', use_symbols: true).on(book)
129
+ # => ["Sayings of the Century"]
130
+ ```
131
+
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:
135
+
136
+ ```ruby
137
+ book_class = Struct.new(:title)
138
+ book = book_class.new("Sayings of the Century")
139
+
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"
152
+
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 example given this JSON data:
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` or `#gsub!` to modify the object in place.
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!`. To remove all keys under a certain path, use `#delete` or `#delete!`. You can even chain these methods together as follows:
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"}'
@@ -202,4 +255,11 @@ o = JsonPath.for(json).
202
255
 
203
256
  # Contributions
204
257
 
205
- Please feel free to submit an Issue or a Pull Request any time you feel like you would like to contribute. Thank you!
258
+ Please feel free to submit an Issue or a Pull Request any time you feel like
259
+ you would like to contribute. Thank you!
260
+
261
+ ## Running an individual test
262
+
263
+ ```ruby
264
+ ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6
265
+ ```
@@ -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,17 @@ 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
+ }
22
+
15
23
  attr_accessor :path
16
24
 
17
25
  def initialize(path, opts = {})
18
- @opts = opts
26
+ @opts = DEFAULT_OPTIONS.merge(opts)
19
27
  scanner = StringScanner.new(path.strip)
20
28
  @path = []
21
29
  until scanner.eos?
@@ -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
@@ -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
- # TODO: Change this to `slice(*keys)` when ruby version support is > 2.4
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.select { |k| keys.include?(k) }
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
- if node.is_a?(Hash)
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[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)
@@ -162,12 +155,12 @@ class JsonPath
162
155
  el == '@' ? '@' : "['#{el}']"
163
156
  end.join
164
157
  begin
165
- return JsonPath::Parser.new(@_current_node).parse(exp_to_eval)
158
+ return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
166
159
  rescue StandardError
167
160
  return default
168
161
  end
169
162
  end
170
- JsonPath::Parser.new(@_current_node).parse(exp)
163
+ JsonPath::Parser.new(@_current_node, @options).parse(exp)
171
164
  end
172
165
  end
173
166
  end
@@ -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.
@@ -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.dig(*elements)
97
+ dig(@_current_node, *elements)
95
98
  else
96
99
  elements.inject(@_current_node, &:__send__)
97
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class JsonPath
4
- VERSION = '1.0.7'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -130,6 +130,50 @@ class TestJsonpath < MiniTest::Unit::TestCase
130
130
  assert_equal ['value'], JsonPath.new('$.b').on(object)
131
131
  end
132
132
 
133
+ def test_works_on_object
134
+ klass = Class.new{
135
+ attr_reader :b
136
+ def initialize(b)
137
+ @b = b
138
+ end
139
+ }
140
+ object = klass.new("value")
141
+
142
+ assert_equal ["value"], JsonPath.new('$.b').on(object)
143
+ end
144
+
145
+ def test_works_on_object_can_be_disabled
146
+ klass = Class.new{
147
+ attr_reader :b
148
+ def initialize(b)
149
+ @b = b
150
+ end
151
+ }
152
+ object = klass.new("value")
153
+
154
+ assert_equal [], JsonPath.new('$.b', allow_send: false).on(object)
155
+ end
156
+
157
+ def test_works_on_diggable
158
+ klass = Class.new{
159
+ attr_reader :h
160
+ def initialize(h)
161
+ @h = h
162
+ end
163
+ def dig(*keys)
164
+ @h.dig(*keys)
165
+ end
166
+ }
167
+
168
+ object = klass.new('a' => 'some', 'b' => 'value')
169
+ assert_equal ['value'], JsonPath.new('$.b').on(object)
170
+
171
+ object = {
172
+ "foo" => klass.new('a' => 'some', 'b' => 'value')
173
+ }
174
+ assert_equal ['value'], JsonPath.new('$.foo.b').on(object)
175
+ end
176
+
133
177
  def test_works_on_non_hash_with_filters
134
178
  klass = Struct.new(:a, :b)
135
179
  first_object = klass.new('some', 'value')
@@ -138,6 +182,24 @@ class TestJsonpath < MiniTest::Unit::TestCase
138
182
  assert_equal ['other value'], JsonPath.new('$[?(@.a == "next")].b').on([first_object, second_object])
139
183
  end
140
184
 
185
+ def test_works_on_hash_with_summary
186
+ object = {
187
+ "foo" => [{
188
+ "a" => "some",
189
+ "b" => "value"
190
+ }]
191
+ }
192
+ assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object)
193
+ end
194
+
195
+ def test_works_on_non_hash_with_summary
196
+ klass = Struct.new(:a, :b)
197
+ object = {
198
+ "foo" => [klass.new("some", "value")]
199
+ }
200
+ assert_equal [{ "b" => "value" }], JsonPath.new("$.foo[*](b)").on(object)
201
+ end
202
+
141
203
  def test_recognize_array_with_evald_index
142
204
  assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object)
143
205
  end
@@ -849,7 +911,34 @@ class TestJsonpath < MiniTest::Unit::TestCase
849
911
  assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]")
850
912
  end
851
913
 
852
- def test_selecting_multiple_keys
914
+ def test_selecting_multiple_keys_on_hash
915
+ json = '
916
+ {
917
+ "category": "reference",
918
+ "author": "Nigel Rees",
919
+ "title": "Sayings of the Century",
920
+ "price": 8.95
921
+ }
922
+ '.to_json
923
+ assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.(category,author)')
924
+ end
925
+
926
+ def test_selecting_multiple_keys_on_sub_hash
927
+ skip("Failing as the semantics of .(x,y) is unclear")
928
+ json = '
929
+ {
930
+ "book": {
931
+ "category": "reference",
932
+ "author": "Nigel Rees",
933
+ "title": "Sayings of the Century",
934
+ "price": 8.95
935
+ }
936
+ }
937
+ '.to_json
938
+ assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, '$.book.(category,author)')
939
+ end
940
+
941
+ def test_selecting_multiple_keys_on_array
853
942
  json = '
854
943
  {
855
944
  "store": {
@@ -874,7 +963,7 @@ class TestJsonpath < MiniTest::Unit::TestCase
874
963
  assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)')
875
964
  end
876
965
 
877
- def test_selecting_multiple_keys_with_filter
966
+ def test_selecting_multiple_keys_on_array_with_filter
878
967
  json = '
879
968
  {
880
969
  "store": {
@@ -925,6 +1014,32 @@ class TestJsonpath < MiniTest::Unit::TestCase
925
1014
  assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )")
926
1015
  end
927
1016
 
1017
+ def test_use_symbol_opt
1018
+ json = {
1019
+ store: {
1020
+ book: [
1021
+ {
1022
+ category: "reference",
1023
+ author: "Nigel Rees",
1024
+ title: "Sayings of the Century",
1025
+ price: 8.95
1026
+ },
1027
+ {
1028
+ category: "fiction",
1029
+ author: "Evelyn Waugh",
1030
+ title: "Sword of Honour",
1031
+ price: 12.99
1032
+ }
1033
+ ]
1034
+ }
1035
+ }
1036
+ on = ->(path){ JsonPath.on(json, path, use_symbols: true) }
1037
+ assert_equal ['reference', 'fiction'], on.("$.store.book[*].category")
1038
+ assert_equal ['reference', 'fiction'], on.("$..category")
1039
+ assert_equal ['reference'], on.("$.store.book[?(@['price'] == 8.95)].category")
1040
+ assert_equal [{'category' => 'reference'}], on.("$.store.book[?(@['price'] == 8.95)](category)")
1041
+ end
1042
+
928
1043
  def test_object_method_send
929
1044
  j = {height: 5, hash: "some_hash"}.to_json
930
1045
  hs = JsonPath.new "$..send"
@@ -945,6 +1060,44 @@ class TestJsonpath < MiniTest::Unit::TestCase
945
1060
  assert_equal ['foo'], JsonPath.new('$.1').on(data.to_json)
946
1061
  end
947
1062
 
1063
+ def test_behavior_on_null_and_missing
1064
+ data = {
1065
+ "foo" => nil,
1066
+ "bar" => {
1067
+ "baz" => nil
1068
+ },
1069
+ "bars" => [
1070
+ { "foo" => 12 },
1071
+ { "foo" => nil },
1072
+ { }
1073
+ ]
1074
+ }
1075
+ assert_equal [nil], JsonPath.new('$.foo').on(data)
1076
+ assert_equal [nil], JsonPath.new('$.bar.baz').on(data)
1077
+ assert_equal [], JsonPath.new('$.baz').on(data)
1078
+ assert_equal [], JsonPath.new('$.bar.foo').on(data)
1079
+ assert_equal [12, nil], JsonPath.new('$.bars[*].foo').on(data)
1080
+ end
1081
+
1082
+ def test_default_path_leaf_to_null_opt
1083
+ data = {
1084
+ "foo" => nil,
1085
+ "bar" => {
1086
+ "baz" => nil
1087
+ },
1088
+ "bars" => [
1089
+ { "foo" => 12 },
1090
+ { "foo" => nil },
1091
+ { }
1092
+ ]
1093
+ }
1094
+ assert_equal [nil], JsonPath.new('$.foo', default_path_leaf_to_null: true).on(data)
1095
+ assert_equal [nil], JsonPath.new('$.bar.baz', default_path_leaf_to_null: true).on(data)
1096
+ assert_equal [nil], JsonPath.new('$.baz', default_path_leaf_to_null: true).on(data)
1097
+ assert_equal [nil], JsonPath.new('$.bar.foo', default_path_leaf_to_null: true).on(data)
1098
+ assert_equal [12, nil, nil], JsonPath.new('$.bars[*].foo', default_path_leaf_to_null: true).on(data)
1099
+ end
1100
+
948
1101
  def example_object
949
1102
  { 'store' => {
950
1103
  'book' => [
@@ -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.0.7
4
+ version: 1.1.0
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: 2020-12-15 00:00:00.000000000 Z
12
+ date: 2020-12-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_json
@@ -118,17 +118,19 @@ files:
118
118
  - bin/jsonpath
119
119
  - jsonpath.gemspec
120
120
  - lib/jsonpath.rb
121
+ - lib/jsonpath/dig.rb
121
122
  - lib/jsonpath/enumerable.rb
122
123
  - lib/jsonpath/parser.rb
123
124
  - lib/jsonpath/proxy.rb
124
125
  - lib/jsonpath/version.rb
125
126
  - test/test_jsonpath.rb
126
127
  - test/test_jsonpath_bin.rb
128
+ - test/test_readme.rb
127
129
  homepage: https://github.com/joshbuddy/jsonpath
128
130
  licenses:
129
131
  - MIT
130
132
  metadata: {}
131
- post_install_message:
133
+ post_install_message:
132
134
  rdoc_options: []
133
135
  require_paths:
134
136
  - lib
@@ -143,8 +145,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
145
  - !ruby/object:Gem::Version
144
146
  version: '0'
145
147
  requirements: []
146
- rubygems_version: 3.0.3
147
- signing_key:
148
+ rubygems_version: 3.1.2
149
+ signing_key:
148
150
  specification_version: 4
149
151
  summary: Ruby implementation of http://goessner.net/articles/JsonPath/
150
152
  test_files: []