bruce-jsonpath 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +3 -0
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.markdown +102 -0
- data/Rakefile +66 -0
- data/VERSION +1 -0
- data/lib/jsonpath/nodes.rb +213 -0
- data/lib/jsonpath/parser.rb +1854 -0
- data/lib/jsonpath/parser.treetop +106 -0
- data/lib/jsonpath.rb +22 -0
- data/test/parser_test.rb +180 -0
- data/test/reference_test.rb +136 -0
- data/test/test_helper.rb +37 -0
- metadata +77 -0
data/.document
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Bruce Williams
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# JSONPath
|
2
|
+
|
3
|
+
JSONPath support for Ruby.
|
4
|
+
|
5
|
+
For more information on JSONPath, see [Stefan Goessner's blog entry] [1], or
|
6
|
+
the [JS and PHP implementations] [2] on Google Code.
|
7
|
+
|
8
|
+
## Installing
|
9
|
+
|
10
|
+
gem install bruce-jsonpath --source 'http://gems.github.com'
|
11
|
+
|
12
|
+
### Dependencies
|
13
|
+
|
14
|
+
JSONPath uses [Treetop] [3] for parsing.
|
15
|
+
|
16
|
+
## Completeness
|
17
|
+
|
18
|
+
As of 2009-07-17, this implementation passes all tests from the
|
19
|
+
[JS and PHP implementations] [2] (after modifying the script expressions
|
20
|
+
for Ruby) -- in addition to its own expanded test suite.
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
Execute JSONPath queries against a Ruby data
|
25
|
+
structure (as would be parsed from JSON using the `json` or `yajl` gems).
|
26
|
+
|
27
|
+
Only one method is needed:
|
28
|
+
|
29
|
+
JSONPath.lookup(hash_or_array, path)
|
30
|
+
|
31
|
+
### Features
|
32
|
+
|
33
|
+
It supports hash traversal by key:
|
34
|
+
|
35
|
+
JSONPath.lookup({"a" => 1}, '$.a')
|
36
|
+
# => [1]
|
37
|
+
JSONPath.lookup({"foo" => {"bar baz" => 2}}, "$.foo['bar baz']")
|
38
|
+
# => [2]
|
39
|
+
|
40
|
+
Array traversal by index, including `start:stop:step` slices:
|
41
|
+
|
42
|
+
JSONPath.lookup([1, 2, [3, 4, 5], 6], '$[2][-2:]')
|
43
|
+
# => [4, 5]
|
44
|
+
|
45
|
+
Wildcards:
|
46
|
+
|
47
|
+
JSONPath.lookup({"a" => {"b" => 3, "c" => 2}}, "$.a.*")
|
48
|
+
# => [3, 2]
|
49
|
+
|
50
|
+
Descendant traversal (think `//` in XPath):
|
51
|
+
|
52
|
+
JSONPath.lookup({'e' => 1, 'b' => [{'e' => 3}]}, '$..e')
|
53
|
+
# => [1, 3]
|
54
|
+
|
55
|
+
Peek at the tests for more ideas.
|
56
|
+
|
57
|
+
#### Experimental Support
|
58
|
+
|
59
|
+
It has experimental support for JSONPath's script expressions, including
|
60
|
+
filters. Since JSONPath uses the underlying language in script expressions,
|
61
|
+
that means we have access to Ruby (supporting arbitrarily complex traversal).
|
62
|
+
As in other JSONPath implementations, `@` is replaced by the current node.
|
63
|
+
|
64
|
+
lists = [
|
65
|
+
[1, 2, 3, 4],
|
66
|
+
[5, 6],
|
67
|
+
[7, 8, 9, 10]
|
68
|
+
]
|
69
|
+
JSONPath.lookup(lists, "$.*[(@.length - 1)]")
|
70
|
+
=> [4, 6, 10]
|
71
|
+
|
72
|
+
And filters:
|
73
|
+
|
74
|
+
books = [
|
75
|
+
{"name" => 'Bruce', "age" => 29},
|
76
|
+
{"name" => "Braedyn", "age" => 3},
|
77
|
+
{"name" => "Jamis", "age" => 2},
|
78
|
+
]
|
79
|
+
JSONPath.lookup(people, "$[?(@['age'] % 2 == 0)].name")
|
80
|
+
# => ['Jamis']
|
81
|
+
|
82
|
+
For more information, see the the [JSONPath introductory article] [1].
|
83
|
+
|
84
|
+
## Contributing and Reporting Issues
|
85
|
+
|
86
|
+
The [project] [4] is hosted on [GitHub] [5], where I gladly accept pull
|
87
|
+
requests.
|
88
|
+
|
89
|
+
If you run into any problems, please either (in order of preference) post
|
90
|
+
something on the [issue tracker] [6], send me a message on GitHub, or email me.
|
91
|
+
|
92
|
+
## Copyright
|
93
|
+
|
94
|
+
Copyright (c) 2009 Bruce Williams, based on work by Stefan Goessner.
|
95
|
+
See LICENSE.
|
96
|
+
|
97
|
+
[1]: http://goessner.net/articles/JsonPath/
|
98
|
+
[2]: http://code.google.com/p/jsonpath/
|
99
|
+
[3]: http://treetop.rubyforge.org/
|
100
|
+
[4]: http://github.com/bruce/jsonpath
|
101
|
+
[5]: http://github.com/
|
102
|
+
[6]: http://github.com/bruce/jsonpath/issues
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "jsonpath"
|
8
|
+
gem.summary = %Q{JSONPath support for Ruby}
|
9
|
+
gem.email = "bruce@codefluency.com"
|
10
|
+
gem.homepage = "http://github.com/bruce/jsonpath"
|
11
|
+
gem.authors = ["Bruce Williams"]
|
12
|
+
gem.add_dependency 'treetop'
|
13
|
+
# gem is a Gem::Specification... see http:// www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
18
|
+
end
|
19
|
+
|
20
|
+
require 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/**/*_test.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'rcov/rcovtask'
|
29
|
+
Rcov::RcovTask.new do |test|
|
30
|
+
test.libs << 'test'
|
31
|
+
test.pattern = 'test/**/*_test.rb'
|
32
|
+
test.verbose = true
|
33
|
+
test.rcov_opts << '--exclude gems'
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
task :default => :test
|
43
|
+
|
44
|
+
require 'rake/rdoctask'
|
45
|
+
Rake::RDocTask.new do |rdoc|
|
46
|
+
if File.exist?('VERSION.yml')
|
47
|
+
config = YAML.load(File.read('VERSION.yml'))
|
48
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
49
|
+
else
|
50
|
+
version = ""
|
51
|
+
end
|
52
|
+
|
53
|
+
rdoc.rdoc_dir = 'rdoc'
|
54
|
+
rdoc.title = "jsonpath #{version}"
|
55
|
+
rdoc.rdoc_files.include('README*')
|
56
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
57
|
+
end
|
58
|
+
|
59
|
+
file 'lib/jsonpath/parser.rb' => 'lib/jsonpath/parser.treetop' do |t|
|
60
|
+
sh "tt 'lib/jsonpath/parser.treetop'"
|
61
|
+
end
|
62
|
+
|
63
|
+
task :treetop => 'lib/jsonpath/parser.rb'
|
64
|
+
|
65
|
+
|
66
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.8.0
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module JSONPath
|
2
|
+
|
3
|
+
module Nodes
|
4
|
+
|
5
|
+
class RootNode < Treetop::Runtime::SyntaxNode
|
6
|
+
def walk(object)
|
7
|
+
selectors.elements.inject([object]) do |reduce, selector|
|
8
|
+
selector.descend(*reduce)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class PathNode < Treetop::Runtime::SyntaxNode
|
14
|
+
|
15
|
+
def traversing_descendants?
|
16
|
+
respond_to?(:lower) && lower.text_value == '..'
|
17
|
+
end
|
18
|
+
|
19
|
+
def traverse(obj, &block)
|
20
|
+
if !respond_to?(:lower) || lower.text_value == '.'
|
21
|
+
obj.each(&block)
|
22
|
+
elsif lower.text_value == '..'
|
23
|
+
obj.each do |o|
|
24
|
+
recurse(o, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def recurse(obj, &block)
|
30
|
+
block.call(obj)
|
31
|
+
children = case obj
|
32
|
+
when Hash
|
33
|
+
obj.values
|
34
|
+
when Array
|
35
|
+
obj
|
36
|
+
else
|
37
|
+
return
|
38
|
+
end
|
39
|
+
children.each do |child|
|
40
|
+
recurse(child, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class WildcardNode < PathNode
|
47
|
+
def descend(*objects)
|
48
|
+
results = []
|
49
|
+
traverse(objects) do |obj|
|
50
|
+
values = case obj
|
51
|
+
when Hash
|
52
|
+
obj.values
|
53
|
+
when Array
|
54
|
+
obj
|
55
|
+
else
|
56
|
+
next
|
57
|
+
end
|
58
|
+
results.push(*values)
|
59
|
+
# Note: I really don't like this special case. This happens
|
60
|
+
# because when wildcarding regularly, the results are the *children*,
|
61
|
+
# but when using a .. descendant selector, you want the main parent,
|
62
|
+
# too. According to the JSONPath docs, '$..*' means "All members of
|
63
|
+
# [the] JSON structure." Should this support Array, as well?
|
64
|
+
if obj.is_a?(Hash) && traversing_descendants?
|
65
|
+
results.push(obj) unless results.include?(obj)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
results
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class KeyNode < PathNode
|
73
|
+
|
74
|
+
# supports finding the key from self or child elements
|
75
|
+
def find_keys(node=self, results=[], checked=[])
|
76
|
+
if node.respond_to?(:key)
|
77
|
+
results << node.key.text_value
|
78
|
+
end
|
79
|
+
if node.elements && !node.elements.empty?
|
80
|
+
node.elements.each do |element|
|
81
|
+
find_keys(element, results)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
results
|
85
|
+
end
|
86
|
+
|
87
|
+
def descend(*objects)
|
88
|
+
results = []
|
89
|
+
keys = find_keys
|
90
|
+
traverse(objects) do |obj|
|
91
|
+
if obj.is_a?(Hash)
|
92
|
+
keys.each do |key|
|
93
|
+
if obj.key?(key)
|
94
|
+
results << obj[key]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
results
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class IndexNode < PathNode
|
104
|
+
def descend(*objects)
|
105
|
+
offset = Integer(index.text_value)
|
106
|
+
results = []
|
107
|
+
traverse(objects) do |obj|
|
108
|
+
if obj.is_a?(Array)
|
109
|
+
if obj.size > offset
|
110
|
+
results << obj[offset]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
results
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class SliceNode < PathNode
|
119
|
+
|
120
|
+
def descend(*objects)
|
121
|
+
results = []
|
122
|
+
traverse(objects) do |obj|
|
123
|
+
if obj.is_a?(Array)
|
124
|
+
values = obj[start_offset..stop_offset(obj)]
|
125
|
+
0.step(values.size - 1, step_size) do |n|
|
126
|
+
results << values[n]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
results
|
131
|
+
end
|
132
|
+
|
133
|
+
def start_offset
|
134
|
+
@start_offset ||= Integer(start.text_value)
|
135
|
+
end
|
136
|
+
|
137
|
+
def stop_offset(obj)
|
138
|
+
@stop_offset ||= if respond_to?(:stop)
|
139
|
+
Integer(stop.text_value)
|
140
|
+
else
|
141
|
+
obj.size
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def step_size
|
146
|
+
@step_size ||= if respond_to?(:step)
|
147
|
+
Integer(step.text_value)
|
148
|
+
else
|
149
|
+
1
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
class CodeNode < PathNode
|
156
|
+
|
157
|
+
def code
|
158
|
+
@code ||= begin
|
159
|
+
text = template_code.text_value
|
160
|
+
text.gsub('@', '(obj)').gsub('\\(obj)', '@')
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def execute(obj)
|
165
|
+
eval(code, binding)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
class ExprNode < CodeNode
|
171
|
+
|
172
|
+
def descend(*objects)
|
173
|
+
results = []
|
174
|
+
traverse(objects) do |obj|
|
175
|
+
res = execute(obj)
|
176
|
+
case obj
|
177
|
+
when Hash
|
178
|
+
next unless obj.key?(res)
|
179
|
+
when Array
|
180
|
+
next unless obj.size > res
|
181
|
+
else
|
182
|
+
next
|
183
|
+
end
|
184
|
+
results << obj[res]
|
185
|
+
end
|
186
|
+
results
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
class FilterNode < CodeNode
|
192
|
+
|
193
|
+
class Error < ::ArgumentError; end
|
194
|
+
|
195
|
+
def descend(*objects)
|
196
|
+
results = []
|
197
|
+
traverse(objects) do |set|
|
198
|
+
next unless set.is_a?(Array) || set.is_a?(Hash)
|
199
|
+
values = set.is_a?(Array) ? set : set.values
|
200
|
+
values.each do |obj|
|
201
|
+
if execute(obj)
|
202
|
+
results << obj
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
results
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|