jsonpathv2 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/README.md +139 -0
- data/ROADMAP.md +13 -0
- data/Rakefile +12 -0
- data/bin/jsonpathv2 +22 -0
- data/jsonpathv2.gemspec +33 -0
- data/lib/jsonpathv2/enumerable.rb +147 -0
- data/lib/jsonpathv2/proxy.rb +55 -0
- data/lib/jsonpathv2/version.rb +3 -0
- data/lib/jsonpathv2.rb +92 -0
- data/test/test_jsonpath.rb +236 -0
- data/test/test_jsonpath_bin.rb +21 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 42c5754087dca72b09158229ea91cb5041026c27
|
4
|
+
data.tar.gz: c0d184648b89b424eb5e87a5d2ea4d034290ac2e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9fdb268247ff8e762fadad1875d9ce64f9c8884d72d7d2e1f4e35e06a5fa8fd0300b0ea1739931bfe0a4b675309db3675bc796692f457dba14adc51ed5bbac8e
|
7
|
+
data.tar.gz: be83d73bfa5b14d43f331067c99f806967f5f007f0bba985142be06bbf7af37391027abfd72cca38f62a781efb6ed2fa139c7b2e7b545f480eaffae39e38ddc5
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# Travis Build
|
2
|
+
[](https://travis-ci.org/Skarlso/jsonpathv2)
|
3
|
+
|
4
|
+
# JsonPath - Origin
|
5
|
+
|
6
|
+
This gem was forked, than re-written from this Gem: [JsonPath](https://github.com/joshbuddy/jsonpath). Since the original owner clearly abandoned that project, I took the liberty to fork it, and start fixing it. Please feel free to submit any issues you may encounter. PRs are very welcomed.
|
7
|
+
|
8
|
+
# JsonPath
|
9
|
+
|
10
|
+
This is an implementation of http://goessner.net/articles/JsonPath/.
|
11
|
+
|
12
|
+
## What is JsonPath?
|
13
|
+
|
14
|
+
JsonPath is a way of addressing elements within a JSON object. Similar to xpath of yore, JsonPath lets you
|
15
|
+
traverse a json object and manipulate or access it.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Command-line
|
20
|
+
|
21
|
+
There is stand-alone usage through the binary `jsonpathv2`
|
22
|
+
|
23
|
+
jsonpathv2 [expression] (file|string)
|
24
|
+
|
25
|
+
If you omit the second argument, it will read stdin, assuming one valid JSON object
|
26
|
+
per line. Expression must be a valid jsonpathv2 expression.
|
27
|
+
|
28
|
+
### Library
|
29
|
+
|
30
|
+
To use JsonPath as a library simply include and get goin'!
|
31
|
+
|
32
|
+
~~~~~ {ruby}
|
33
|
+
require 'jsonpathv2'
|
34
|
+
|
35
|
+
json = <<-HERE_DOC
|
36
|
+
{"store":
|
37
|
+
{"bicycle":
|
38
|
+
{"price":19.95, "color":"red"},
|
39
|
+
"book":[
|
40
|
+
{"price":8.95, "category":"reference", "title":"Sayings of the Century", "author":"Nigel Rees"},
|
41
|
+
{"price":12.99, "category":"fiction", "title":"Sword of Honour", "author":"Evelyn Waugh"},
|
42
|
+
{"price":8.99, "category":"fiction", "isbn":"0-553-21311-3", "title":"Moby Dick", "author":"Herman Melville","color":"blue"},
|
43
|
+
{"price":22.99, "category":"fiction", "isbn":"0-395-19395-8", "title":"The Lord of the Rings", "author":"Tolkien"}
|
44
|
+
]
|
45
|
+
}
|
46
|
+
}
|
47
|
+
HERE_DOC
|
48
|
+
~~~~~
|
49
|
+
|
50
|
+
Now that we have a JSON object, let's get all the prices present in the object. We create an object for the path
|
51
|
+
in the following way.
|
52
|
+
|
53
|
+
~~~~~ {ruby}
|
54
|
+
path = JsonPath.new('$..price')
|
55
|
+
~~~~~
|
56
|
+
|
57
|
+
Now that we have a path, let's apply it to the object above.
|
58
|
+
|
59
|
+
~~~~~ {ruby}
|
60
|
+
path.on(json)
|
61
|
+
# => [19.95, 8.95, 12.99, 8.99, 22.99]
|
62
|
+
~~~~~
|
63
|
+
|
64
|
+
Or on some other object ...
|
65
|
+
|
66
|
+
~~~~~ {ruby}
|
67
|
+
path.on('{"books":[{"title":"A Tale of Two Somethings","price":18.88}]}')
|
68
|
+
# => [18.88]
|
69
|
+
~~~~~
|
70
|
+
|
71
|
+
You can also just combine this into one mega-call with the convenient `JsonPath.on` method.
|
72
|
+
|
73
|
+
~~~~~ {ruby}
|
74
|
+
JsonPath.on(json, '$..author')
|
75
|
+
# => ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "Tolkien"]
|
76
|
+
~~~~~
|
77
|
+
|
78
|
+
Of course the full JsonPath syntax is supported, such as array slices
|
79
|
+
|
80
|
+
~~~~~ {ruby}
|
81
|
+
JsonPath.new('$..book[::2]').on(json)
|
82
|
+
# => [
|
83
|
+
# {"price"=>8.95, "category"=>"reference", "author"=>"Nigel Rees", "title"=>"Sayings of the Century"},
|
84
|
+
# {"price"=>8.99, "category"=>"fiction", "author"=>"Herman Melville", "title"=>"Moby Dick", "isbn"=>"0-553-21311-3"}
|
85
|
+
# ]
|
86
|
+
~~~~~
|
87
|
+
|
88
|
+
...and evals.
|
89
|
+
|
90
|
+
~~~~~ {ruby}
|
91
|
+
JsonPath.new('$..price[?(@ < 10)]').on(json)
|
92
|
+
# => [8.95, 8.99]
|
93
|
+
~~~~~
|
94
|
+
|
95
|
+
There is a convenience method, `#first` that gives you the first element for a JSON object and path.
|
96
|
+
|
97
|
+
~~~~~ {ruby}
|
98
|
+
JsonPath.new('$..color').first(object)
|
99
|
+
# => "red"
|
100
|
+
~~~~~
|
101
|
+
|
102
|
+
As well, we can directly create an `Enumerable` at any time using `#[]`.
|
103
|
+
|
104
|
+
~~~~~ {ruby}
|
105
|
+
enum = JsonPath.new('$..color')[object]
|
106
|
+
# => #<JsonPath::Enumerable:...>
|
107
|
+
enum.first
|
108
|
+
# => "red"
|
109
|
+
enum.any?{ |c| c == 'red' }
|
110
|
+
# => true
|
111
|
+
~~~~~
|
112
|
+
|
113
|
+
You can optionally prevent eval from being called on sub-expressions by passing in :allow_eval => false to the constructor.
|
114
|
+
|
115
|
+
### Manipulation
|
116
|
+
|
117
|
+
If you'd like to do substitution in a json object, you can use `#gsub` or `#gsub!` to modify the object in place.
|
118
|
+
|
119
|
+
~~~~~ {ruby}
|
120
|
+
JsonPath.for('{"candy":"lollipop"}').gsub('$..candy') {|v| "big turks" }.to_hash
|
121
|
+
~~~~~
|
122
|
+
|
123
|
+
The result will be
|
124
|
+
|
125
|
+
~~~~~ {ruby}
|
126
|
+
{'candy' => 'big turks'}
|
127
|
+
~~~~~
|
128
|
+
|
129
|
+
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:
|
130
|
+
|
131
|
+
~~~~~ {ruby}
|
132
|
+
json = '{"candy":"lollipop","noncandy":null,"other":"things"}'
|
133
|
+
o = JsonPath.for(json).
|
134
|
+
gsub('$..candy') {|v| "big turks" }.
|
135
|
+
compact.
|
136
|
+
delete('$..other').
|
137
|
+
to_hash
|
138
|
+
# => {"candy" => "big turks"}
|
139
|
+
~~~~~
|
data/ROADMAP.md
ADDED
data/Rakefile
ADDED
data/bin/jsonpathv2
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'jsonpathv2'
|
4
|
+
require 'multi_json'
|
5
|
+
|
6
|
+
def usage
|
7
|
+
puts "jsonpathv2 [expression] (file|string)
|
8
|
+
|
9
|
+
If you omit the second argument, it will read stdin, assuming one valid JSON
|
10
|
+
object per line. Expression must be a valid jsonpathv2 expression."
|
11
|
+
exit!
|
12
|
+
end
|
13
|
+
|
14
|
+
usage unless ARGV[0]
|
15
|
+
|
16
|
+
jsonpathv2 = JsonPath.new(ARGV[0])
|
17
|
+
case ARGV[1]
|
18
|
+
when nil # stdin
|
19
|
+
puts MultiJson.encode(jsonpathv2.on(MultiJson.decode(STDIN.read)))
|
20
|
+
when String
|
21
|
+
puts MultiJson.encode(jsonpathv2.on(MultiJson.decode(File.exist?(ARGV[1]) ? File.read(ARGV[1]) : ARGV[1])))
|
22
|
+
end
|
data/jsonpathv2.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'lib', 'jsonpathv2', 'version')
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'jsonpathv2'
|
7
|
+
s.version = JsonPath::VERSION
|
8
|
+
s.required_rubygems_version =
|
9
|
+
Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
10
|
+
s.authors = ['Gergely Brautigam']
|
11
|
+
s.summary = 'Ruby implementation of http://goessner.net/articles/JsonPath/'
|
12
|
+
s.description = 'Ruby implementation of http://goessner.net/articles/JsonPath/.'
|
13
|
+
s.email = 'skarlso777@gmail.com'
|
14
|
+
s.extra_rdoc_files = ['README.md']
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.homepage = 'https://github.com/Skarlso/jsonpathv2'
|
17
|
+
s.rdoc_options = ['--charset=UTF-8']
|
18
|
+
s.require_paths = ['lib']
|
19
|
+
s.rubygems_version = '1.3.7'
|
20
|
+
s.test_files = `git ls-files`.split("\n").select { |f| f =~ /^spec/ }
|
21
|
+
s.rubyforge_project = 'jsonpathv2'
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n")
|
23
|
+
.map { |f| File.basename(f) }
|
24
|
+
s.licenses = ['MIT']
|
25
|
+
|
26
|
+
# dependencies
|
27
|
+
s.add_runtime_dependency 'multi_json'
|
28
|
+
s.add_development_dependency 'code_stats'
|
29
|
+
s.add_development_dependency 'rake'
|
30
|
+
s.add_development_dependency 'minitest', '~> 2.2.0'
|
31
|
+
s.add_development_dependency 'phocus'
|
32
|
+
s.add_development_dependency 'bundler'
|
33
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
class JsonPath
|
2
|
+
class Enumerable
|
3
|
+
include ::Enumerable
|
4
|
+
attr_reader :allow_eval
|
5
|
+
alias_method :allow_eval?, :allow_eval
|
6
|
+
|
7
|
+
def initialize(path, object, mode, options = nil)
|
8
|
+
@path = path.path
|
9
|
+
@object = object
|
10
|
+
@mode = mode
|
11
|
+
@options = options
|
12
|
+
@allow_eval = if @options && @options.key?(:allow_eval)
|
13
|
+
@options[:allow_eval]
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def each(context = @object, key = nil, pos = 0, &blk)
|
20
|
+
node = key ? context[key] : context
|
21
|
+
@_current_node = node
|
22
|
+
return yield_value(blk, context, key) if pos == @path.size
|
23
|
+
case expr = @path[pos]
|
24
|
+
when '*', '..', '@'
|
25
|
+
each(context, key, pos + 1, &blk)
|
26
|
+
when '$'
|
27
|
+
each(context, key, pos + 1, &blk) if node == @object
|
28
|
+
when /^\[(.*)\]$/
|
29
|
+
expr[1, expr.size - 2].split(',').each do |sub_path|
|
30
|
+
case sub_path[0]
|
31
|
+
when '\'', '"'
|
32
|
+
if node.is_a?(Hash)
|
33
|
+
k = sub_path[1, sub_path.size - 2]
|
34
|
+
each(node, k, pos + 1, &blk) if node.key?(k)
|
35
|
+
end
|
36
|
+
when '?'
|
37
|
+
raise 'Cannot use ?(...) unless eval is enabled' unless allow_eval?
|
38
|
+
case node
|
39
|
+
when Array
|
40
|
+
node.size.times do |index|
|
41
|
+
@_current_node = node[index]
|
42
|
+
if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
43
|
+
each(@_current_node, nil, pos + 1, &blk)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
when Hash
|
47
|
+
if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
48
|
+
each(@_current_node, nil, pos + 1, &blk)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
yield node if process_function_or_literal(sub_path[1, sub_path.size - 1])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
if node.is_a?(Array)
|
55
|
+
next if node.empty?
|
56
|
+
array_args = sub_path.split(':')
|
57
|
+
if array_args[0] == '*'
|
58
|
+
start_idx = 0
|
59
|
+
end_idx = node.size - 1
|
60
|
+
else
|
61
|
+
start_idx = process_function_or_literal(array_args[0], 0)
|
62
|
+
next unless start_idx
|
63
|
+
end_idx = (array_args[1] && process_function_or_literal(array_args[1], -1) || (sub_path.count(':') == 0 ? start_idx : -1))
|
64
|
+
next unless end_idx
|
65
|
+
if start_idx == end_idx
|
66
|
+
next unless start_idx < node.size
|
67
|
+
end
|
68
|
+
end
|
69
|
+
start_idx %= node.size
|
70
|
+
end_idx %= node.size
|
71
|
+
step = process_function_or_literal(array_args[2], 1)
|
72
|
+
next unless step
|
73
|
+
(start_idx..end_idx).step(step) { |i| each(node, i, pos + 1, &blk) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
if pos == (@path.size - 1) && node && allow_eval?
|
79
|
+
if eval("node #{@path[pos]}")
|
80
|
+
yield_value(blk, context, key)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
if pos > 0 && @path[pos - 1] == '..'
|
86
|
+
case node
|
87
|
+
when Hash then node.each { |k, _| each(node, k, pos, &blk) }
|
88
|
+
when Array then node.each_with_index { |_, i| each(node, i, pos, &blk) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def yield_value(blk, context, key)
|
96
|
+
case @mode
|
97
|
+
when nil
|
98
|
+
blk.call(key ? context[key] : context)
|
99
|
+
when :compact
|
100
|
+
context.delete(key) if key && context[key].nil?
|
101
|
+
when :delete
|
102
|
+
context.delete(key) if key
|
103
|
+
when :substitute
|
104
|
+
if key
|
105
|
+
context[key] = blk.call(context[key])
|
106
|
+
else
|
107
|
+
context.replace(blk.call(context[key]))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def process_function_or_literal(exp, default = nil)
|
113
|
+
return default if exp.nil? || exp.empty?
|
114
|
+
return Integer(exp) if exp[0] != '('
|
115
|
+
return nil unless allow_eval? && @_current_node
|
116
|
+
|
117
|
+
identifiers = /@?(\.(\w+))+/.match(exp)
|
118
|
+
|
119
|
+
if !identifiers.nil? &&
|
120
|
+
!@_current_node.methods.include?(identifiers[2].to_sym)
|
121
|
+
|
122
|
+
exp_to_eval = exp.dup
|
123
|
+
exp_to_eval[identifiers[0]] = identifiers[0].split('.').map do |el|
|
124
|
+
el == '@' ? '@_current_node' : "['#{el}']"
|
125
|
+
end.join
|
126
|
+
|
127
|
+
begin
|
128
|
+
return eval(exp_to_eval)
|
129
|
+
# if eval failed because of bad arguments or missing methods
|
130
|
+
rescue StandardError
|
131
|
+
return default
|
132
|
+
end
|
133
|
+
end
|
134
|
+
# otherwise eval as is
|
135
|
+
# TODO: this eval is wrong, because hash accessor could be nil and nil
|
136
|
+
# cannot be compared with anything, for instance,
|
137
|
+
# @a_current_node['price'] - we can't be sure that 'price' are in every
|
138
|
+
# node, but it's only in several nodes I wrapped this eval into rescue
|
139
|
+
# returning false when error, but this eval should be refactored.
|
140
|
+
begin
|
141
|
+
eval(exp.gsub(/@/, '@_current_node'))
|
142
|
+
rescue
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class JsonPath
|
2
|
+
class Proxy
|
3
|
+
attr_reader :obj
|
4
|
+
alias_method :to_hash, :obj
|
5
|
+
|
6
|
+
def initialize(obj)
|
7
|
+
@obj = obj
|
8
|
+
end
|
9
|
+
|
10
|
+
def gsub(path, replacement = nil, &replacement_block)
|
11
|
+
_gsub(_deep_copy, path, replacement ? proc { replacement } : replacement_block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def gsub!(path, replacement = nil, &replacement_block)
|
15
|
+
_gsub(@obj, path, replacement ? proc { replacement } : replacement_block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(path = JsonPath::PATH_ALL)
|
19
|
+
_delete(_deep_copy, path)
|
20
|
+
end
|
21
|
+
|
22
|
+
def delete!(path = JsonPath::PATH_ALL)
|
23
|
+
_delete(@obj, path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def compact(path = JsonPath::PATH_ALL)
|
27
|
+
_compact(_deep_copy, path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def compact!(path = JsonPath::PATH_ALL)
|
31
|
+
_compact(@obj, path)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def _deep_copy
|
37
|
+
Marshal.load(Marshal.dump(@obj))
|
38
|
+
end
|
39
|
+
|
40
|
+
def _gsub(obj, path, replacement)
|
41
|
+
JsonPath.new(path)[obj, :substitute].each(&replacement)
|
42
|
+
Proxy.new(obj)
|
43
|
+
end
|
44
|
+
|
45
|
+
def _delete(obj, path)
|
46
|
+
JsonPath.new(path)[obj, :delete].each
|
47
|
+
Proxy.new(obj)
|
48
|
+
end
|
49
|
+
|
50
|
+
def _compact(obj, path)
|
51
|
+
JsonPath.new(path)[obj, :compact].each
|
52
|
+
Proxy.new(obj)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/jsonpathv2.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'multi_json'
|
3
|
+
require 'jsonpathv2/proxy'
|
4
|
+
require 'jsonpathv2/enumerable'
|
5
|
+
require 'jsonpathv2/version'
|
6
|
+
|
7
|
+
class JsonPath
|
8
|
+
PATH_ALL = '$..*'.freeze
|
9
|
+
|
10
|
+
attr_accessor :path
|
11
|
+
|
12
|
+
def initialize(path, opts = nil)
|
13
|
+
@opts = opts
|
14
|
+
scanner = StringScanner.new(path)
|
15
|
+
@path = []
|
16
|
+
until scanner.eos?
|
17
|
+
if token = scanner.scan(/\$|@|\*|\.\./)
|
18
|
+
@path << token
|
19
|
+
elsif token = scanner.scan(/[a-zA-Z0-9_-]+/)
|
20
|
+
@path << "['#{token}']"
|
21
|
+
elsif token = scanner.scan(/'(.*?)'/)
|
22
|
+
@path << "[#{token}]"
|
23
|
+
elsif token = scanner.scan(/\[/)
|
24
|
+
@path << find_matching_brackets(token, scanner)
|
25
|
+
elsif token = scanner.scan(/\]/)
|
26
|
+
raise ArgumentError, 'unmatched closing bracket'
|
27
|
+
elsif scanner.scan(/\./)
|
28
|
+
nil
|
29
|
+
elsif token = scanner.scan(/[><=] \d+/)
|
30
|
+
@path.last << token
|
31
|
+
elsif token = scanner.scan(/./)
|
32
|
+
@path.last << token
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_matching_brackets(token, scanner)
|
38
|
+
count = 1
|
39
|
+
until count.zero?
|
40
|
+
if t = scanner.scan(/\[/)
|
41
|
+
token << t
|
42
|
+
count += 1
|
43
|
+
elsif t = scanner.scan(/\]/)
|
44
|
+
token << t
|
45
|
+
count -= 1
|
46
|
+
elsif t = scanner.scan(/[^\[\]]+/)
|
47
|
+
token << t
|
48
|
+
elsif scanner.eos?
|
49
|
+
raise ArgumentError, 'unclosed bracket'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
token
|
53
|
+
end
|
54
|
+
|
55
|
+
def join(join_path)
|
56
|
+
res = deep_clone
|
57
|
+
res.path += JsonPath.new(join_path).path
|
58
|
+
res
|
59
|
+
end
|
60
|
+
|
61
|
+
def on(obj_or_str)
|
62
|
+
enum_on(obj_or_str).to_a
|
63
|
+
end
|
64
|
+
|
65
|
+
def first(obj_or_str, *args)
|
66
|
+
enum_on(obj_or_str).first(*args)
|
67
|
+
end
|
68
|
+
|
69
|
+
def enum_on(obj_or_str, mode = nil)
|
70
|
+
JsonPath::Enumerable.new(self, self.class.process_object(obj_or_str), mode,
|
71
|
+
@opts)
|
72
|
+
end
|
73
|
+
alias_method :[], :enum_on
|
74
|
+
|
75
|
+
def self.on(obj_or_str, path, opts = nil)
|
76
|
+
new(path, opts).on(process_object(obj_or_str))
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.for(obj_or_str)
|
80
|
+
Proxy.new(process_object(obj_or_str))
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def self.process_object(obj_or_str)
|
86
|
+
obj_or_str.is_a?(String) ? MultiJson.decode(obj_or_str) : obj_or_str
|
87
|
+
end
|
88
|
+
|
89
|
+
def deep_clone
|
90
|
+
Marshal.load Marshal.dump(self)
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
class TestJsonpath < MiniTest::Unit::TestCase
|
2
|
+
|
3
|
+
def setup
|
4
|
+
@object = example_object
|
5
|
+
@object2 = example_object
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_bracket_matching
|
9
|
+
assert_raises(ArgumentError) {
|
10
|
+
JsonPath.new('$.store.book[0')
|
11
|
+
}
|
12
|
+
assert_raises(ArgumentError) {
|
13
|
+
JsonPath.new('$.store.book[0]]')
|
14
|
+
}
|
15
|
+
assert_equal [9], JsonPath.new('$.store.book[0].price').on(@object)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_lookup_direct_path
|
19
|
+
assert_equal 7, JsonPath.new('$.store.*').on(@object).first['book'].size
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_lookup_missing_element
|
23
|
+
assert_equal [], JsonPath.new('$.store.book[99].price').on(@object)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_retrieve_all_authors
|
27
|
+
assert_equal [
|
28
|
+
@object['store']['book'][0]['author'],
|
29
|
+
@object['store']['book'][1]['author'],
|
30
|
+
@object['store']['book'][2]['author'],
|
31
|
+
@object['store']['book'][3]['author'],
|
32
|
+
@object['store']['book'][4]['author'],
|
33
|
+
@object['store']['book'][5]['author'],
|
34
|
+
@object['store']['book'][6]['author']
|
35
|
+
], JsonPath.new('$..author').on(@object)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_retrieve_all_prices
|
39
|
+
assert_equal [
|
40
|
+
@object['store']['bicycle']['price'],
|
41
|
+
@object['store']['book'][0]['price'],
|
42
|
+
@object['store']['book'][1]['price'],
|
43
|
+
@object['store']['book'][2]['price'],
|
44
|
+
@object['store']['book'][3]['price']
|
45
|
+
].sort, JsonPath.new('$..price').on(@object).sort
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_recognize_array_splices
|
49
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0:1:1]').on(@object)
|
50
|
+
assert_equal [@object['store']['book'][1], @object['store']['book'][3], @object['store']['book'][5]], JsonPath.new('$..book[1::2]').on(@object)
|
51
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][2], @object['store']['book'][4], @object['store']['book'][6]], JsonPath.new('$..book[::2]').on(@object)
|
52
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new('$..book[:-5:2]').on(@object)
|
53
|
+
assert_equal [@object['store']['book'][5], @object['store']['book'][6]], JsonPath.new('$..book[5::]').on(@object)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_recognize_array_comma
|
57
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][1]], JsonPath.new('$..book[0,1]').on(@object)
|
58
|
+
assert_equal [@object['store']['book'][2], @object['store']['book'][6]], JsonPath.new('$..book[2,-1::]').on(@object)
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_recognize_filters
|
62
|
+
assert_equal [@object['store']['book'][2], @object['store']['book'][3]], JsonPath.new("$..book[?(@['isbn'])]").on(@object)
|
63
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] < 10)]").on(@object)
|
64
|
+
assert_equal [@object['store']['book'][0], @object['store']['book'][2]], JsonPath.new("$..book[?(@['price'] == 9)]").on(@object)
|
65
|
+
assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@['price'] > 20)]").on(@object)
|
66
|
+
end
|
67
|
+
|
68
|
+
if RUBY_VERSION[/^1\.9/]
|
69
|
+
def test_recognize_filters_on_val
|
70
|
+
assert_equal [@object['store']['book'][1]['price'], @object['store']['book'][3]['price'], @object['store']['bicycle']['price']], JsonPath.new("$..price[?(@ > 10)]").on(@object)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_no_eval
|
75
|
+
assert_equal [], JsonPath.new('$..book[(@.length-2)]', :allow_eval => false).on(@object)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_paths_with_underscores
|
79
|
+
assert_equal [@object['store']['bicycle']['catalogue_number']], JsonPath.new('$.store.bicycle.catalogue_number').on(@object)
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_path_with_hyphens
|
83
|
+
assert_equal [@object['store']['bicycle']['single-speed']], JsonPath.new('$.store.bicycle.single-speed').on(@object)
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_paths_with_numbers
|
87
|
+
assert_equal [@object['store']['bicycle']['2seater']], JsonPath.new('$.store.bicycle.2seater').on(@object)
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_recognize_array_with_evald_index
|
91
|
+
assert_equal [@object['store']['book'][2]], JsonPath.new('$..book[(@.length-5)]').on(@object)
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_use_first
|
95
|
+
assert_equal @object['store']['book'][2], JsonPath.new('$..book[(@.length-5)]').first(@object)
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_counting
|
99
|
+
assert_equal 49, JsonPath.new('$..*').on(@object).to_a.size
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_space_in_path
|
103
|
+
assert_equal ['e'], JsonPath.new("$.'c d'").on({"a" => "a","b" => "b", "c d" => "e"})
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_class_method
|
107
|
+
assert_equal JsonPath.new('$..author').on(@object), JsonPath.on(@object, '$..author')
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_join
|
111
|
+
assert_equal JsonPath.new('$.store.book..author').on(@object), JsonPath.new('$.store').join('book..author').on(@object)
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_gsub
|
115
|
+
@object2['store']['bicycle']['price'] += 10
|
116
|
+
@object2['store']['book'][0]['price'] += 10
|
117
|
+
@object2['store']['book'][1]['price'] += 10
|
118
|
+
@object2['store']['book'][2]['price'] += 10
|
119
|
+
@object2['store']['book'][3]['price'] += 10
|
120
|
+
assert_equal @object2, JsonPath.for(@object).gsub('$..price') { |p| p + 10 }.to_hash
|
121
|
+
end
|
122
|
+
|
123
|
+
def test_gsub!
|
124
|
+
JsonPath.for(@object).gsub!('$..price') { |p| p + 10 }
|
125
|
+
assert_equal 30, @object['store']['bicycle']['price']
|
126
|
+
assert_equal 19, @object['store']['book'][0]['price']
|
127
|
+
assert_equal 23, @object['store']['book'][1]['price']
|
128
|
+
assert_equal 19, @object['store']['book'][2]['price']
|
129
|
+
assert_equal 33, @object['store']['book'][3]['price']
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_weird_gsub!
|
133
|
+
h = {'hi' => 'there'}
|
134
|
+
JsonPath.for(@object).gsub!('$.*') { |n| h }
|
135
|
+
assert_equal h, @object
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_compact
|
139
|
+
h = {'hi' => 'there', 'you' => nil}
|
140
|
+
JsonPath.for(h).compact!
|
141
|
+
assert_equal({'hi' => 'there'}, h)
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_delete
|
145
|
+
h = {'hi' => 'there', 'you' => nil}
|
146
|
+
JsonPath.for(h).delete!('*.hi')
|
147
|
+
assert_equal({'you' => nil}, h)
|
148
|
+
end
|
149
|
+
|
150
|
+
def test_wildcard
|
151
|
+
assert_equal @object['store']['book'].collect{|e| e['price']}.compact, JsonPath.on(@object, '$..book[*].price')
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_wildcard_empty_array
|
155
|
+
object = @object.merge("bicycle" => { "tire" => [] })
|
156
|
+
assert_equal [], JsonPath.on(object, "$..bicycle.tire[*]")
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_support_filter_by_array_childnode_value
|
160
|
+
assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@.price > 20)]").on(@object)
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_support_filter_by_childnode_value_with_inconsistent_children
|
164
|
+
@object['store']['book'][0] = "string_instead_of_object"
|
165
|
+
assert_equal [@object['store']['book'][3]], JsonPath.new("$..book[?(@.price > 20)]").on(@object)
|
166
|
+
end
|
167
|
+
|
168
|
+
def test_support_filter_by_childnode_value_and_select_child_key
|
169
|
+
assert_equal [23], JsonPath.new("$..book[?(@.price > 20)].price").on(@object)
|
170
|
+
end
|
171
|
+
|
172
|
+
def test_support_filter_by_childnode_value_over_childnode_and_select_child_key
|
173
|
+
assert_equal ["Osennie Vizity"], JsonPath.new("$..book[?(@.written.year == 1996)].title").on(@object)
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_support_filter_by_object_childnode_value
|
177
|
+
data = {
|
178
|
+
"data" => {
|
179
|
+
"type" => "users",
|
180
|
+
"id" => "123"
|
181
|
+
}
|
182
|
+
}
|
183
|
+
assert_equal [{"type"=>"users", "id"=>"123"}], JsonPath.new("$.data[?(@.type == 'users')]").on(data)
|
184
|
+
assert_equal [], JsonPath.new("$.data[?(@.type == 'admins')]").on(data)
|
185
|
+
end
|
186
|
+
|
187
|
+
def example_object
|
188
|
+
{ 'store' => {
|
189
|
+
'book' => [
|
190
|
+
{ 'category' => 'reference',
|
191
|
+
'author' => 'Nigel Rees',
|
192
|
+
'title' => 'Sayings of the Century',
|
193
|
+
'price' => 9 },
|
194
|
+
{ 'category' => 'fiction',
|
195
|
+
'author' => 'Evelyn Waugh',
|
196
|
+
'title' => 'Sword of Honour',
|
197
|
+
'price' => 13 },
|
198
|
+
{ 'category' => 'fiction',
|
199
|
+
'author' => 'Herman Melville',
|
200
|
+
'title' => 'Moby Dick',
|
201
|
+
'isbn' => '0-553-21311-3',
|
202
|
+
'price' => 9 },
|
203
|
+
{ 'category' => 'fiction',
|
204
|
+
'author' => 'J. R. R. Tolkien',
|
205
|
+
'title' => 'The Lord of the Rings',
|
206
|
+
'isbn' => '0-395-19395-8',
|
207
|
+
'price' => 23 },
|
208
|
+
{ 'category' => 'russian_fiction',
|
209
|
+
'author' => 'Lukyanenko',
|
210
|
+
'title' => 'Imperatory Illuziy',
|
211
|
+
'written' => {
|
212
|
+
'year' => 1995
|
213
|
+
} },
|
214
|
+
{ 'category' => 'russian_fiction',
|
215
|
+
'author' => 'Lukyanenko',
|
216
|
+
'title' => 'Osennie Vizity',
|
217
|
+
'written' => {
|
218
|
+
'year' => 1996
|
219
|
+
} },
|
220
|
+
{ 'category' => 'russian_fiction',
|
221
|
+
'author' => 'Lukyanenko',
|
222
|
+
'title' => 'Ne vremya dlya drakonov',
|
223
|
+
'written' => {
|
224
|
+
'year' => 1997
|
225
|
+
} }
|
226
|
+
],
|
227
|
+
'bicycle' => {
|
228
|
+
'color' => 'red',
|
229
|
+
'price' => 20,
|
230
|
+
'catalogue_number' => 123_45,
|
231
|
+
'single-speed' => 'no',
|
232
|
+
'2seater' => 'yes'
|
233
|
+
}
|
234
|
+
} }
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class TestJsonpathBin < MiniTest::Unit::TestCase
|
2
|
+
def setup
|
3
|
+
@runner = 'ruby -Ilib bin/jsonpathv2'
|
4
|
+
@original_dir = Dir.pwd
|
5
|
+
Dir.chdir(File.join(File.dirname(__FILE__), '..'))
|
6
|
+
end
|
7
|
+
|
8
|
+
def teardown
|
9
|
+
Dir.chdir(@original_dir)
|
10
|
+
`rm /tmp/test.json`
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_stdin
|
14
|
+
assert_equal '["time"]', `echo '{"test": "time"}' | #{@runner} '$.test'`.strip
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_stdin
|
18
|
+
File.open('/tmp/test.json', 'w') { |f| f << '{"test": "time"}' }
|
19
|
+
assert_equal '["time"]', `#{@runner} '$.test' /tmp/test.json`.strip
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jsonpathv2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gergely Brautigam
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: multi_json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: code_stats
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.2.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: phocus
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Ruby implementation of http://goessner.net/articles/JsonPath/.
|
98
|
+
email: skarlso777@gmail.com
|
99
|
+
executables:
|
100
|
+
- jsonpathv2
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files:
|
103
|
+
- README.md
|
104
|
+
files:
|
105
|
+
- ".rspec"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- README.md
|
109
|
+
- ROADMAP.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/jsonpathv2
|
112
|
+
- jsonpathv2.gemspec
|
113
|
+
- lib/jsonpathv2.rb
|
114
|
+
- lib/jsonpathv2/enumerable.rb
|
115
|
+
- lib/jsonpathv2/proxy.rb
|
116
|
+
- lib/jsonpathv2/version.rb
|
117
|
+
- test/test_jsonpath.rb
|
118
|
+
- test/test_jsonpath_bin.rb
|
119
|
+
homepage: https://github.com/Skarlso/jsonpathv2
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata: {}
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options:
|
125
|
+
- "--charset=UTF-8"
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project: jsonpathv2
|
140
|
+
rubygems_version: 2.6.6
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: Ruby implementation of http://goessner.net/articles/JsonPath/
|
144
|
+
test_files: []
|