jpath 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|