bruce-jsonpath 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
data/lib/jsonpath.rb ADDED
@@ -0,0 +1,22 @@
1
+ require 'rubygems'
2
+ require 'treetop'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ require 'jsonpath/parser'
6
+ require 'jsonpath/nodes'
7
+
8
+ module JSONPath
9
+
10
+ Parser = ::JSONPathGrammarParser
11
+ class ParseError < ::SyntaxError; end
12
+
13
+ def self.lookup(obj, path)
14
+ parser = Parser.new
15
+ if (result = parser.parse(path))
16
+ result.walk(obj)
17
+ else
18
+ raise ParseError, parser.failure_reason
19
+ end
20
+ end
21
+
22
+ end
@@ -0,0 +1,180 @@
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 "parse bareword child with single terminal" do
10
+ path = '$.a'
11
+ assert_resolves({"a" => 1}, path, [1])
12
+ end
13
+ should "parse subscripted quoted child with single terminal" do
14
+ path = "$['a b']"
15
+ assert_resolves({"a b" => 1}, path, [1])
16
+ end
17
+ should "parse subscripted quoted child and chained bareword" do
18
+ path = "$['a b'].c"
19
+ assert_resolves({"a b" => {"c" => 1}}, path, [1])
20
+ end
21
+ should "parses bare wildcard" do
22
+ path = "$.*"
23
+ assert_resolves({"a" => 1, "b" => 2}, path, [1, 2])
24
+ end
25
+ should "parse quoted wildcard on hash" do
26
+ path = "$['*']"
27
+ assert_resolves({"a" => 1, "b" => 2}, path, [1, 2])
28
+ end
29
+ should "parse quoted key outside of brackets" do
30
+ path = "$.'a b'"
31
+ assert_resolves({"a b" => 1}, path, [1])
32
+ end
33
+ end
34
+ context "array" do
35
+ should "parse bare wildcard" do
36
+ path = "$.*"
37
+ assert_resolves([1, 2, 3], path, [1, 2, 3])
38
+ end
39
+ should "parses subscripted quoted wildcard" do
40
+ path = "$['*']"
41
+ assert_resolves([1, 2, 3], path, [1, 2, 3])
42
+ end
43
+ should "parses through bare wildcard" do
44
+ path = "$.*.name"
45
+ assert_resolves([{"name" => 1}, {"name" => 2}, {"name" => 3}], path, [1, 2, 3])
46
+ end
47
+ should "parse index to single terminal" do
48
+ path = "$[1]"
49
+ assert_resolves(%w(foo bar baz), path, %w(bar))
50
+ end
51
+ should "parse index to multiple terminals" do
52
+ path = "$.*[1].name"
53
+ assert_resolves({
54
+ "a" => [1, {"name" => 2}, 3],
55
+ "b" => [4, {"name" => 5}, 6],
56
+ "c" => [7, {"name" => 8}, 9],
57
+ }, path, [2, 5, 8])
58
+ end
59
+ end
60
+ context "combination wildcards" do
61
+ should "parses through bare wildcard on array with additional wildcard" do
62
+ path = "$.*.names.*"
63
+ assert_resolves([
64
+ {"names" => %w(foo bar)},
65
+ {"names" => %w(baz quux)},
66
+ {"names" => %w(spam eggs)}
67
+ ], path, %w(foo bar baz quux spam eggs))
68
+ end
69
+ end
70
+ context "using slices" do
71
+ setup {
72
+ @deep = [
73
+ {"a" => {"name" => "a1"}, "b" => {"name" => "b1"}},
74
+ {"c" => {"name" => "c1"}, "d" => {"name" => "d1"}},
75
+ {"e" => {"name" => "e1"}, "f" => {"name" => "f1"}},
76
+ {"g" => {"name" => "g1"}, "h" => {"name" => "h1"}},
77
+ {"i" => {"name" => "i1"}, "j" => {"name" => "j1"}},
78
+ {"k" => {"name" => "k1"}, "l" => {"name" => "l1"}},
79
+ {"m" => {"name" => "m1"}, "n" => {"name" => "n1"}},
80
+ ]
81
+ @shallow = [1, 2, 3, 4, 5, 6]
82
+ }
83
+ context "with explicit start and stop" do
84
+ context "with implicit step" do
85
+ should "parse to single terminal" do
86
+ path = '$[2:4]'
87
+ assert_resolves(@shallow, path, [3, 4, 5])
88
+ end
89
+ should "parse to multiple terminals" do
90
+ path = '$[2:4].*.name'
91
+ assert_resolves(@deep, path, %w(e1 f1 g1 h1 i1 j1))
92
+ end
93
+ end
94
+ context "with explicit step" do
95
+ should "parse to single terminal" do
96
+ path = '$[2:4:2]'
97
+ assert_resolves(@shallow, path, [3, 5])
98
+ end
99
+ should "parse to multiple terminals" do
100
+ path = '$[2:4:2].*.name'
101
+ assert_resolves(@deep, path, %w(e1 f1 i1 j1))
102
+ end
103
+ end
104
+ end
105
+ context "with explicit start and implict stop" do
106
+ context "with implicit step" do
107
+ should "parse to single terminal" do
108
+ path = '$[2:]'
109
+ assert_resolves(@shallow, path, [3, 4, 5, 6])
110
+ end
111
+ should "parse to multiple terminals" do
112
+ path = '$[2:].*.name'
113
+ assert_resolves(@deep, path, %w(e1 f1 g1 h1 i1 j1 k1 l1 m1 n1))
114
+ end
115
+ end
116
+ context "with explicit step" do
117
+ should "parse to single terminal" do
118
+ path = '$[2::2]'
119
+ assert_resolves(@shallow, path, [3, 5])
120
+ end
121
+ should "parse to multiple terminals" do
122
+ path = '$[2::2].*.name'
123
+ assert_resolves(@deep, path, %w(e1 f1 i1 j1 m1 n1))
124
+ end
125
+ end
126
+ end
127
+ end
128
+ context "supporting filters in Ruby" do
129
+ setup do
130
+ @numbers = [1, 2, 3, 4, 5, 6, 7, 8]
131
+ @hashes = [
132
+ {"name" => 'Bruce', "age" => 29},
133
+ {"name" => "Braedyn", "age" => 3},
134
+ {"name" => "Jamis", "age" => 2},
135
+ ]
136
+ end
137
+ context "when using self-contained single statements" do
138
+ should "support simple object operations" do
139
+ path = '$[?(@ % 2 == 0)]'
140
+ assert_resolves(@numbers, path, [2, 4, 6, 8])
141
+ end
142
+ should "support manual object pathing" do
143
+ path = %($[?(@['age'] % 2 == 0)].name)
144
+ assert_resolves(@hashes, path, ["Jamis"])
145
+ end
146
+ end
147
+
148
+ end
149
+ context "descendants" do
150
+ setup do
151
+ @object = {
152
+ "a" => [1, 2, [3, 4]],
153
+ "b" => {
154
+ "c" => 5,
155
+ "e" => [6, 7]
156
+ }
157
+ }
158
+ end
159
+ should "be found with wildcard" do
160
+ path = '$..*'
161
+ 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])
162
+ end
163
+ should "be found with deeper key" do
164
+ path = '$..e'
165
+ assert_resolves(@object, path, [[6, 7]])
166
+ end
167
+ should "be found with deeper index" do
168
+ path = '$..[0]'
169
+ assert_resolves(@object, path, [1, 3, 6])
170
+ end
171
+ should "resolve deeper chained selectors" do
172
+ path = '$..e[?(@ % 2 == 0)]'
173
+ assert_resolves(@object, path, [6])
174
+ end
175
+
176
+ end
177
+
178
+ end
179
+
180
+ 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
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bruce-jsonpath
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Bruce Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-17 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: treetop
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description:
26
+ email: bruce@codefluency.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.markdown
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.markdown
39
+ - Rakefile
40
+ - VERSION
41
+ - lib/jsonpath.rb
42
+ - lib/jsonpath/nodes.rb
43
+ - lib/jsonpath/parser.rb
44
+ - lib/jsonpath/parser.treetop
45
+ - test/parser_test.rb
46
+ - test/reference_test.rb
47
+ - test/test_helper.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/bruce/jsonpath
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.2.0
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: JSONPath support for Ruby
74
+ test_files:
75
+ - test/parser_test.rb
76
+ - test/reference_test.rb
77
+ - test/test_helper.rb