jsonpath 1.0.5 → 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: 2eda27c92a0608bfe616796cdfdfd8020db7e91f1246533d04e8d141fd2df17c
4
- data.tar.gz: 12bb579fd9915045afeffc02f6ef9c911b5dcf97597c20c8f7c2990bcacc58df
3
+ metadata.gz: 27b1b5e54ebef03b9cde7f447c1dc6fb8e411b4a28f5039426c87782b250ab1a
4
+ data.tar.gz: ae08f25885125bde4f301d905b9c6446bf0e922516aabce101a4e90088fd6e68
5
5
  SHA512:
6
- metadata.gz: b7ad942b6146950aed615c56d3944f7888cded5dba6eeb76048db8489e56d19e978c380f43befa4a4914437e1880d4fddc3b0df8b447cea1c5dc60a32548b217
7
- data.tar.gz: 1d79042eefed77ca6956f6a75a148a695a9851afe6f72d99a490802a6aaaaf5abc84653fdc5aa296a31bf6151ac1965c2f6d85c55efd4915d3ced951eba11a40
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
data/.travis.yml CHANGED
@@ -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,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
+ ```
data/jsonpath.gemspec CHANGED
@@ -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'
data/lib/jsonpath.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'strscan'
4
4
  require 'multi_json'
5
5
  require 'jsonpath/proxy'
6
+ require 'jsonpath/dig'
6
7
  require 'jsonpath/enumerable'
7
8
  require 'jsonpath/version'
8
9
  require 'jsonpath/parser'
@@ -12,10 +13,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] == '*'
@@ -128,10 +121,9 @@ class JsonPath
128
121
  end
129
122
 
130
123
  def yield_value(blk, context, key)
131
- key = Integer(key) rescue key if key
132
124
  case @mode
133
125
  when nil
134
- blk.call(key ? context[key] : context)
126
+ blk.call(key ? dig_one(context, key) : context)
135
127
  when :compact
136
128
  if key && context[key].nil?
137
129
  key.is_a?(Integer) ? context.delete_at(key) : context.delete(key)
@@ -163,12 +155,12 @@ class JsonPath
163
155
  el == '@' ? '@' : "['#{el}']"
164
156
  end.join
165
157
  begin
166
- return JsonPath::Parser.new(@_current_node).parse(exp_to_eval)
158
+ return JsonPath::Parser.new(@_current_node, @options).parse(exp_to_eval)
167
159
  rescue StandardError
168
160
  return default
169
161
  end
170
162
  end
171
- JsonPath::Parser.new(@_current_node).parse(exp)
163
+ JsonPath::Parser.new(@_current_node, @options).parse(exp)
172
164
  end
173
165
  end
174
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.
@@ -71,13 +75,16 @@ class JsonPath
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.5'
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,7 +911,34 @@ class TestJsonpath < MiniTest::Unit::TestCase
833
911
  assert_equal [], JsonPath.on(json, "$.phoneNumbers[?(@[0].type == 'home')]")
834
912
  end
835
913
 
836
- 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
837
942
  json = '
838
943
  {
839
944
  "store": {
@@ -858,7 +963,7 @@ class TestJsonpath < MiniTest::Unit::TestCase
858
963
  assert_equal [{ 'category' => 'reference', 'author' => 'Nigel Rees' }, { 'category' => 'fiction', 'author' => 'Evelyn Waugh' }], JsonPath.on(json, '$.store.book[*](category,author)')
859
964
  end
860
965
 
861
- def test_selecting_multiple_keys_with_filter
966
+ def test_selecting_multiple_keys_on_array_with_filter
862
967
  json = '
863
968
  {
864
969
  "store": {
@@ -909,6 +1014,32 @@ class TestJsonpath < MiniTest::Unit::TestCase
909
1014
  assert_equal [{ 'cate gory' => 'reference', 'author' => 'Nigel Rees' }], JsonPath.on(json, "$.store.book[?(@['price'] == 8.95)]( cate gory, author )")
910
1015
  end
911
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
+
912
1043
  def test_object_method_send
913
1044
  j = {height: 5, hash: "some_hash"}.to_json
914
1045
  hs = JsonPath.new "$..send"
@@ -922,6 +1053,51 @@ class TestJsonpath < MiniTest::Unit::TestCase
922
1053
  assert_equal(['should_still_work'], hs.on(j))
923
1054
  end
924
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
+
925
1101
  def example_object
926
1102
  { 'store' => {
927
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.5
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-10-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
@@ -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.6
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: []