siren 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +16 -0
- data/Manifest.txt +22 -0
- data/README.txt +318 -0
- data/Rakefile +19 -0
- data/lib/siren.rb +76 -0
- data/lib/siren/json.rb +1039 -0
- data/lib/siren/json.tt +86 -0
- data/lib/siren/json_query.rb +2414 -0
- data/lib/siren/json_query.tt +184 -0
- data/lib/siren/json_query_nodes.rb +333 -0
- data/lib/siren/node.rb +37 -0
- data/lib/siren/parser.rb +205 -0
- data/lib/siren/reference.rb +30 -0
- data/lib/siren/walker.rb +33 -0
- data/test/fixtures/beatles.json +11 -0
- data/test/fixtures/car.rb +14 -0
- data/test/fixtures/names.json +7 -0
- data/test/fixtures/people.json +34 -0
- data/test/fixtures/person.rb +15 -0
- data/test/fixtures/refs.json +26 -0
- data/test/fixtures/store.json +32 -0
- data/test/test_siren.rb +181 -0
- metadata +109 -0
data/History.txt
ADDED
@@ -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
|
+
|
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/lib/siren.rb
ADDED
@@ -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
|
+
|