jsonpath 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []