jpath 0.1
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/LICENSE +21 -0
- data/README.md +58 -0
- data/Rakefile +13 -0
- data/lib/jpath.rb +21 -0
- data/lib/jpath/item.rb +138 -0
- data/lib/jpath/parser.rb +128 -0
- data/lib/jpath/parser/formula.rb +225 -0
- data/lib/jpath/parser/path.rb +53 -0
- data/lib/jpath/parser/step.rb +126 -0
- data/lib/jpath/pointer.rb +65 -0
- data/spec/item_spec.rb +84 -0
- data/spec/jpath_spec.rb +59 -0
- data/spec/parser_spec.rb +36 -0
- data/spec/pointer_spec.rb +77 -0
- data/spec/spec_helper.rb +33 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 457110306795a1ccec58c95678abe4ffba72799a
|
4
|
+
data.tar.gz: d36bbbe5ffc6d83dc9b2d48842a6edb05320c839
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ec250f68aa9480e96ff7ed5fba174eecdecf7bb89b14ba584f1b8f945c7e81bab2a34cc287e3f4ac90b3aa04fde54fe99a418713a366d0c10abd2b61229ac539
|
7
|
+
data.tar.gz: 1379625372cb7675bac5cb2c9ac400748d26559b70147ad3eec33b52fa41e027bb2ab6c7aa9b3ce724e45244523aca666a474af6773c882bc169f75efe4fe1a9
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Merimond Corporation
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# JPath
|
2
|
+
|
3
|
+
JPath is a Ruby library that allows to execute XPath queries on JSON documents.
|
4
|
+
|
5
|
+
### Installation
|
6
|
+
|
7
|
+
JPath doesn't use any dependencies. Simply install via rubygems:
|
8
|
+
|
9
|
+
gem install jpath
|
10
|
+
|
11
|
+
or clone it from github:
|
12
|
+
|
13
|
+
git clone https://github.com/merimond/jpath.git
|
14
|
+
|
15
|
+
JPath is in early beta stage. So having an up-to-date Git version is more preferable.
|
16
|
+
|
17
|
+
## Library
|
18
|
+
|
19
|
+
Please note that JPath works with *standard* XPath syntax, not one of XPath alternatives, such as the great [JSONPath](http://goessner.net/articles/JsonPath/) syntax by Stefan Goessner. If you prefer the latter, there's a [terrific library](https://github.com/joshbuddy/jsonpath) by Joshua Hull that you should check out.
|
20
|
+
|
21
|
+
### A few caveats
|
22
|
+
|
23
|
+
Although JSON and XML are both data containers, they are quite different when it comes to searching and manipulating data. So JPath _may_ behave somewhat differently from what you woud expect, although we tried to make some sensible assumptions while transforming the XML-oriented search paradigm into the JSON world.
|
24
|
+
|
25
|
+
## Features
|
26
|
+
|
27
|
+
JPath support all the basic and some of the more advanced XPath functions:
|
28
|
+
|
29
|
+
Simple chains with children and ancestors:
|
30
|
+
|
31
|
+
/store/book/author
|
32
|
+
//author
|
33
|
+
/store/*
|
34
|
+
|
35
|
+
Position predicates:
|
36
|
+
|
37
|
+
//book[last()]
|
38
|
+
//book[position()<3]
|
39
|
+
|
40
|
+
Attribute and child predicates:
|
41
|
+
|
42
|
+
//book[@price<10]
|
43
|
+
//book[isbn]
|
44
|
+
|
45
|
+
Multiple predicates:
|
46
|
+
|
47
|
+
//book[@price>10][last()]
|
48
|
+
|
49
|
+
And all of the above in non-abbreviated form:
|
50
|
+
|
51
|
+
/descendant::book
|
52
|
+
/child::book[attribute::price="8.95"]
|
53
|
+
|
54
|
+
Test files are a bit of a mess at the moment, but they should give you an indication of what's currently supported.
|
55
|
+
|
56
|
+
## Contributing
|
57
|
+
|
58
|
+
Fork, send pull requests, file bug reports -- any help is welcome.
|
data/Rakefile
ADDED
data/lib/jpath.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module JPath
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
require_relative "jpath/item"
|
7
|
+
require_relative "jpath/pointer"
|
8
|
+
require_relative "jpath/parser"
|
9
|
+
require_relative "jpath/parser/formula"
|
10
|
+
require_relative "jpath/parser/step"
|
11
|
+
require_relative "jpath/parser/path"
|
12
|
+
|
13
|
+
def self.find(json, xpath)
|
14
|
+
parse(xpath).from(json)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse(xpath)
|
18
|
+
Parser.path(xpath)
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/lib/jpath/item.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
module JPath
|
2
|
+
class Item
|
3
|
+
|
4
|
+
def initialize(hash)
|
5
|
+
@hash = hash
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](pointer)
|
9
|
+
result = @hash
|
10
|
+
if pointer.is_a?(String)
|
11
|
+
pointer = Pointer.parse(pointer)
|
12
|
+
end
|
13
|
+
pointer.each do |key|
|
14
|
+
unless result.is_a?(Array) or result.is_a?(Hash)
|
15
|
+
return nil
|
16
|
+
end
|
17
|
+
if result.is_a?(Array) && !key.is_a?(Numeric)
|
18
|
+
return nil
|
19
|
+
end
|
20
|
+
result = result[key]
|
21
|
+
end
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def parent(pointer)
|
26
|
+
if pointer.is_a?(String)
|
27
|
+
pointer = Pointer.parse(pointer)
|
28
|
+
end
|
29
|
+
if pointer.top?
|
30
|
+
return nil
|
31
|
+
end
|
32
|
+
if pointer.index?
|
33
|
+
pointer.grandparent
|
34
|
+
else
|
35
|
+
pointer.parent
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def ancestors(pointer)
|
40
|
+
if pointer.is_a?(String)
|
41
|
+
pointer = Pointer.parse(pointer)
|
42
|
+
end
|
43
|
+
obj = parent(pointer)
|
44
|
+
if obj.nil?
|
45
|
+
[]
|
46
|
+
else
|
47
|
+
[obj, ancestors(obj)].flatten
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def attributes(pointer)
|
52
|
+
enum_for(:each_attribute, pointer)
|
53
|
+
end
|
54
|
+
|
55
|
+
def children(pointer)
|
56
|
+
enum_for(:each_child, pointer)
|
57
|
+
end
|
58
|
+
|
59
|
+
def descendants(pointer)
|
60
|
+
enum_for(:each_descendant, pointer)
|
61
|
+
end
|
62
|
+
|
63
|
+
def following(pointer)
|
64
|
+
enum_for(:each_following, pointer)
|
65
|
+
end
|
66
|
+
|
67
|
+
def preceding(pointer)
|
68
|
+
enum_for(:each_preceding, pointer)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def each_attribute(pointer)
|
74
|
+
children(pointer).select do |child|
|
75
|
+
unless self[child].is_a?(Hash)
|
76
|
+
yield child
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def each_child(pointer, &block)
|
82
|
+
if pointer.is_a?(String)
|
83
|
+
pointer = Pointer.parse(pointer)
|
84
|
+
end
|
85
|
+
obj = self[pointer]
|
86
|
+
if obj.is_a?(Hash)
|
87
|
+
obj.each do |key, value|
|
88
|
+
if value.is_a?(Array)
|
89
|
+
each_child(pointer + key, &block)
|
90
|
+
else
|
91
|
+
yield pointer + key
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if obj.is_a?(Array)
|
96
|
+
obj.each_with_index do |item, index|
|
97
|
+
yield pointer + index
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def each_descendant(pointer)
|
103
|
+
children(pointer).each do |p1|
|
104
|
+
yield p1
|
105
|
+
each_descendant(p1) do |p2|
|
106
|
+
yield p2
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def each_following(pointer)
|
112
|
+
flag = false
|
113
|
+
parent = parent(pointer)
|
114
|
+
each_child(parent) do |child|
|
115
|
+
if flag
|
116
|
+
yield child
|
117
|
+
end
|
118
|
+
if child == pointer
|
119
|
+
flag = true
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def each_preceding(pointer)
|
125
|
+
flag = true
|
126
|
+
parent = parent(pointer)
|
127
|
+
each_child(parent) do |child|
|
128
|
+
if child == pointer
|
129
|
+
flag = false
|
130
|
+
end
|
131
|
+
if flag
|
132
|
+
yield child
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
data/lib/jpath/parser.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module JPath
|
2
|
+
module Parser
|
3
|
+
|
4
|
+
def self.path(s)
|
5
|
+
unless s.is_a?(StringScanner)
|
6
|
+
s = StringScanner.new(s)
|
7
|
+
end
|
8
|
+
Path.new do |path|
|
9
|
+
each_step(s) do |step|
|
10
|
+
path << step
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.predicates(s)
|
16
|
+
enum_for(:each_predicate, s).to_a
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.steps(s)
|
20
|
+
enum_for(:each_step, s).to_a
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.expressions(s)
|
24
|
+
enum_for(:each_expression, s).to_a
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def self.next_expression(s)
|
30
|
+
if s.scan /position\(\)/
|
31
|
+
return Position.new
|
32
|
+
end
|
33
|
+
if s.scan /(\d+\.\d+)/
|
34
|
+
return Number.new(s[1].to_f)
|
35
|
+
end
|
36
|
+
if s.scan /(\d+)/
|
37
|
+
return Number.new(s[1].to_i)
|
38
|
+
end
|
39
|
+
if s.scan /@([\w*]+)/
|
40
|
+
return Attribute.new(s[1])
|
41
|
+
end
|
42
|
+
if s.scan /\=/
|
43
|
+
return Operator.new("=")
|
44
|
+
end
|
45
|
+
if s.scan /</
|
46
|
+
return Operator.new("<")
|
47
|
+
end
|
48
|
+
if s.scan />/
|
49
|
+
return Operator.new(">")
|
50
|
+
end
|
51
|
+
if s.scan /"([^"]+)"/
|
52
|
+
return Literal.new(s[1])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.each_expression(s)
|
57
|
+
until s.eos?
|
58
|
+
obj = next_expression(s)
|
59
|
+
break if obj.nil?
|
60
|
+
yield obj
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.next_predicate(s)
|
65
|
+
if s.scan /\[(\d+)\]/
|
66
|
+
return Formula.new("=", Position.new, Number.new(s[1].to_i)).to_predicate
|
67
|
+
end
|
68
|
+
if s.scan /\[last\(\)\]/
|
69
|
+
return Formula.new("=", Position.new, Last.new).to_predicate
|
70
|
+
end
|
71
|
+
if s.scan /\[@?([\w*]+)\]/
|
72
|
+
return Predicate.new(Contains.new(Attribute.new(s[1])))
|
73
|
+
end
|
74
|
+
unless s.scan(/\[/)
|
75
|
+
return
|
76
|
+
end
|
77
|
+
exp = expressions(s).inject do |result, expression|
|
78
|
+
result + expression
|
79
|
+
end
|
80
|
+
if exp.nil? || !exp.boolean?
|
81
|
+
raise "Invalid predicate: %s" % s.rest.inspect
|
82
|
+
end
|
83
|
+
unless s.scan(/\]/)
|
84
|
+
raise "Invalid predicate: %s" % s.rest.inspect
|
85
|
+
end
|
86
|
+
Predicate.new(exp)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.each_predicate(s)
|
90
|
+
until s.eos?
|
91
|
+
obj = next_predicate(s)
|
92
|
+
break if obj.nil?
|
93
|
+
yield obj
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.next_step(s)
|
98
|
+
if s.scan /\.?\/\/([\w*]+)/
|
99
|
+
return Descendant.new(s[1])
|
100
|
+
elsif s.scan /descendant::([\w*]+)/
|
101
|
+
return Descendant.new(s[1])
|
102
|
+
elsif s.scan /child::([\w*]+)/
|
103
|
+
return Child.new(s[1])
|
104
|
+
elsif s.scan /([\w*]+)/
|
105
|
+
return Child.new(s[1])
|
106
|
+
elsif s.scan /.\//
|
107
|
+
return Self.new
|
108
|
+
elsif s.bol? && s.scan(/\//)
|
109
|
+
return Root.new
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.each_step(s)
|
114
|
+
until s.eos?
|
115
|
+
obj = next_step(s)
|
116
|
+
break if obj.nil?
|
117
|
+
each_predicate(s) do |p|
|
118
|
+
obj << p
|
119
|
+
end
|
120
|
+
yield obj
|
121
|
+
unless s.match?(/\/\//)
|
122
|
+
s.scan /\//
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
module JPath
|
2
|
+
module Parser
|
3
|
+
class Predicate
|
4
|
+
|
5
|
+
attr_reader :expression
|
6
|
+
|
7
|
+
def initialize(expression)
|
8
|
+
@expression = expression
|
9
|
+
end
|
10
|
+
|
11
|
+
def true?(*args)
|
12
|
+
expression.true?(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"[%s]" % expression
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
class Operator
|
21
|
+
|
22
|
+
attr_reader :char
|
23
|
+
|
24
|
+
attr_reader :parts
|
25
|
+
|
26
|
+
def initialize(char)
|
27
|
+
@char = char
|
28
|
+
@parts = []
|
29
|
+
end
|
30
|
+
|
31
|
+
def boolean?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def +(other)
|
36
|
+
add(other)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(other)
|
40
|
+
@parts << other
|
41
|
+
if parts.size < 2
|
42
|
+
self
|
43
|
+
else
|
44
|
+
Formula.new(char, parts)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
char.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
class Formula
|
54
|
+
|
55
|
+
attr_reader :parts
|
56
|
+
|
57
|
+
attr_reader :type
|
58
|
+
|
59
|
+
def initialize(type, *parts)
|
60
|
+
@type = type
|
61
|
+
@parts = parts.flatten
|
62
|
+
end
|
63
|
+
|
64
|
+
def true?(*args)
|
65
|
+
f = first.value(*args)
|
66
|
+
s = second.value(*args)
|
67
|
+
if requires_boolean?
|
68
|
+
unless f === true or f === false
|
69
|
+
return false
|
70
|
+
end
|
71
|
+
unless s === true or s === false
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
else
|
75
|
+
unless f.is_a?(Numeric)
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
unless s.is_a?(Numeric)
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
case type
|
83
|
+
when "+"
|
84
|
+
f + s
|
85
|
+
when "-"
|
86
|
+
f - s
|
87
|
+
when "*"
|
88
|
+
f * s
|
89
|
+
when "/"
|
90
|
+
f / s
|
91
|
+
when ">"
|
92
|
+
f > s
|
93
|
+
when "<"
|
94
|
+
f < s
|
95
|
+
when "="
|
96
|
+
f == s
|
97
|
+
when "and"
|
98
|
+
f && s
|
99
|
+
when "or"
|
100
|
+
f || s
|
101
|
+
else
|
102
|
+
raise "Invalid formula type: %s" % type
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def first
|
107
|
+
parts[0]
|
108
|
+
end
|
109
|
+
|
110
|
+
def second
|
111
|
+
parts[1]
|
112
|
+
end
|
113
|
+
|
114
|
+
def requires_boolean?
|
115
|
+
%w(and or).include?(type)
|
116
|
+
end
|
117
|
+
|
118
|
+
def boolean?
|
119
|
+
%w(> < = and or).include?(type)
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_predicate
|
123
|
+
Predicate.new(self)
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_s
|
127
|
+
"%s%s%s" % [first, type, second]
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
class Contains
|
132
|
+
|
133
|
+
attr_reader :item
|
134
|
+
|
135
|
+
def initialize(item)
|
136
|
+
@item = item
|
137
|
+
end
|
138
|
+
|
139
|
+
def true?(item, context, index, size)
|
140
|
+
item.children(context).map(&:name).include?(@item.name)
|
141
|
+
end
|
142
|
+
|
143
|
+
def boolean?
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
def to_predicate
|
148
|
+
Predicate.new(self)
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_s
|
152
|
+
item.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
class Position
|
157
|
+
|
158
|
+
def to_s
|
159
|
+
"position()"
|
160
|
+
end
|
161
|
+
|
162
|
+
def value(hash, pointer, index, size)
|
163
|
+
index
|
164
|
+
end
|
165
|
+
|
166
|
+
def +(other)
|
167
|
+
other.add(self)
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
class Last
|
172
|
+
|
173
|
+
def value(hash, pointer, index, size)
|
174
|
+
size
|
175
|
+
end
|
176
|
+
|
177
|
+
def to_s
|
178
|
+
"last()"
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
class Literal
|
183
|
+
|
184
|
+
attr_reader :string
|
185
|
+
|
186
|
+
def initialize(string)
|
187
|
+
@string = string
|
188
|
+
end
|
189
|
+
|
190
|
+
def value(*args)
|
191
|
+
string
|
192
|
+
end
|
193
|
+
|
194
|
+
def boolean?
|
195
|
+
false
|
196
|
+
end
|
197
|
+
|
198
|
+
def to_s
|
199
|
+
string.inspect
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
class Number
|
204
|
+
|
205
|
+
attr_reader :number
|
206
|
+
|
207
|
+
def initialize(number)
|
208
|
+
@number = number
|
209
|
+
end
|
210
|
+
|
211
|
+
def value(*args)
|
212
|
+
number
|
213
|
+
end
|
214
|
+
|
215
|
+
def boolean?
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_s
|
220
|
+
number.to_s
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module JPath
|
2
|
+
module Parser
|
3
|
+
class Union
|
4
|
+
|
5
|
+
def initialize(*paths)
|
6
|
+
@paths = paths
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
@paths.map(&:to_s).join("|")
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
class Path
|
15
|
+
|
16
|
+
attr_reader :steps
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@steps = []
|
20
|
+
if block_given?
|
21
|
+
yield self
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def from(item)
|
26
|
+
unless item.is_a?(Item)
|
27
|
+
item = Item.new(item)
|
28
|
+
end
|
29
|
+
list = [Pointer.new]
|
30
|
+
steps.each do |step|
|
31
|
+
list = list.map { |node|
|
32
|
+
step.from(item, node).to_a
|
33
|
+
}.flatten
|
34
|
+
end
|
35
|
+
list = list.map do |pointer|
|
36
|
+
item[pointer]
|
37
|
+
end
|
38
|
+
list
|
39
|
+
end
|
40
|
+
|
41
|
+
def <<(step)
|
42
|
+
unless step.is_a?(Self)
|
43
|
+
@steps << step
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
@steps.join("/")
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module JPath
|
2
|
+
module Parser
|
3
|
+
class Step
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
attr_reader :predicates
|
8
|
+
|
9
|
+
def initialize(name)
|
10
|
+
@name = name
|
11
|
+
@predicates = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def wildcard?
|
15
|
+
name == '*'
|
16
|
+
end
|
17
|
+
|
18
|
+
def +(other)
|
19
|
+
other.add(self)
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(predicate)
|
23
|
+
@predicates << predicate
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"%s::%s%s" % [axis, name, predicates.map(&:to_s).join("")]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
class Self < Step
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
super(nil)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"self"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
class Root < Step
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
super(nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
def from(item, context)
|
49
|
+
[context]
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
""
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
class Attribute < Step
|
58
|
+
|
59
|
+
def boolean?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
def value(item, context, index, size)
|
64
|
+
value = item[context + name]
|
65
|
+
if value.nil?
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
if value =~ /\A\d+\.\d+\z/
|
69
|
+
value.to_f
|
70
|
+
elsif value =~ /\A\d+\z/
|
71
|
+
value.to_i
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def axis
|
78
|
+
"attribute"
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
class Node < Step
|
83
|
+
|
84
|
+
def select(item, list)
|
85
|
+
select_by_predicates item, select_by_name(list)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def select_by_name(list)
|
91
|
+
wildcard? ? list : list.select { |child| child.name == @name }
|
92
|
+
end
|
93
|
+
|
94
|
+
def select_by_predicates(item, list)
|
95
|
+
predicates.each_with_index { |p| list = select_by_predicate(item, list, p) }; list
|
96
|
+
end
|
97
|
+
|
98
|
+
def select_by_predicate(item, list, p)
|
99
|
+
list.select.with_index { |context, index| p.true?(item, context, index + 1, list.size) }
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
class Child < Node
|
104
|
+
|
105
|
+
def from(item, context)
|
106
|
+
return select item, item.children(context)
|
107
|
+
end
|
108
|
+
|
109
|
+
def axis
|
110
|
+
"child"
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
class Descendant < Node
|
115
|
+
|
116
|
+
def from(item, context)
|
117
|
+
return select item, item.descendants(context)
|
118
|
+
end
|
119
|
+
|
120
|
+
def axis
|
121
|
+
"descendant"
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module JPath
|
2
|
+
class Pointer
|
3
|
+
|
4
|
+
def self.parse(string)
|
5
|
+
new string.split(".").map { |str|
|
6
|
+
str =~ /\A\d+\z/ ? str.to_i : str
|
7
|
+
}.to_a
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(*ary)
|
11
|
+
@ary = ary.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
to_s == other.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def +(string)
|
19
|
+
self.class.new(@ary + [string])
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&block)
|
23
|
+
@ary.each(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def name
|
27
|
+
index? ? (parent.nil? ? nil : parent.name) : @ary.last
|
28
|
+
end
|
29
|
+
|
30
|
+
def grandparent
|
31
|
+
parent.nil? ? nil : parent.parent
|
32
|
+
end
|
33
|
+
|
34
|
+
def parent
|
35
|
+
top? ? nil : self.class.new(without_last)
|
36
|
+
end
|
37
|
+
|
38
|
+
def size
|
39
|
+
@ary.size
|
40
|
+
end
|
41
|
+
|
42
|
+
def index?
|
43
|
+
@ary.last.is_a?(Integer)
|
44
|
+
end
|
45
|
+
|
46
|
+
def top?
|
47
|
+
@ary.empty? or top_index?
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_s
|
51
|
+
@ary.join(".")
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def top_index?
|
57
|
+
index? && size == 1
|
58
|
+
end
|
59
|
+
|
60
|
+
def without_last
|
61
|
+
@ary[0...-1]
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/spec/item_spec.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JPath::Item do
|
4
|
+
|
5
|
+
let(:item) do
|
6
|
+
JPath::Item.new(HASH)
|
7
|
+
end
|
8
|
+
|
9
|
+
context "#[]" do
|
10
|
+
it "returns smth" do
|
11
|
+
item["store.book.0"].should == HASH["store"]["book"][0]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "#parent" do
|
16
|
+
it "returns smth" do
|
17
|
+
item.parent("store.book.0").to_s.should == "store"
|
18
|
+
end
|
19
|
+
it "returns smth" do
|
20
|
+
item.parent("store.book.0.title").to_s.should == "store.book.0"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "#ancestors" do
|
25
|
+
it "returns smth" do
|
26
|
+
item.ancestors("store.book.0").map(&:to_s).should == ["store", ""]
|
27
|
+
end
|
28
|
+
it "returns smth" do
|
29
|
+
item.ancestors("store.book.0.title").map(&:to_s).should == ["store.book.0", "store", ""]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "#children" do
|
34
|
+
it "returns smth" do
|
35
|
+
item.children("store").map(&:to_s).should == ["store.book.0", "store.book.1", "store.book.2", "store.book.3", "store.bicycle"]
|
36
|
+
end
|
37
|
+
it "returns smth" do
|
38
|
+
item.children("store.bicycle").map(&:to_s).should == ["store.bicycle.color", "store.bicycle.price"]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "#attributes" do
|
43
|
+
it "returns smth" do
|
44
|
+
item.attributes("store").map(&:to_s).should == []
|
45
|
+
end
|
46
|
+
it "returns smth" do
|
47
|
+
item.attributes("store.bicycle").map(&:to_s).should == ["store.bicycle.color", "store.bicycle.price"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "#descendants" do
|
52
|
+
it "returns smth" do
|
53
|
+
item.descendants("store.bicycle").map(&:to_s).should == ["store.bicycle.color", "store.bicycle.price"]
|
54
|
+
end
|
55
|
+
it "returns smth" do
|
56
|
+
item.descendants("store").map(&:to_s).should == ["store.book.0", "store.book.0.category", "store.book.0.author", "store.book.0.title", "store.book.0.price", "store.book.1", "store.book.1.category.0", "store.book.1.author", "store.book.1.title", "store.book.1.price", "store.book.2", "store.book.2.category", "store.book.2.author", "store.book.2.title", "store.book.2.isbn", "store.book.2.price", "store.book.3", "store.book.3.category", "store.book.3.author", "store.book.3.title", "store.book.3.isbn", "store.book.3.price", "store.bicycle", "store.bicycle.color", "store.bicycle.price"]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "#following" do
|
61
|
+
it "returns smth" do
|
62
|
+
item.following("store.book.0").map(&:to_s).should == ["store.book.1", "store.book.2", "store.book.3", "store.bicycle"]
|
63
|
+
end
|
64
|
+
it "returns smth" do
|
65
|
+
item.following("store.book.0.author").map(&:to_s).should == ["store.book.0.title", "store.book.0.price"]
|
66
|
+
end
|
67
|
+
it "returns smth" do
|
68
|
+
item.following("store.bicycle").map(&:to_s).should == []
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "#preceding" do
|
73
|
+
it "returns smth" do
|
74
|
+
item.preceding("store.book.2").map(&:to_s).should == ["store.book.0", "store.book.1"]
|
75
|
+
end
|
76
|
+
it "returns smth" do
|
77
|
+
item.preceding("store.book.0.author").map(&:to_s).should == ["store.book.0.category"]
|
78
|
+
end
|
79
|
+
it "returns smth" do
|
80
|
+
item.preceding("store.bicycle").map(&:to_s).should == ["store.book.0", "store.book.1", "store.book.2", "store.book.3"]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
data/spec/jpath_spec.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JPath do
|
4
|
+
|
5
|
+
context "/store/book/author" do
|
6
|
+
it "returns the authors of all books in the store" do
|
7
|
+
JPath.find(HASH, "/store/book/author").should == ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context "//author" do
|
12
|
+
it "returns all authors" do
|
13
|
+
JPath.find(HASH, "//author").should == ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "/store/*" do
|
18
|
+
it "all things in store, which are some books and a red bicycle" do
|
19
|
+
JPath.find(HASH, "/store/*").should == [HASH["store"]["book"][0], HASH["store"]["book"][1], HASH["store"]["book"][2], HASH["store"]["book"][3], HASH["store"]["bicycle"]]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "/store//price" do
|
24
|
+
it "returns the price of everything in the store" do
|
25
|
+
JPath.find(HASH, "/store//price").should == [8.95, 12.99, 8.95, 22.99, 19.95]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "//book[3]" do
|
30
|
+
it "returns the third book" do
|
31
|
+
JPath.find(HASH, "//book[3]").should == [HASH["store"]["book"][2]]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "//book[last()]" do
|
36
|
+
it "the last book in order" do
|
37
|
+
JPath.find(HASH, "//book[last()]").should == [HASH["store"]["book"][3]]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "//book[position()<3]" do
|
42
|
+
it "returns the first two books" do
|
43
|
+
JPath.find(HASH, "//book[position()<3]").should == [HASH["store"]["book"][0], HASH["store"]["book"][1]]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "//book[isbn]" do
|
48
|
+
it "returns all books with isbn number" do
|
49
|
+
JPath.find(HASH, "//book[isbn]").should == [HASH["store"]["book"][2], HASH["store"]["book"][3]]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "//book[@price<10]" do
|
54
|
+
it "returns all books cheapier than 10" do
|
55
|
+
JPath.find(HASH, "//book[@price<10]").should == [HASH["store"]["book"][0], HASH["store"]["book"][2]]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JPath::Parser do
|
4
|
+
|
5
|
+
it "parses child" do
|
6
|
+
JPath.parse("store").to_s.should == "child::store"
|
7
|
+
JPath.parse("child::store").to_s.should == "child::store"
|
8
|
+
JPath.parse("*").to_s.should == "child::*"
|
9
|
+
JPath.parse("./store").to_s.should == "child::store"
|
10
|
+
JPath.parse("/store").to_s.should == "/child::store"
|
11
|
+
JPath.parse("/child::store").to_s.should == "/child::store"
|
12
|
+
JPath.parse("/*").to_s.should == "/child::*"
|
13
|
+
JPath.parse("/store/book").to_s.should == "/child::store/child::book"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "parses descendant" do
|
17
|
+
JPath.parse("//book").to_s.should == "descendant::book"
|
18
|
+
JPath.parse("descendant::book").to_s.should == "descendant::book"
|
19
|
+
JPath.parse("//*").to_s.should == "descendant::*"
|
20
|
+
JPath.parse(".//book").to_s.should == "descendant::book"
|
21
|
+
JPath.parse("/descendant::book").to_s.should == "/descendant::book"
|
22
|
+
JPath.parse("/*").to_s.should == "/child::*"
|
23
|
+
JPath.parse("/store//price").to_s.should == "/child::store/descendant::price"
|
24
|
+
end
|
25
|
+
|
26
|
+
it "parses attributes" do
|
27
|
+
JPath.parse('book[1]').to_s.should == 'child::book[position()=1]'
|
28
|
+
JPath.parse('book[last()]').to_s.should == 'child::book[position()=last()]'
|
29
|
+
JPath.parse('book[position()>1]').to_s.should == 'child::book[position()>1]'
|
30
|
+
JPath.parse('book[@price]').to_s.should == 'child::book[attribute::price]'
|
31
|
+
JPath.parse('book[@price="8.95"]').to_s.should == 'child::book[attribute::price="8.95"]'
|
32
|
+
JPath.parse('book[@price=8.95]').to_s.should == 'child::book[attribute::price=8.95]'
|
33
|
+
JPath.parse('book[@price<8.95]').to_s.should == 'child::book[attribute::price<8.95]'
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JPath::Pointer do
|
4
|
+
|
5
|
+
context ".parse" do
|
6
|
+
it "returns Pointer" do
|
7
|
+
JPath::Pointer.parse("store.book.0.title").to_s.should == 'store.book.0.title'
|
8
|
+
end
|
9
|
+
it "returns Pointer" do
|
10
|
+
JPath::Pointer.parse("").to_s.should == ''
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "#index?" do
|
15
|
+
it "returns false" do
|
16
|
+
JPath::Pointer.new().should_not be_index
|
17
|
+
end
|
18
|
+
it "returns false" do
|
19
|
+
JPath::Pointer.new("store", "book", 0, "title").should_not be_index
|
20
|
+
end
|
21
|
+
it "returns true" do
|
22
|
+
JPath::Pointer.new(0).should be_index
|
23
|
+
end
|
24
|
+
it "returns true" do
|
25
|
+
JPath::Pointer.new("store", "book", 0).should be_index
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "#name" do
|
30
|
+
it "returns nil" do
|
31
|
+
JPath::Pointer.new().name.should be_nil
|
32
|
+
end
|
33
|
+
it "returns name" do
|
34
|
+
JPath::Pointer.new("store", "book", 0, "title").name.should == 'title'
|
35
|
+
end
|
36
|
+
it "returns true" do
|
37
|
+
JPath::Pointer.new("store", "book").name.should == 'book'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "#top?" do
|
42
|
+
it "returns true" do
|
43
|
+
JPath::Pointer.new().should be_top
|
44
|
+
end
|
45
|
+
it "returns true" do
|
46
|
+
JPath::Pointer.new(0).should be_top
|
47
|
+
end
|
48
|
+
it "returns false" do
|
49
|
+
JPath::Pointer.new("store").should_not be_top
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "#parent" do
|
54
|
+
it "returns nil" do
|
55
|
+
JPath::Pointer.new(0).parent.should be_nil
|
56
|
+
end
|
57
|
+
it "returns root" do
|
58
|
+
JPath::Pointer.new("store").parent.to_s.should == ""
|
59
|
+
end
|
60
|
+
it "returns root" do
|
61
|
+
JPath::Pointer.new("store", "0").parent.to_s.should == 'store'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "#grandparent" do
|
66
|
+
it "returns nil" do
|
67
|
+
JPath::Pointer.new(0).grandparent.should be_nil
|
68
|
+
end
|
69
|
+
it "returns nil" do
|
70
|
+
JPath::Pointer.new("store").grandparent.should be_nil
|
71
|
+
end
|
72
|
+
it "returns root" do
|
73
|
+
JPath::Pointer.new("store", "0").grandparent.to_s.should == ""
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'jpath'
|
2
|
+
|
3
|
+
HASH = {
|
4
|
+
"store" => {
|
5
|
+
"book" => [{
|
6
|
+
"category" => "reference",
|
7
|
+
"author" => "Nigel Rees",
|
8
|
+
"title" => "Sayings of the Century",
|
9
|
+
"price" => 8.95
|
10
|
+
}, {
|
11
|
+
"category" => ["fiction"],
|
12
|
+
"author" => "Evelyn Waugh",
|
13
|
+
"title" => "Sword of Honour",
|
14
|
+
"price" => 12.99
|
15
|
+
}, {
|
16
|
+
"category" => "fiction",
|
17
|
+
"author" => "Herman Melville",
|
18
|
+
"title" => "Moby Dick",
|
19
|
+
"isbn" => "0-553-21311-3",
|
20
|
+
"price" => 8.95
|
21
|
+
}, {
|
22
|
+
"category" => "fiction",
|
23
|
+
"author" => "J. R. R. Tolkien",
|
24
|
+
"title" => "The Lord of the Rings",
|
25
|
+
"isbn" => "0-395-19395-8",
|
26
|
+
"price" => 22.99
|
27
|
+
}],
|
28
|
+
"bicycle" => {
|
29
|
+
"color" => "red",
|
30
|
+
"price" => 19.95
|
31
|
+
}
|
32
|
+
}
|
33
|
+
}
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jpath
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alex Serebryakov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: JPath is a Ruby library that allows to execute XPath queries on JSON
|
28
|
+
documents
|
29
|
+
email:
|
30
|
+
- alex@merimond.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- lib/jpath/item.rb
|
36
|
+
- lib/jpath/parser/formula.rb
|
37
|
+
- lib/jpath/parser/path.rb
|
38
|
+
- lib/jpath/parser/step.rb
|
39
|
+
- lib/jpath/parser.rb
|
40
|
+
- lib/jpath/pointer.rb
|
41
|
+
- lib/jpath.rb
|
42
|
+
- Rakefile
|
43
|
+
- README.md
|
44
|
+
- LICENSE
|
45
|
+
- spec/item_spec.rb
|
46
|
+
- spec/jpath_spec.rb
|
47
|
+
- spec/parser_spec.rb
|
48
|
+
- spec/pointer_spec.rb
|
49
|
+
- spec/spec_helper.rb
|
50
|
+
homepage: https://github.com/merimond/jpath
|
51
|
+
licenses: []
|
52
|
+
metadata: {}
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - '>='
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubyforge_project: jpath
|
69
|
+
rubygems_version: 2.0.3
|
70
|
+
signing_key:
|
71
|
+
specification_version: 4
|
72
|
+
summary: XPath queries for JSON documents
|
73
|
+
test_files:
|
74
|
+
- spec/item_spec.rb
|
75
|
+
- spec/jpath_spec.rb
|
76
|
+
- spec/parser_spec.rb
|
77
|
+
- spec/pointer_spec.rb
|
78
|
+
- spec/spec_helper.rb
|