siren 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ === 0.2.0 / 2009-07-07
2
+
3
+ * Mostly complete implementation of JSONQuery as described
4
+ by SitePen. This would have been 0.1 but it supercedes
5
+ John's version.
6
+
7
+
8
+ === 0.1.1 / 2009-06-22
9
+
10
+ * Incomplete fork released through GitHub by John Nunemaker
11
+
12
+
13
+ === 0.1.0 / Never released
14
+
15
+ * Work begun January 2009, left incomplete.
16
+
@@ -0,0 +1,22 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/siren.rb
6
+ lib/siren/json.tt
7
+ lib/siren/json.rb
8
+ lib/siren/json_query.tt
9
+ lib/siren/json_query.rb
10
+ lib/siren/json_query_nodes.rb
11
+ lib/siren/node.rb
12
+ lib/siren/parser.rb
13
+ lib/siren/reference.rb
14
+ lib/siren/walker.rb
15
+ test/test_siren.rb
16
+ test/fixtures/beatles.json
17
+ test/fixtures/car.rb
18
+ test/fixtures/names.json
19
+ test/fixtures/people.json
20
+ test/fixtures/person.rb
21
+ test/fixtures/refs.json
22
+ test/fixtures/store.json
@@ -0,0 +1,318 @@
1
+ = Siren
2
+
3
+ * http://github.com/jcoglan/siren
4
+
5
+ Siren is a JSON and JSONQuery interpreter for Ruby. It extends the normal functionality
6
+ of JSON-to-Ruby processing with cross-references, automatic typecasting,
7
+ and a succinct query language for filtering JSON and Ruby object graphs.
8
+
9
+
10
+ == Installation
11
+
12
+ sudo gem install siren
13
+
14
+
15
+ == Usage
16
+
17
+ As expected, Siren can be used as a basic JSON parser, though if that's all you
18
+ want you'll be better off with more performant libraries like the +json+ gem.
19
+
20
+ Siren.parse '[{"name": "mike"}]'
21
+ #=> [{"name"=>"mike"}]
22
+
23
+ The features listed below go beyond standard JSON and are modelled on the feature
24
+ set listed in this SitePen article:
25
+
26
+ http://www.sitepen.com/blog/2008/07/16/jsonquery-data-querying-beyond-jsonpath/
27
+
28
+
29
+ === Cross-references
30
+
31
+ Siren allows JSON objects to be given IDs, and for other objects to make
32
+ references back to them from within the same document. This allows for cyclic
33
+ data structures and other objects inexpressible in standard JSON.
34
+
35
+ data = Siren.parse <<-JSON
36
+ [
37
+ {
38
+ "id": "obj1",
39
+ "name": "mike"
40
+ }, {
41
+ "name": "bob",
42
+ "friend": {"$ref": "obj1"}
43
+ }
44
+ ]
45
+ JSON
46
+
47
+ #=> [ {"name" => "mike", "id" => "obj1"},
48
+ {"name" => "bob", "friend" => {"name" => "mike", "id" => "obj1"} } ]
49
+
50
+ So +bob+ has a cross-reference to +mike+, which the parser resolves for us. Note
51
+ +mike+ is not copied but is referenced by +bob+:
52
+
53
+ data[0].__id__ #=> -607191558
54
+ data[1]['friend'].__id__ #=> -607191558
55
+
56
+ The general syntax for a cross-reference is <tt>{"$ref": ID_STRING}</tt>; the parser
57
+ will replace this with the referenced object in the Ruby output.
58
+
59
+
60
+ === Automatic typecasting
61
+
62
+ JSON parsers typically output hashes and arrays, these being Ruby's closest analogues
63
+ to the JavaScript types that JSON expresses. Siren allows objects to be imbued with
64
+ a +type+ attribute, which if present causes the parser to cast the object to an
65
+ instance of the named type instead of a hash. Instead of hash keys, the keys in the
66
+ JSON object become instance variables of the typed object. To allow the parser to use
67
+ a class for casting, it must be extended using <tt>Siren::Node</tt>:
68
+
69
+ class PersonModel
70
+ extend Siren::Node
71
+ end
72
+
73
+ data = Siren.parse <<-JSON
74
+ {
75
+ "type": "person_model",
76
+ "name": "Jimmy",
77
+ "age": 25
78
+ }
79
+ JSON
80
+
81
+ #=> #<PersonModel @type="person_model", @age=25, @name="Jimmy">
82
+
83
+
84
+ == JSONQuery
85
+
86
+ JSONQuery is a language designed for filtering and transforming JSON-like object graphs.
87
+ Queries may be run against Ruby objects of any type, and may also be used as values in
88
+ JSON documents to generate data during parsing. For example:
89
+
90
+ # Run a filter against an array
91
+ adults = Siren.query "$[? @.age >= 18 ]", people
92
+
93
+ # Embed the query for expansion by the parser
94
+ people = Siren.parse <<-JSON
95
+ {
96
+ "people": [
97
+ {"name": "John", "age": 23},
98
+ {"name": "Paul", "age": 21},
99
+ {"name": "George", "age": 12},
100
+ {"name": "Ringo", "age": 17}
101
+ ],
102
+
103
+ "adults": {"$ref": "$.people[? @.age >= 18 ]"}
104
+ }
105
+ JSON
106
+
107
+ As for IDs, queries are embedded using the <tt>$ref</tt> syntax. Parsing the above
108
+ yields the following Ruby object:
109
+
110
+ { "adults" => [ {"name"=>"John", "age"=>23},
111
+ {"name"=>"Paul", "age"=>21}
112
+ ],
113
+ "people" => [ {"name"=>"John", "age"=>23},
114
+ {"name"=>"Paul", "age"=>21},
115
+ {"name"=>"George", "age"=>12},
116
+ {"name"=>"Ringo", "age"=>17}
117
+ ]
118
+ }
119
+
120
+ Siren supports the following sections of the JSONQuery language. A query is composed of
121
+ an object selector followed by zero or more filters.
122
+
123
+
124
+ === Object selectors
125
+
126
+ An object selector is either a valid object ID, or the special symbols <tt>$</tt>
127
+ or <tt>@</tt>.
128
+
129
+ * An object ID is a letter followed by zero or more letters, numbers or underscores.
130
+ Use of an ID produces a reference to the object with that +id+ property, as illustrated
131
+ above.
132
+ * <tt>$</tt> represents the root object the query is being run against, or the root of
133
+ the JSON document currently being parsed.
134
+ * <tt>@</tt> represents the current object in a looping operation, such as a map, sort
135
+ or filter.
136
+
137
+
138
+ === Expressions
139
+
140
+ Expressions are used within filters to generate data and perform comparisons. They work
141
+ in a similar fashion to JavaScript expressions, and may contain strings (enclosed in single
142
+ quotes), numbers, +true+, +false+, +null+ or other JSONQuery expressions. The following
143
+ operators are supported:
144
+
145
+ * Arithmetic: <tt>+</tt>, <tt>-</tt>, <tt>*</tt>, <tt>/</tt>, <tt>%</tt>
146
+ * Comparison: <tt><</tt>, <tt><=</tt>, <tt>></tt>, <tt>>=</tt>
147
+ * Equality: <tt>=</tt> for equality, <tt>!=</tt> for inequality
148
+ * String matching: <tt>=</tt> for case-sensitive matching, <tt>~</tt> for case-insensitive
149
+ * Logic: <tt>&</tt> for boolean AND, <tt>|</tt> boolean OR (not bitwise)
150
+ * Subexpressions are delimited using parentheses <tt>(</tt> and <tt>)</tt>
151
+
152
+
153
+ === String matching
154
+
155
+ The <tt>=</tt> and <tt>~</tt> operators, when used with strings, perform case-sensitive
156
+ and case-insensitive matching respectively. Within a string, <tt>*</tt> matches zero or
157
+ more characters of any type, and <tt>?</tt> matches a single character.
158
+
159
+ Siren.query "$[? @ = 'b*']", %w[foo bar baz]
160
+ #=> ["bar", "baz"]
161
+
162
+ Siren.query "$[? @ = 'b?']", %w[foo bar baz]
163
+ #=> []
164
+
165
+ Siren.query "$[? @ ~ 'BA?']", %w[foo bar baz]
166
+ #=> ["bar", "baz"]
167
+
168
+
169
+ === Field access filter
170
+
171
+ A field access filter selects a named field from an object. Fields are accessed using the
172
+ dot notation, or the bracket notation with an expression evaluating to a string or an integer.
173
+ This filter does one of the following based on the object's type:
174
+
175
+ * If the object is a +Hash+, the value for the named key is produced.
176
+ * If the object is an +Array+, the value at the given index is produced.
177
+ * If the object has the named method, the value of that method call is returned.
178
+ * Otherwise, the value of the named instance variable is produced.
179
+
180
+ data = {
181
+ "key" => "foo",
182
+ "name" => { "foo" => "bar" }
183
+ }
184
+
185
+ Siren.query "$.key", data
186
+ #=> "foo"
187
+
188
+ Siren.query "$[1]", %w[foo bar whizz]
189
+ #=> "bar"
190
+
191
+ Siren.query "$.name[ $.key ]", data
192
+ #=> "bar"
193
+
194
+ Siren.query "$.size", [3,4,5]
195
+ #=> 3
196
+
197
+ Fields can also be accessed recursively; the syntax <tt>..symbol</tt> returns an array
198
+ of all the values in the object with the property name +symbol+.
199
+
200
+ data = {"rec" => 6, "key" => {"rec" => 7}}
201
+
202
+ Siren.query "$..rec", data
203
+ #=> [6, 7]
204
+
205
+
206
+ === Array slice filter
207
+
208
+ The array slice filter selects a subset of values from an array. The syntax is <tt>[a:b:s]</tt>,
209
+ where +a+ is the start index, +b+ the end index and +s+ the step amount. The step may be
210
+ omitted (as in <tt>[a:b]</tt>), in which case the step defaults to <tt>1</tt>.
211
+
212
+ Siren.query "$[1:5:2]", %w[a b c d e f g h]
213
+ #=> ["b", "d", "f"]
214
+
215
+
216
+ === Selection filter
217
+
218
+ The selection filter extracts a subset of an array using a predicate condition.
219
+ Predicates are enclosed in <tt>[? ... ]</tt>, and within this block the symbol <tt>@</tt>
220
+ refers to each member of the array in turn. For example:
221
+
222
+ Siren.query "$.strings[? @.length > 3]", {'strings' => %w[the quick brown fox]}
223
+ #=> ["quick", "brown"]
224
+
225
+ Siren.query "$[? @ > 2 & @ <= 5]", [9,2,7,3,8,5]
226
+ #=> [3, 5]
227
+
228
+ Selections can be made recursively by prefixing the filter with two dots (<tt>..</tt>).
229
+ This will search the filtered object recursively for any properties that match the
230
+ predicate expression, and return them as a flat array.
231
+
232
+ data = [
233
+ 8,2,5,7,3,
234
+ {
235
+ 'foo' => 12,
236
+ 'bar' => 7,
237
+ 'whizz' => [9,2,6,4]
238
+ }
239
+ ]
240
+
241
+ Siren.query "$..[? @ % 4 = 0]", data
242
+ #=> [8, 4, 12]
243
+
244
+
245
+ === Mapping filter
246
+
247
+ Mappings work like Ruby's <tt>Enumerable#map</tt> method, producing a new array by
248
+ applying some expression to the members of another. Mappings are expressed by enclosing
249
+ the mapping expression in <tt>[= ... ]</tt>
250
+
251
+ Siren.query "$[= @[ 'upcase' ] ]", %w[the quick brown fox]
252
+ #=> ["THE", "QUICK", "BROWN", "FOX"]
253
+
254
+ Siren.query "$[= @.length + 2]", %w[the quick brown fox]
255
+ #=> [5, 7, 7, 5]
256
+
257
+ Just like other types of filters, mapping filters may use subqueries inside the
258
+ main query:
259
+
260
+ Siren.query "$[= @[? @ > 4] ]", [ [2,9,4,3], [5,8,2], [3,6] ]
261
+ #=> [[9], [5, 8], [6]]
262
+
263
+ Siren.query "$[= @[? @ > 4][0] + @.size ]", [ [2,9,4,3], [5,8,2], [3,6] ]
264
+ #=> [13, 8, 8]
265
+
266
+
267
+ === Sorting filter
268
+
269
+ The sorting filter returns a sorted version of the array it is operating on. The sort
270
+ must contain one or more comma-separated expressions to use to compare the items in the
271
+ array, and each expression must specify ascending (<tt>/</tt>) or descending (<tt>\\</tt>)
272
+ order. Expressions are prioritized from left to right. For example, the following sorts
273
+ the people in the list in ascending order by last name, then in descending order by first
274
+ name for people with the same last name:
275
+
276
+ people = Siren.parse <<-JSON
277
+ [
278
+ {"first": "Thom", "last": "Yorke"},
279
+ {"first": "Jonny", "last": "Greenwood"},
280
+ {"first": "Ed", "last": "O'Brien"},
281
+ {"first": "Colin", "last": "Greenwood"},
282
+ {"first": "Phil", "last": "Selway"}
283
+ ]
284
+ JSON
285
+
286
+ Siren.query "$[ /@.last, \\@.first ]", people
287
+
288
+ # => [ { "last"=>"Greenwood", "first"=>"Jonny"},
289
+ # { "last"=>"Greenwood", "first"=>"Colin"},
290
+ # { "last"=>"O'Brien", "first"=>"Ed"},
291
+ # { "last"=>"Selway", "first"=>"Phil"},
292
+ # { "last"=>"Yorke", "first"=>"Thom"} ]
293
+
294
+
295
+ == License
296
+
297
+ (The MIT License)
298
+
299
+ Copyright (c) 2009 James Coglan
300
+
301
+ Permission is hereby granted, free of charge, to any person obtaining
302
+ a copy of this software and associated documentation files (the
303
+ 'Software'), to deal in the Software without restriction, including
304
+ without limitation the rights to use, copy, modify, merge, publish,
305
+ distribute, sublicense, and/or sell copies of the Software, and to
306
+ permit persons to whom the Software is furnished to do so, subject to
307
+ the following conditions:
308
+
309
+ The above copyright notice and this permission notice shall be
310
+ included in all copies or substantial portions of the Software.
311
+
312
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
313
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
314
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
315
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
316
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
317
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
318
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/siren.rb'
6
+
7
+ Hoe.spec('siren') do |p|
8
+ # p.rubyforge_name = 'sirenx' # if different than lowercase project name
9
+ p.developer('James Coglan', 'jcoglan@googlemail.com')
10
+ p.extra_deps = %w[treetop eventful]
11
+ end
12
+
13
+ task :tt do
14
+ %w(json json_query).each do |grammar|
15
+ `tt lib/siren/#{grammar}.tt`
16
+ end
17
+ end
18
+
19
+ # vim: syntax=Ruby
@@ -0,0 +1,76 @@
1
+ require 'set'
2
+ require 'rubygems'
3
+ require 'treetop'
4
+ require 'eventful'
5
+
6
+ %w[ json json_query json_query_nodes
7
+ walker parser node reference
8
+ ].each do |path|
9
+ require File.dirname(__FILE__) + '/siren/' + path
10
+ end
11
+
12
+ module Siren
13
+ VERSION = '0.2.0'
14
+
15
+ TYPE_FIELD = "type"
16
+ ID_FIELD = "id"
17
+ REF_FIELD = "$ref"
18
+
19
+ class JsonParser
20
+ include Siren::Walker
21
+ end
22
+
23
+ def self.each(object, &block)
24
+ case object
25
+ when Array then object.each_with_index { |x,i| yield(i,x) }
26
+ when Hash then object.each { |k,v| yield(k,v) }
27
+ when Enumerable then each(object.to_a, &block)
28
+ else nil
29
+ end
30
+ end
31
+
32
+ def self.parse(string, &block)
33
+ @json_parser ||= JsonParser.new
34
+ Reference.flush!
35
+ @symbols = {}
36
+
37
+ result = @json_parser.parse(string).value rescue nil
38
+
39
+ @json_parser.walk(result) do |holder, key, value|
40
+ if Hash === value && value[REF_FIELD]
41
+ value = Reference.new(value)
42
+ value.on(:resolve) do |ref, root, symbols|
43
+ holder[key] = ref.find(root, symbols, holder)
44
+ end
45
+ end
46
+ value
47
+ end
48
+
49
+ @json_parser.walk(result) do |holder, key, value|
50
+ if Hash === value
51
+ id = value[ID_FIELD]
52
+ value = Node.from_json(value)
53
+ @symbols[id] = value
54
+ end
55
+ value
56
+ end
57
+
58
+ result = Node.from_json(result)
59
+
60
+ Reference.resolve!(result, @symbols)
61
+ @json_parser.walk(result, &block) if block_given?
62
+
63
+ result
64
+ end
65
+
66
+ def self.query(expression, root)
67
+ compile_query(expression).value(root, @symbols || {})
68
+ end
69
+
70
+ def self.compile_query(expression)
71
+ @query_parser ||= JsonQueryParser.new
72
+ @query_cache ||= {}
73
+ @query_cache[expression] ||= @query_parser.parse(expression)
74
+ end
75
+ end
76
+