riffraff_jsonpath 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ grammar JSONPathGrammar
2
+
3
+ rule path
4
+ root selectors:child+ <JSONPath::Nodes::RootNode>
5
+ end
6
+
7
+ rule root
8
+ '$'
9
+ end
10
+
11
+ rule number
12
+ '-'? [0-9]+
13
+ end
14
+
15
+ rule wildcard
16
+ '*' / '\'' '*' '\''
17
+ end
18
+
19
+ rule lower
20
+ descendant / '.'
21
+ end
22
+
23
+ rule descendant
24
+ '..'
25
+ end
26
+
27
+ rule word_list
28
+ quoted_word ',' ' '* word_list
29
+ /
30
+ quoted_word
31
+ end
32
+
33
+ rule quoted_word
34
+ '\'' key:(!'\'' . )+ '\''
35
+ end
36
+
37
+ rule child
38
+ # .foo or ..foo
39
+ lower key:bareword <JSONPath::Nodes::KeyNode>
40
+ /
41
+ # .* or ..*
42
+ lower wildcard <JSONPath::Nodes::WildcardNode>
43
+ /
44
+ # '?(@ % 2 == 0)'
45
+ lower '\'?(' template_code:(!')\'' . )+ ')\'' <JSONPath::Nodes::FilterNode>
46
+ /
47
+ # .'foo'
48
+ lower '\'' key:(!'\'' . )+ '\'' <JSONPath::Nodes::KeyNode>
49
+ /
50
+ # [*] and ['*']
51
+ '[' wildcard ']' <JSONPath::Nodes::WildcardNode>
52
+ /
53
+ # [0]
54
+ '[' index:number ']' <JSONPath::Nodes::IndexNode>
55
+ /
56
+ # .[0] and ..[0]
57
+ lower '[' index:number ']' <JSONPath::Nodes::IndexNode>
58
+ /
59
+ # [1:2]
60
+ '[' start:number ':' stop:number ']' <JSONPath::Nodes::SliceNode>
61
+ /
62
+ # .[1:2] and ..[1:2]
63
+ lower '[' start:number ':' stop:number ']' <JSONPath::Nodes::SliceNode>
64
+ /
65
+ # [1:]
66
+ '[' start:number ':]' <JSONPath::Nodes::SliceNode>
67
+ /
68
+ # .[1:] and ..[1:]
69
+ lower '[' start:number ':]' <JSONPath::Nodes::SliceNode>
70
+ /
71
+ # [1:4:2]
72
+ '[' start:number ':' stop:number ':' step:number ']' <JSONPath::Nodes::SliceNode>
73
+ /
74
+ # .[1:4:2] and ..[1:4:2]
75
+ lower '[' start:number ':' stop:number ':' step:number ']' <JSONPath::Nodes::SliceNode>
76
+ /
77
+ # [1::2]
78
+ '[' start:number '::' step:number ']' <JSONPath::Nodes::SliceNode>
79
+ /
80
+ # .[1::2] and ..[1::2]
81
+ lower '[' start:number '::' step:number ']' <JSONPath::Nodes::SliceNode>
82
+ /
83
+ # [(@.length - 1)]
84
+ '[(' template_code:(!')]' . )+ ')]' <JSONPath::Nodes::ExprNode>
85
+ /
86
+ # .[(@.length - 1)] and ..[(@.length - 1)]
87
+ lower '[(' template_code:(!')]' . )+ ')]' <JSONPath::Nodes::ExprNode>
88
+ /
89
+ # [?(@ % 2 == 0)]
90
+ '[?(' template_code:(!')]' . )+ ')]' <JSONPath::Nodes::FilterNode>
91
+ /
92
+ # .[?(@ % 2 == 0)] and ..[?(@ % 2 == 0)]
93
+ lower '[?(' template_code:(!')]' . )+ ')]' <JSONPath::Nodes::FilterNode>
94
+ /
95
+ # ['foo']
96
+ '[' word_list ']' <JSONPath::Nodes::KeyNode>
97
+ /
98
+ # .['foo'] and ..['foo]
99
+ lower '[' word_list ']' <JSONPath::Nodes::KeyNode>
100
+ end
101
+
102
+ rule bareword
103
+ [_a-zA-Z0-9]+
104
+ end
105
+
106
+ end
@@ -0,0 +1,199 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+
3
+ # The content below was taken from the tests for the JS and PHP
4
+ # reference implementations at http://code.google.com/p/jsonpath/
5
+ class ParserTest < Test::Unit::TestCase
6
+
7
+ context 'Traversing' do
8
+ context "hash" do
9
+ should "handle underscore in field names" do
10
+ path = '$.a_b'
11
+ assert_resolves({"a_b" => 1}, path, [1])
12
+ end
13
+ should "parse bareword child with single terminal" do
14
+ path = '$.a'
15
+ assert_resolves({"a" => 1}, path, [1])
16
+ end
17
+ should "parse subscripted quoted child with single terminal" do
18
+ path = "$['a b']"
19
+ assert_resolves({"a b" => 1}, path, [1])
20
+ end
21
+ should "parse subscripted quoted child and chained bareword" do
22
+ path = "$['a b'].c"
23
+ assert_resolves({"a b" => {"c" => 1}}, path, [1])
24
+ end
25
+ should "parses bare wildcard" do
26
+ path = "$.*"
27
+ assert_resolves({"a" => 1, "b" => 2}, path, [1, 2])
28
+ end
29
+ should "parse quoted wildcard on hash" do
30
+ path = "$['*']"
31
+ assert_resolves({"a" => 1, "b" => 2}, path, [1, 2])
32
+ end
33
+ should "parse quoted key outside of brackets" do
34
+ path = "$.'a b'"
35
+ assert_resolves({"a b" => 1}, path, [1])
36
+ end
37
+ end
38
+ context "array" do
39
+ should "parse bare wildcard" do
40
+ path = "$.*"
41
+ assert_resolves([1, 2, 3], path, [1, 2, 3])
42
+ end
43
+ should "parses subscripted quoted wildcard" do
44
+ path = "$['*']"
45
+ assert_resolves([1, 2, 3], path, [1, 2, 3])
46
+ end
47
+ should "parses through bare wildcard" do
48
+ path = "$.*.name"
49
+ assert_resolves([{"name" => 1}, {"name" => 2}, {"name" => 3}], path, [1, 2, 3])
50
+ end
51
+ should "parse index to single terminal" do
52
+ path = "$[1]"
53
+ assert_resolves(%w(foo bar baz), path, %w(bar))
54
+ end
55
+ should "parse index to multiple terminals" do
56
+ path = "$.*[1].name"
57
+ assert_resolves({
58
+ "a" => [1, {"name" => 2}, 3],
59
+ "b" => [4, {"name" => 5}, 6],
60
+ "c" => [7, {"name" => 8}, 9],
61
+ }, path, [2, 5, 8])
62
+ end
63
+ should "find all descendants in a array selection" do
64
+ path = "$.[0]..name"
65
+ assert_resolves([[
66
+ {'name'=>'joe'},
67
+ {'name'=>'jean'},
68
+ {'name'=>'jane'}]],
69
+ path, ['joe', 'jean', 'jane'])
70
+ path = "$.ary[0]..name"
71
+ assert_resolves({
72
+ "ary"=>[[
73
+ {'name'=>'joe'},
74
+ {'name'=>'jean'},
75
+ {'name'=>'jane'}]]},
76
+ path, ['joe', 'jean', 'jane'])
77
+ end
78
+ end
79
+ context "combination wildcards" do
80
+ should "parses through bare wildcard on array with additional wildcard" do
81
+ path = "$.*.names.*"
82
+ assert_resolves([
83
+ {"names" => %w(foo bar)},
84
+ {"names" => %w(baz quux)},
85
+ {"names" => %w(spam eggs)}
86
+ ], path, %w(foo bar baz quux spam eggs))
87
+ end
88
+ end
89
+ context "using slices" do
90
+ setup {
91
+ @deep = [
92
+ {"a" => {"name" => "a1"}, "b" => {"name" => "b1"}},
93
+ {"c" => {"name" => "c1"}, "d" => {"name" => "d1"}},
94
+ {"e" => {"name" => "e1"}, "f" => {"name" => "f1"}},
95
+ {"g" => {"name" => "g1"}, "h" => {"name" => "h1"}},
96
+ {"i" => {"name" => "i1"}, "j" => {"name" => "j1"}},
97
+ {"k" => {"name" => "k1"}, "l" => {"name" => "l1"}},
98
+ {"m" => {"name" => "m1"}, "n" => {"name" => "n1"}},
99
+ ]
100
+ @shallow = [1, 2, 3, 4, 5, 6]
101
+ }
102
+ context "with explicit start and stop" do
103
+ context "with implicit step" do
104
+ should "parse to single terminal" do
105
+ path = '$[2:4]'
106
+ assert_resolves(@shallow, path, [3, 4, 5])
107
+ end
108
+ should "parse to multiple terminals" do
109
+ path = '$[2:4].*.name'
110
+ assert_resolves(@deep, path, %w(e1 f1 g1 h1 i1 j1))
111
+ end
112
+ end
113
+ context "with explicit step" do
114
+ should "parse to single terminal" do
115
+ path = '$[2:4:2]'
116
+ assert_resolves(@shallow, path, [3, 5])
117
+ end
118
+ should "parse to multiple terminals" do
119
+ path = '$[2:4:2].*.name'
120
+ assert_resolves(@deep, path, %w(e1 f1 i1 j1))
121
+ end
122
+ end
123
+ end
124
+ context "with explicit start and implict stop" do
125
+ context "with implicit step" do
126
+ should "parse to single terminal" do
127
+ path = '$[2:]'
128
+ assert_resolves(@shallow, path, [3, 4, 5, 6])
129
+ end
130
+ should "parse to multiple terminals" do
131
+ path = '$[2:].*.name'
132
+ assert_resolves(@deep, path, %w(e1 f1 g1 h1 i1 j1 k1 l1 m1 n1))
133
+ end
134
+ end
135
+ context "with explicit step" do
136
+ should "parse to single terminal" do
137
+ path = '$[2::2]'
138
+ assert_resolves(@shallow, path, [3, 5])
139
+ end
140
+ should "parse to multiple terminals" do
141
+ path = '$[2::2].*.name'
142
+ assert_resolves(@deep, path, %w(e1 f1 i1 j1 m1 n1))
143
+ end
144
+ end
145
+ end
146
+ end
147
+ context "supporting filters in Ruby" do
148
+ setup do
149
+ @numbers = [1, 2, 3, 4, 5, 6, 7, 8]
150
+ @hashes = [
151
+ {"name" => 'Bruce', "age" => 29},
152
+ {"name" => "Braedyn", "age" => 3},
153
+ {"name" => "Jamis", "age" => 2},
154
+ ]
155
+ end
156
+ context "when using self-contained single statements" do
157
+ should "support simple object operations" do
158
+ path = '$[?(@ % 2 == 0)]'
159
+ assert_resolves(@numbers, path, [2, 4, 6, 8])
160
+ end
161
+ should "support manual object pathing" do
162
+ path = %($[?(@['age'] % 2 == 0)].name)
163
+ assert_resolves(@hashes, path, ["Jamis"])
164
+ end
165
+ end
166
+
167
+ end
168
+ context "descendants" do
169
+ setup do
170
+ @object = {
171
+ "a" => [1, 2, [3, 4]],
172
+ "b" => {
173
+ "c" => 5,
174
+ "e" => [6, 7]
175
+ }
176
+ }
177
+ end
178
+ should "be found with wildcard" do
179
+ path = '$..*'
180
+ assert_resolves(@object, path, [[1, 2, [3, 4]], 1, 2, [3, 4], 3, 4, {"c" => 5, "e" => [6, 7]}, 5, [6, 7], 6, 7, @object])
181
+ end
182
+ should "be found with deeper key" do
183
+ path = '$..e'
184
+ assert_resolves(@object, path, [[6, 7]])
185
+ end
186
+ should "be found with deeper index" do
187
+ path = '$..[0]'
188
+ assert_resolves(@object, path, [1, 3, 6])
189
+ end
190
+ should "resolve deeper chained selectors" do
191
+ path = '$..e[?(@ % 2 == 0)]'
192
+ assert_resolves(@object, path, [6])
193
+ end
194
+
195
+ end
196
+
197
+ end
198
+
199
+ end
@@ -0,0 +1,136 @@
1
+ require File.dirname(__FILE__) << "/test_helper"
2
+ require 'json'
3
+
4
+ # The content below was taken from the tests for the JS and PHP
5
+ # reference implementations at http://code.google.com/p/jsonpath/
6
+ class ReferenceTest < Test::Unit::TestCase
7
+
8
+ context 'Sample 1' do
9
+ setup { @json = %({"a":"a","b":"b","c d":"e"}) }
10
+ should 'resolve a simple path' do
11
+ assert_resolves(object, "$.a", ["a"])
12
+ end
13
+ should 'resolve a path with quotes in brackets' do
14
+ assert_resolves(object, "$['a']", ["a"])
15
+ end
16
+ should 'resolve a path with a space' do
17
+ assert_resolves(object, "$.'c d'", ["e"])
18
+ end
19
+ should 'resolve a star' do
20
+ assert_resolves(object, "$.*", ["a", "b", "e"])
21
+ end
22
+ should 'resolve a star with quotes in brackets' do
23
+ assert_resolves(object, "$['*']", ["a", "b", "e"])
24
+ end
25
+ should 'resolve a star with quotes' do
26
+ assert_resolves(object, "$[*]", ["a", "b", "e"])
27
+ end
28
+ end
29
+ context 'Sample 2' do
30
+ setup { @json = %([1, "2", 3.14, true, null]) }
31
+ should 'resolve with a number in brackets' do
32
+ assert_resolves(object, "$[0]", [1])
33
+ end
34
+ should 'resolve another number in brackets' do
35
+ assert_resolves(object, "$[4]", [nil])
36
+ end
37
+ should 'resolve a star in brackets' do
38
+ assert_resolves(object, "$[*]", [1, "2", 3.14, true, nil])
39
+ end
40
+ should 'resolve an end slice' do
41
+ assert_resolves(object, "$[-1:]", [nil])
42
+ end
43
+ end
44
+ context 'Sample 3' do
45
+ setup { @json = %({"points":[{"id": "i1", "x": 4, "y": -5}, {"id": "i2", "x": -2, "y": 2, "z": 1}, {"id": "i3", "x": 8, "y": 3}, {"id": "i4", "x": -6, "y": -1}, {"id": "i5", "x": 0, "y": 2, "z": 1}, {"id": "i6", "x": 1, "y": 4}]}) }
46
+ should 'resolve correctly' do
47
+ assert_resolves(object, "$.points[1]", [{"id" => "i2", "x" => -2, "y" => 2, "z" => 1}])
48
+ end
49
+ should 'resolve a chained path' do
50
+ assert_resolves(object, "$.points[4].x", [0])
51
+ end
52
+ should 'resolve by attribute match' do
53
+ assert_resolves(object, "$.points[?(@['id']=='i4')].x", [-6])
54
+ end
55
+ should 'resolve a chained path with a star in brackets' do
56
+ assert_resolves(object, "$.points[*].x", [4, -2, 8, -6, 0, 1])
57
+ end
58
+ should 'resolve by attribute operation' do
59
+ assert_resolves(object, "$['points'][?(@['x']*@['x']+@['y']*@['y'] > 50)].id", ["i3"])
60
+ end
61
+ should 'resolve by attribute existence' do
62
+ assert_resolves(object, "$.points[?(@['z'])].id", ["i2", "i5"])
63
+ end
64
+ should 'resolve by length property operation' do
65
+ assert_resolves(object, "$.points[(@.length-1)].id", ["i6"])
66
+ end
67
+ end
68
+ context 'Sample 4' do
69
+ setup { @json = %({"menu":{"header":"SVG Viewer","items":[{"id": "Open"}, {"id": "OpenNew", "label": "Open New"}, null, {"id": "ZoomIn", "label": "Zoom In"}, {"id": "ZoomOut", "label": "Zoom Out"}, {"id": "OriginalView", "label": "Original View"}, null, {"id": "Quality"}, {"id": "Pause"}, {"id": "Mute"}, null, {"id": "Find", "label": "Find..."}, {"id": "FindAgain", "label": "Find Again"}, {"id": "Copy"}, {"id": "CopyAgain", "label": "Copy Again"}, {"id": "CopySVG", "label": "Copy SVG"}, {"id": "ViewSVG", "label": "View SVG"}, {"id": "ViewSource", "label": "View Source"}, {"id": "SaveAs", "label": "Save As"}, null, {"id": "Help"}, {"id": "About", "label": "About Adobe CVG Viewer..."}]}}) }
70
+ should 'resolve testing on attribute' do
71
+ assert_resolves(object, "$.menu.items[?(@ && @['id'] && !@['label'])].id", ["Open", "Quality", "Pause", "Mute", "Copy", "Help"])
72
+ end
73
+ should 'resolve testing on attribute with regular expression' do
74
+ assert_resolves(object, "$.menu.items[?(@ && @['label'] && @['label'] =~ /SVG/)].id", ["CopySVG", "ViewSVG"])
75
+ end
76
+ should 'resolve on negative' do
77
+ # !nil == true in Ruby
78
+ assert_resolves(object, "$.menu.items[?(!@)]", [nil, nil, nil, nil])
79
+ end
80
+ should 'resolve descendant with number in brackets' do
81
+ assert_resolves(object, "$..[0]", [{"id" => "Open"}])
82
+ end
83
+ end
84
+ context 'Sample 5' do
85
+ setup { @json = %({"a":[1, 2, 3, 4],"b":[5, 6, 7, 8]}) }
86
+ should 'resolve descendant with number in brackets' do
87
+ assert_resolves(object, "$..[0]", [1, 5])
88
+ end
89
+ should 'resolve descendant last items' do
90
+ assert_resolves(object, "$..[-1:]", [4, 8])
91
+ end
92
+ should 'resolve by descendant value' do
93
+ assert_resolves(object, "$..[?(@.is_a?(Numeric) && @ % 2 == 0)]", [2, 4, 6, 8])
94
+ end
95
+ end
96
+ context 'Sample 6' do
97
+ setup { @json = %({"lin":{"color":"red","x":2,"y":3},"cir":{"color":"blue","x":5,"y":2,"r":1},"arc":{"color":"green","x":2,"y":4,"r":2,"phi0":30,"dphi":120},"pnt":{"x":0,"y":7}}) }
98
+ should 'resolve by operation in quotes' do
99
+ assert_resolves(object, "$.'?(@['color'])'.x", [2, 5, 2])
100
+ end
101
+ should 'resolve by multiple quoted values in brackets' do
102
+ assert_resolves(object, "$['lin','cir'].color", ["red", "blue"])
103
+ end
104
+ end
105
+ context 'Sample 7' do
106
+ setup { @json = %({"text":["hello", "world2.0"]}) }
107
+ should 'resolve correctly filter 1' do
108
+ assert_resolves(object, "$.text[?(@.length > 5)]", ["world2.0"])
109
+ end
110
+ should 'resolve correctly filter 2' do
111
+ assert_resolves(object, "$.text[?(@[0, 1] == 'h')]", ["hello"])
112
+ end
113
+ end
114
+ context 'Sample 8' do
115
+ setup { @json = %({"a":{"a":2,"b":3},"b":{"a":4,"b":5},"c":{"a":{"a":6,"b":7},"c":8}}) }
116
+ should 'resolve descendant' do
117
+ assert_resolves(object, "$..a", [{"a" => 2, "b" => 3}, 2, 4, {"a" => 6, "b" => 7}, 6])
118
+ end
119
+ end
120
+ context 'Sample 10' do
121
+ setup { @json = %({"a":[{"a": 5, "@": 2, "$": 3}, {"a": 6, "@": 3, "$": 4}, {"a": 7, "@": 4, "$": 5}]}) }
122
+ should 'resolve with quoted operation and escaped special character' do
123
+ assert_resolves(object, "$.a[?(@['\\@']==3)]", [{"a" => 6, "@" => 3, "$" => 4}])
124
+ end
125
+ should 'resolve with quotes and brackets in operation' do
126
+ assert_resolves(object, "$.a[?(@['$']==5)]", [{"a" => 7, "@" => 4, "$" => 5}])
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def object
133
+ JSON.parse(@json)
134
+ end
135
+
136
+ end
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'jsonpath'
8
+
9
+ class Test::Unit::TestCase
10
+
11
+ private
12
+
13
+ def parser
14
+ @parser ||= JSONPath::Parser.new
15
+ end
16
+
17
+ def parse(path)
18
+ parser.parse(path)
19
+ end
20
+
21
+ def assert_parses(path)
22
+ result = parse(path)
23
+ assert result, parser.failure_reason
24
+ end
25
+
26
+ def assert_resolves(obj, path, result)
27
+ assert_parses path
28
+ assert_equal safe_sort(result), safe_sort(parse(path).walk(obj))
29
+ end
30
+
31
+ def safe_sort(objs)
32
+ objs.sort_by do |obj|
33
+ obj ? obj.to_s : 0.to_s
34
+ end
35
+ end
36
+
37
+ end