jsonpath 1.0.3 → 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: 762aacec760a67a4097f47df701a1cec0f3f524ae016a2b724873c625c25672d
4
- data.tar.gz: a3076196bc9d30f4cad3ee9c0ab3d58f9bedef4b7d17d7934e7e930c351ee1c0
3
+ metadata.gz: 27b1b5e54ebef03b9cde7f447c1dc6fb8e411b4a28f5039426c87782b250ab1a
4
+ data.tar.gz: ae08f25885125bde4f301d905b9c6446bf0e922516aabce101a4e90088fd6e68
5
5
  SHA512:
6
- metadata.gz: 7894391f0693b7ebb3f3d8bca20dcf2eeb54af85e6ca268ff75fe120c64ae5e8ff09fa9a7b82cacbb8a5776d03dfa0f9bcc1f4716eea63988910cbb501d64236
7
- data.tar.gz: c0f5c57d5c527a432a1c02fa728e32f8f8a7009efd0a31778b2766d97eba1e752ec1779e2d725fc233731f64541be72c9e896a511f312f8b5e3a4a1ed3e31a8d
6
+ metadata.gz: e4e778c37086407f3fb101b23c7f0507c48f0714ad2ae671d14ce52a2397e18f0f7433b8fa0fe651e503285002b4db1b70dad91ae4de1cf51b948f9ec736818b
7
+ data.tar.gz: b0dac82fcb16862190ae1405680d4779d6b3208d6ad31e25e5b0eb800877ca8bf56bae2789bac11452dfd872593071334205208bcb3a7320e3a8c0b94ba206c9
data/.gitignore CHANGED
@@ -5,3 +5,6 @@ doc/*
5
5
  .yardoc
6
6
  .DS_Store
7
7
  .idea
8
+ vendor
9
+ .tags
10
+ *.gem
@@ -1,11 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.3.8
4
- - 2.4.6
5
- - 2.5.5
6
- - 2.6.3
3
+ - 2.5
4
+ - 2.6
5
+ - 2.7
7
6
  - ruby-head
8
7
  - jruby-head
9
-
10
- before_install:
11
- - gem install bundler -v '< 2'
8
+ - truffleruby-head
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,35 +111,123 @@ 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"]
124
130
  ```
125
131
 
126
- ### Running an individual test
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:
127
135
 
128
136
  ```ruby
129
- ruby -Ilib:../lib test/test_jsonpath.rb --name test_wildcard_on_intermediary_element_v6
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", ...}]
179
+ ```
180
+
181
+ ### Selecting Values
182
+
183
+ It's possible to select results once a query has been defined after the query. For
184
+ example given this JSON data:
185
+
186
+ ```bash
187
+ {
188
+ "store": {
189
+ "book": [
190
+ {
191
+ "category": "reference",
192
+ "author": "Nigel Rees",
193
+ "title": "Sayings of the Century",
194
+ "price": 8.95
195
+ },
196
+ {
197
+ "category": "fiction",
198
+ "author": "Evelyn Waugh",
199
+ "title": "Sword of Honour",
200
+ "price": 12.99
201
+ }
202
+ ]
203
+ }
204
+ ```
205
+
206
+ ... and this query:
207
+
208
+ ```ruby
209
+ "$.store.book[*](category,author)"
210
+ ```
211
+
212
+ ... the result can be filtered as such:
213
+
214
+ ```bash
215
+ [
216
+ {
217
+ "category" : "reference",
218
+ "author" : "Nigel Rees"
219
+ },
220
+ {
221
+ "category" : "fiction",
222
+ "author" : "Evelyn Waugh"
223
+ }
224
+ ]
130
225
  ```
131
226
 
132
227
  ### Manipulation
133
228
 
134
- 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.
135
231
 
136
232
  ```ruby
137
233
  JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash
@@ -143,7 +239,9 @@ The result will be
143
239
  {'candy' => 'big turks'}
144
240
  ```
145
241
 
146
- 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:
147
245
 
148
246
  ```ruby
149
247
  json = '{"candy":"lollipop","noncandy":null,"other":"things"}'
@@ -157,4 +255,11 @@ o = JsonPath.for(json).
157
255
 
158
256
  # Contributions
159
257
 
160
- 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
+ ```
@@ -5,10 +5,7 @@ require File.join(File.dirname(__FILE__), 'lib', 'jsonpath', 'version')
5
5
  Gem::Specification.new do |s|
6
6
  s.name = 'jsonpath'
7
7
  s.version = JsonPath::VERSION
8
- if s.respond_to? :required_rubygems_version=
9
- s.required_rubygems_version =
10
- Gem::Requirement.new('>= 0')
11
- end
8
+ s.required_ruby_version = '>= 2.5'
12
9
  s.authors = ['Joshua Hull', 'Gergely Brautigam']
13
10
  s.summary = 'Ruby implementation of http://goessner.net/articles/JsonPath/'
14
11
  s.description = 'Ruby implementation of http://goessner.net/articles/JsonPath/.'
@@ -16,17 +13,12 @@ Gem::Specification.new do |s|
16
13
  s.extra_rdoc_files = ['README.md']
17
14
  s.files = `git ls-files`.split("\n")
18
15
  s.homepage = 'https://github.com/joshbuddy/jsonpath'
19
- s.rdoc_options = ['--charset=UTF-8']
20
- s.require_paths = ['lib']
21
- s.rubygems_version = '1.3.7'
22
16
  s.test_files = `git ls-files`.split("\n").select { |f| f =~ /^spec/ }
23
- s.rubyforge_project = 'jsonpath'
24
17
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
25
18
  s.licenses = ['MIT']
26
19
 
27
20
  # dependencies
28
21
  s.add_runtime_dependency 'multi_json'
29
- s.add_runtime_dependency 'to_regexp', '~> 0.2.1'
30
22
  s.add_development_dependency 'bundler'
31
23
  s.add_development_dependency 'code_stats'
32
24
  s.add_development_dependency 'minitest', '~> 2.2.0'
@@ -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'
@@ -10,30 +11,39 @@ require 'jsonpath/parser'
10
11
  # JsonPath: initializes the class with a given JsonPath and parses that path
11
12
  # into a token array.
12
13
  class JsonPath
13
- PATH_ALL = '$..*'.freeze
14
+ PATH_ALL = '$..*'
15
+
16
+ DEFAULT_OPTIONS = {
17
+ :default_path_leaf_to_null => false,
18
+ :symbolize_keys => false,
19
+ :use_symbols => false,
20
+ :allow_send => true
21
+ }
14
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?
22
- if token = scanner.scan(/\$\B|@\B|\*|\.\./)
30
+ if (token = scanner.scan(/\$\B|@\B|\*|\.\./))
23
31
  @path << token
24
- elsif token = scanner.scan(/[\$@a-zA-Z0-9:{}_-]+/)
32
+ elsif (token = scanner.scan(/[$@a-zA-Z0-9:{}_-]+/))
25
33
  @path << "['#{token}']"
26
- elsif token = scanner.scan(/'(.*?)'/)
34
+ elsif (token = scanner.scan(/'(.*?)'/))
27
35
  @path << "[#{token}]"
28
- elsif token = scanner.scan(/\[/)
36
+ elsif (token = scanner.scan(/\[/))
29
37
  @path << find_matching_brackets(token, scanner)
30
- elsif token = scanner.scan(/\]/)
38
+ elsif (token = scanner.scan(/\]/))
31
39
  raise ArgumentError, 'unmatched closing bracket'
40
+ elsif (token = scanner.scan(/\(.*\)/))
41
+ @path << token
32
42
  elsif scanner.scan(/\./)
33
43
  nil
34
- elsif token = scanner.scan(/[><=] \d+/)
44
+ elsif (token = scanner.scan(/[><=] \d+/))
35
45
  @path.last << token
36
- elsif token = scanner.scan(/./)
46
+ elsif (token = scanner.scan(/./))
37
47
  begin
38
48
  @path.last << token
39
49
  rescue RuntimeError
@@ -46,13 +56,13 @@ class JsonPath
46
56
  def find_matching_brackets(token, scanner)
47
57
  count = 1
48
58
  until count.zero?
49
- if t = scanner.scan(/\[/)
59
+ if (t = scanner.scan(/\[/))
50
60
  token << t
51
61
  count += 1
52
- elsif t = scanner.scan(/\]/)
62
+ elsif (t = scanner.scan(/\]/))
53
63
  token << t
54
64
  count -= 1
55
- elsif t = scanner.scan(/[^\[\]]+/)
65
+ elsif (t = scanner.scan(/[^\[\]]+/))
56
66
  token << t
57
67
  elsif scanner.eos?
58
68
  raise ArgumentError, 'unclosed bracket'
@@ -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
 
@@ -28,6 +24,10 @@ class JsonPath
28
24
  each(context, key, pos + 1, &blk) if node == @object
29
25
  when /^\[(.*)\]$/
30
26
  handle_wildecard(node, expr, context, key, pos, &blk)
27
+ when /\(.*\)/
28
+ keys = expr.gsub(/[()]/, '').split(',').map(&:strip)
29
+ new_context = filter_context(context, keys)
30
+ yield_value(blk, new_context, key)
31
31
  end
32
32
 
33
33
  if pos > 0 && @path[pos - 1] == '..' || (@path[pos - 1] == '*' && @path[pos] != '..')
@@ -40,21 +40,30 @@ class JsonPath
40
40
 
41
41
  private
42
42
 
43
+ def filter_context(context, keys)
44
+ case context
45
+ when Hash
46
+ dig_as_hash(context, keys)
47
+ when Array
48
+ context.each_with_object([]) do |c, memo|
49
+ memo << dig_as_hash(c, keys)
50
+ end
51
+ end
52
+ end
53
+
43
54
  def handle_wildecard(node, expr, _context, _key, pos, &blk)
44
55
  expr[1, expr.size - 2].split(',').each do |sub_path|
45
56
  case sub_path[0]
46
57
  when '\'', '"'
47
58
  k = sub_path[1, sub_path.size - 2]
48
- if node.is_a?(Hash)
49
- node[k] ||= nil if @options[:default_path_leaf_to_null]
50
- each(node, k, pos + 1, &blk) if node.key?(k)
51
- elsif node.respond_to?(k.to_s)
59
+ yield_if_diggable(node, k) do
52
60
  each(node, k, pos + 1, &blk)
53
61
  end
54
62
  when '?'
55
63
  handle_question_mark(sub_path, node, pos, &blk)
56
64
  else
57
65
  next if node.is_a?(Array) && node.empty?
66
+ next if node.nil? # when default_path_leaf_to_null is true
58
67
 
59
68
  array_args = sub_path.split(':')
60
69
  if array_args[0] == '*'
@@ -112,10 +121,9 @@ class JsonPath
112
121
  end
113
122
 
114
123
  def yield_value(blk, context, key)
115
- key = Integer(key) rescue key if key
116
124
  case @mode
117
125
  when nil
118
- blk.call(key ? context[key] : context)
126
+ blk.call(key ? dig_one(context, key) : context)
119
127
  when :compact
120
128
  if key && context[key].nil?
121
129
  key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
@@ -147,12 +155,12 @@ class JsonPath
147
155
  el == '@' ? '@' : "['#{el}']"
148
156
  end.join
149
157
  begin
150
- return JsonPath::Parser.new(@_current_node).parse(exp_to_eval)
158
+ return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
151
159
  rescue StandardError
152
160
  return default
153
161
  end
154
162
  end
155
- JsonPath::Parser.new(@_current_node).parse(exp)
163
+ JsonPath::Parser.new(@_current_node, @options).parse(exp)
156
164
  end
157
165
  end
158
166
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'strscan'
4
- require 'to_regexp'
5
4
 
6
5
  class JsonPath
7
6
  # Parser parses and evaluates an expression passed to @_current_node.
8
7
  class Parser
9
- def initialize(node)
8
+ include Dig
9
+
10
+ REGEX = /\A\/(.+)\/([imxnesu]*)\z|\A%r{(.+)}([imxnesu]*)\z/
11
+
12
+ def initialize(node, options)
10
13
  @_current_node = node
11
14
  @_expr_map = {}
15
+ @options = options
12
16
  end
13
17
 
14
18
  # parse will parse an expression in the following way.
@@ -65,19 +69,22 @@ class JsonPath
65
69
  elements = []
66
70
  until scanner.eos?
67
71
  if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/))
68
- elements << t.gsub(/[\[\]'\.]|\s+/, '')
72
+ elements << t.gsub(/[\[\]'.]|\s+/, '')
69
73
  elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
70
74
  operator = t
71
75
  elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/))
72
76
  # If we encounter a node which does not contain `'` it means
73
77
  #  that we are dealing with a boolean type.
74
- operand = if t == 'true'
75
- true
76
- elsif t == 'false'
77
- false
78
- else
79
- operator.to_s.strip == '=~' ? t.to_regexp : t.gsub(%r{^'|'$}, '').strip
80
- end
78
+ operand =
79
+ if t == 'true'
80
+ true
81
+ elsif t == 'false'
82
+ false
83
+ elsif operator.to_s.strip == '=~'
84
+ parse_regex(t)
85
+ else
86
+ t.gsub(%r{^'|'$}, '').strip
87
+ end
81
88
  elsif (t = scanner.scan(/\/\w+\//))
82
89
  elsif (t = scanner.scan(/.*/))
83
90
  raise "Could not process symbol: #{t}"
@@ -87,7 +94,7 @@ class JsonPath
87
94
  el = if elements.empty?
88
95
  @_current_node
89
96
  elsif @_current_node.is_a?(Hash)
90
- @_current_node.dig(*elements)
97
+ dig(@_current_node, *elements)
91
98
  else
92
99
  elements.inject(@_current_node, &:__send__)
93
100
  end
@@ -102,6 +109,31 @@ class JsonPath
102
109
 
103
110
  private
104
111
 
112
+ # /foo/i -> Regex.new("foo", Regexp::IGNORECASE) without using eval
113
+ # also supports %r{foo}i
114
+ # following https://github.com/seamusabshere/to_regexp/blob/master/lib/to_regexp.rb
115
+ def parse_regex(t)
116
+ t =~ REGEX
117
+ content = $1 || $3
118
+ options = $2 || $4
119
+
120
+ raise ArgumentError, "unsupported regex #{t} use /foo/ style" if !content || !options
121
+
122
+ content = content.gsub '\\/', '/'
123
+
124
+ flags = 0
125
+ flags |= Regexp::IGNORECASE if options.include?('i')
126
+ flags |= Regexp::MULTILINE if options.include?('m')
127
+ flags |= Regexp::EXTENDED if options.include?('x')
128
+
129
+ # 'n' = none, 'e' = EUC, 's' = SJIS, 'u' = UTF-8
130
+ lang = options.scan(/[nes]/).join.downcase # ignores u since that is default and causes a warning
131
+
132
+ args = [content, flags]
133
+ args << lang unless lang.empty? # avoid warning
134
+ Regexp.new(*args)
135
+ end
136
+
105
137
  #  This will break down a parenthesis from the left to the right
106
138
  #  and replace the given expression with it's returned value.
107
139
  # It does this in order to make it easy to eliminate groups
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class JsonPath
4
- VERSION = '1.0.3'.freeze
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
@@ -511,15 +573,31 @@ class TestJsonpath < MiniTest::Unit::TestCase
511
573
  assert_equal [{ 'isTrue' => true, 'name' => 'testname1' }], JsonPath.new('$.data[?(@.isTrue)]').on(data)
512
574
  end
513
575
 
514
- def test_regex
515
- assert_equal [], JsonPath.new('$..book[?(@.author =~ /herman/)]').on(@object)
576
+ def test_regex_simple
577
+ assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object)
578
+ end
579
+
580
+ def test_regex_simple_miss
581
+ assert_equal [], JsonPath.new('$.store.book..tags[?(@ =~ /wut/)]').on(@object)
582
+ end
583
+
584
+ def test_regex_r
585
+ assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ %r{asdf})]').on(@object)
586
+ end
587
+
588
+ def test_regex_flags
516
589
  assert_equal [
517
590
  @object['store']['book'][2],
518
591
  @object['store']['book'][4],
519
592
  @object['store']['book'][5],
520
593
  @object['store']['book'][6]
521
594
  ], JsonPath.new('$..book[?(@.author =~ /herman|lukyanenko/i)]').on(@object)
522
- assert_equal %w[asdf asdf2], JsonPath.new('$.store.book..tags[?(@ =~ /asdf/)]').on(@object)
595
+ end
596
+
597
+ def test_regex_error
598
+ assert_raises ArgumentError do
599
+ JsonPath.new('$.store.book..tags[?(@ =~ asdf)]').on(@object)
600
+ end
523
601
  end
524
602
 
525
603
  def test_regression_1
@@ -748,7 +826,7 @@ class TestJsonpath < MiniTest::Unit::TestCase
748
826
  end
749
827
 
750
828
  def test_runtime_error_frozen_string
751
- skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0')
829
+ skip('in ruby version below 2.2.0 this error is not raised') if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2.0') || Gem::Version.new(RUBY_VERSION) > Gem::Version::new('2.6')
752
830
  json = '
753
831
  {
754
832
  "test": "something"
@@ -833,6 +911,193 @@ class TestJsonpath < MiniTest::Unit::TestCase
833
911
  assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]")
834
912
  end
835
913
 
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
942
+ json = '
943
+ {
944
+ "store": {
945
+ "book": [
946
+ {
947
+ "category": "reference",
948
+ "author": "Nigel Rees",
949
+ "title": "Sayings of the Century",
950
+ "price": 8.95
951
+ },
952
+ {
953
+ "category": "fiction",
954
+ "author": "Evelyn Waugh",
955
+ "title": "Sword of Honour",
956
+ "price": 12.99
957
+ }
958
+ ]
959
+ }
960
+ }
961
+ '.to_json
962
+
963
+ assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)')
964
+ end
965
+
966
+ def test_selecting_multiple_keys_on_array_with_filter
967
+ json = '
968
+ {
969
+ "store": {
970
+ "book": [
971
+ {
972
+ "category": "reference",
973
+ "author": "Nigel Rees",
974
+ "title": "Sayings of the Century",
975
+ "price": 8.95
976
+ },
977
+ {
978
+ "category": "fiction",
979
+ "author": "Evelyn Waugh",
980
+ "title": "Sword of Honour",
981
+ "price": 12.99
982
+ }
983
+ ]
984
+ }
985
+ }
986
+ '.to_json
987
+
988
+ assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)](category,author)")
989
+ assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( category, author )")
990
+ end
991
+
992
+ def test_selecting_multiple_keys_with_filter_with_space_in_catergory
993
+ json = '
994
+ {
995
+ "store": {
996
+ "book": [
997
+ {
998
+ "cate gory": "reference",
999
+ "author": "Nigel Rees",
1000
+ "title": "Sayings of the Century",
1001
+ "price": 8.95
1002
+ },
1003
+ {
1004
+ "cate gory": "fiction",
1005
+ "author": "Evelyn Waugh",
1006
+ "title": "Sword of Honour",
1007
+ "price": 12.99
1008
+ }
1009
+ ]
1010
+ }
1011
+ }
1012
+ '.to_json
1013
+
1014
+ assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )")
1015
+ end
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
+
1043
+ def test_object_method_send
1044
+ j = {height: 5, hash: "some_hash"}.to_json
1045
+ hs = JsonPath.new "$..send"
1046
+ assert_equal([], hs.on(j))
1047
+ hs = JsonPath.new "$..hash"
1048
+ assert_equal(["some_hash"], hs.on(j))
1049
+ hs = JsonPath.new "$..send"
1050
+ assert_equal([], hs.on(j))
1051
+ j = {height: 5, send: "should_still_work"}.to_json
1052
+ hs = JsonPath.new "$..send"
1053
+ assert_equal(['should_still_work'], hs.on(j))
1054
+ end
1055
+
1056
+ def test_index_access_by_number
1057
+ data = {
1058
+ '1': 'foo'
1059
+ }
1060
+ assert_equal ['foo'], JsonPath.new('$.1').on(data.to_json)
1061
+ end
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
+
836
1101
  def example_object
837
1102
  { 'store' => {
838
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.3
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: 2019-05-26 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
@@ -25,20 +25,6 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
- - !ruby/object:Gem::Dependency
29
- name: to_regexp
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - "~>"
33
- - !ruby/object:Gem::Version
34
- version: 0.2.1
35
- type: :runtime
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - "~>"
40
- - !ruby/object:Gem::Version
41
- version: 0.2.1
42
28
  - !ruby/object:Gem::Dependency
43
29
  name: bundler
44
30
  requirement: !ruby/object:Gem::Requirement
@@ -132,34 +118,35 @@ files:
132
118
  - bin/jsonpath
133
119
  - jsonpath.gemspec
134
120
  - lib/jsonpath.rb
121
+ - lib/jsonpath/dig.rb
135
122
  - lib/jsonpath/enumerable.rb
136
123
  - lib/jsonpath/parser.rb
137
124
  - lib/jsonpath/proxy.rb
138
125
  - lib/jsonpath/version.rb
139
126
  - test/test_jsonpath.rb
140
127
  - test/test_jsonpath_bin.rb
128
+ - test/test_readme.rb
141
129
  homepage: https://github.com/joshbuddy/jsonpath
142
130
  licenses:
143
131
  - MIT
144
132
  metadata: {}
145
- post_install_message:
146
- rdoc_options:
147
- - "--charset=UTF-8"
133
+ post_install_message:
134
+ rdoc_options: []
148
135
  require_paths:
149
136
  - lib
150
137
  required_ruby_version: !ruby/object:Gem::Requirement
151
138
  requirements:
152
139
  - - ">="
153
140
  - !ruby/object:Gem::Version
154
- version: '0'
141
+ version: '2.5'
155
142
  required_rubygems_version: !ruby/object:Gem::Requirement
156
143
  requirements:
157
144
  - - ">="
158
145
  - !ruby/object:Gem::Version
159
146
  version: '0'
160
147
  requirements: []
161
- rubygems_version: 3.0.3
162
- signing_key:
148
+ rubygems_version: 3.1.2
149
+ signing_key:
163
150
  specification_version: 4
164
151
  summary: Ruby implementation of http://goessner.net/articles/JsonPath/
165
152
  test_files: []