jsonpathv2 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/Skarlso/jsonpathv2.svg?branch=master)](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: []
|