bruce-jsonpath 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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