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/lib/siren/node.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Siren
|
2
|
+
module Node
|
3
|
+
|
4
|
+
def from_json(hash)
|
5
|
+
object = self.new
|
6
|
+
hash.each do |key, value|
|
7
|
+
object.instance_variable_set("@#{key}", value)
|
8
|
+
if Reference === value
|
9
|
+
value.on(:resolve) do |ref, root, symbols|
|
10
|
+
object.instance_variable_set("@#{key}", ref.find(root, symbols, object))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
object
|
15
|
+
end
|
16
|
+
|
17
|
+
@classes = {}
|
18
|
+
|
19
|
+
def self.extended(base)
|
20
|
+
@classes[base.name.split('::').last] = base
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.from_json(hash)
|
24
|
+
hash = Siren.parse(hash) if String === hash
|
25
|
+
return hash unless Hash === hash && hash[TYPE_FIELD]
|
26
|
+
klass = find_class(hash[TYPE_FIELD])
|
27
|
+
klass ? klass.from_json(hash) : hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_class(name)
|
31
|
+
name = name.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
32
|
+
@classes[name]
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
data/lib/siren/parser.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
# Based on http://www.json.org/json_parse.js (public domain)
|
2
|
+
|
3
|
+
module Siren
|
4
|
+
class Parser
|
5
|
+
|
6
|
+
include Walker
|
7
|
+
|
8
|
+
ESCAPEE = {
|
9
|
+
'"' => '"',
|
10
|
+
'\\' => '\\',
|
11
|
+
'/' => '/',
|
12
|
+
'b' => '\b',
|
13
|
+
'f' => '\f',
|
14
|
+
'n' => '\n',
|
15
|
+
'r' => '\r',
|
16
|
+
't' => '\t'
|
17
|
+
}
|
18
|
+
|
19
|
+
attr_reader :at, # The index of the current character
|
20
|
+
:ch # The current character
|
21
|
+
|
22
|
+
def parse(source, &reviver)
|
23
|
+
@text = source.dup
|
24
|
+
@at, @ch = 0, ' '
|
25
|
+
result = value!
|
26
|
+
white!
|
27
|
+
error! "Syntax error" if @ch
|
28
|
+
|
29
|
+
walk(result, &reviver)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def next!(c = nil)
|
35
|
+
# If a c parameter is provided, verify that it matches the current character.
|
36
|
+
error! "Expected '#{c}' instead of '#{@ch}'" if c && c != @ch
|
37
|
+
|
38
|
+
# Get the next character. When there are no more characters,
|
39
|
+
# return the empty string.
|
40
|
+
@ch = @text[at].chr rescue nil
|
41
|
+
@at += 1
|
42
|
+
@ch
|
43
|
+
end
|
44
|
+
|
45
|
+
# Parse a number value.
|
46
|
+
def number!
|
47
|
+
string = ''
|
48
|
+
if @ch == '-'
|
49
|
+
string = '-'
|
50
|
+
next!('-')
|
51
|
+
end
|
52
|
+
while @ch >= '0' && @ch <= '9'
|
53
|
+
string += @ch
|
54
|
+
next!
|
55
|
+
end
|
56
|
+
if @ch == '.'
|
57
|
+
string += '.'
|
58
|
+
while next! && @ch >= '0' && @ch <= '9'
|
59
|
+
string += @ch
|
60
|
+
end
|
61
|
+
end
|
62
|
+
if @ch == 'e' || @ch == 'E'
|
63
|
+
string += @ch
|
64
|
+
next!
|
65
|
+
if @ch == '-' || @ch == '+'
|
66
|
+
string += @ch
|
67
|
+
next!
|
68
|
+
end
|
69
|
+
while @ch >= '0' && @ch <= '9'
|
70
|
+
string += @ch
|
71
|
+
next!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
string.to_f
|
75
|
+
rescue
|
76
|
+
error! "Bad number"
|
77
|
+
end
|
78
|
+
|
79
|
+
#Parse a string value.
|
80
|
+
def string!
|
81
|
+
string = ''
|
82
|
+
# When parsing for string values, we must look for " and \ characters.
|
83
|
+
if @ch == '"'
|
84
|
+
while next!
|
85
|
+
if @ch == '"'
|
86
|
+
next!
|
87
|
+
return string
|
88
|
+
elsif ch == '\\'
|
89
|
+
next!
|
90
|
+
if @ch == 'u'
|
91
|
+
uffff = 0
|
92
|
+
4.times do
|
93
|
+
hex = next!.to_i(16)
|
94
|
+
uffff = uffff * 16 + hex
|
95
|
+
end
|
96
|
+
string += uffff.chr
|
97
|
+
elsif String === ESCAPPE[@ch]
|
98
|
+
string += ESCAPPE[@ch]
|
99
|
+
else
|
100
|
+
break
|
101
|
+
end
|
102
|
+
else
|
103
|
+
string += @ch
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
error! "Bad string"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Skip whitespace.
|
111
|
+
def white!
|
112
|
+
next! while @ch && @ch <= ' '
|
113
|
+
end
|
114
|
+
|
115
|
+
# true, false, undefined or null.
|
116
|
+
def word!
|
117
|
+
case @ch
|
118
|
+
when 't'
|
119
|
+
%w(t r u e).each { |c| next!(c) }
|
120
|
+
return true
|
121
|
+
when 'f'
|
122
|
+
%w(f a l s e).each { |c| next!(c) }
|
123
|
+
return nil
|
124
|
+
when 'n'
|
125
|
+
%w(n u l l).each { |c| next!(c) }
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
error! "Unexpected '#{@ch}'"
|
129
|
+
end
|
130
|
+
|
131
|
+
# Parse an array value.
|
132
|
+
def array!
|
133
|
+
array = []
|
134
|
+
|
135
|
+
if @ch == '['
|
136
|
+
next!('[')
|
137
|
+
white!
|
138
|
+
if @ch == ']'
|
139
|
+
next!(']')
|
140
|
+
return array # empty array
|
141
|
+
end
|
142
|
+
while @ch
|
143
|
+
array << value!
|
144
|
+
white!
|
145
|
+
if @ch == ']'
|
146
|
+
next!(']')
|
147
|
+
return array
|
148
|
+
end
|
149
|
+
next!(',')
|
150
|
+
white!
|
151
|
+
end
|
152
|
+
end
|
153
|
+
error! "Bad array"
|
154
|
+
end
|
155
|
+
|
156
|
+
# Parse an object value.
|
157
|
+
def object!
|
158
|
+
object = {}
|
159
|
+
|
160
|
+
if @ch == '{'
|
161
|
+
next!('{')
|
162
|
+
white!
|
163
|
+
if @ch == '}'
|
164
|
+
next!('}')
|
165
|
+
return object # empty object
|
166
|
+
end
|
167
|
+
while @ch
|
168
|
+
key = string!
|
169
|
+
white!
|
170
|
+
next!(':')
|
171
|
+
error! 'Duplicate key "#{key}"' if object.has_key?(key)
|
172
|
+
object[key] = value!
|
173
|
+
white!
|
174
|
+
if @ch == '}'
|
175
|
+
next!('}')
|
176
|
+
return object
|
177
|
+
end
|
178
|
+
next!(',')
|
179
|
+
white!
|
180
|
+
end
|
181
|
+
end
|
182
|
+
error! "Bad object"
|
183
|
+
end
|
184
|
+
|
185
|
+
# Parse a JSON value. It could be an object, an array, a string, a number,
|
186
|
+
# or a word.
|
187
|
+
def value!
|
188
|
+
white!
|
189
|
+
case @ch
|
190
|
+
when '{' then object!
|
191
|
+
when '[' then array!
|
192
|
+
when '"' then string!
|
193
|
+
when '-' then number!
|
194
|
+
else @ch >= '0' && @ch <= '9' ? number! : word!
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Call error when something is wrong.
|
199
|
+
def error!(message = nil)
|
200
|
+
raise Exception
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Siren
|
2
|
+
class Reference
|
3
|
+
|
4
|
+
include Eventful
|
5
|
+
|
6
|
+
def self.flush!
|
7
|
+
@@cache = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.resolve!(root, symbols)
|
11
|
+
@@cache.each { |id, ref| ref.resolve!(root, symbols) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(hash)
|
15
|
+
@query = Siren.compile_query(hash[REF_FIELD])
|
16
|
+
@@cache ||= {}
|
17
|
+
@@cache[hash.__id__] = self
|
18
|
+
end
|
19
|
+
|
20
|
+
def resolve!(root, symbols)
|
21
|
+
fire(:resolve, root, symbols)
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(root, symbols, current)
|
25
|
+
@query.value(root, symbols, current)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
data/lib/siren/walker.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Siren
|
2
|
+
module Walker
|
3
|
+
|
4
|
+
# If there is a reviver function, we recursively walk the new structure,
|
5
|
+
# passing each name/value pair to the reviver function for possible
|
6
|
+
# transformation, starting with a temporary root object that holds the result
|
7
|
+
# in an empty key. If there is not a reviver function, we simply return the
|
8
|
+
# result.
|
9
|
+
def walk(data, &reviver)
|
10
|
+
data = parse(data) if String === data
|
11
|
+
return data unless block_given?
|
12
|
+
|
13
|
+
walker = lambda do |holder, key|
|
14
|
+
value = holder[key]
|
15
|
+
|
16
|
+
Siren.each(value) do |k, val|
|
17
|
+
v = walker.call(value, k)
|
18
|
+
if v.nil?
|
19
|
+
holder.delete(k)
|
20
|
+
else
|
21
|
+
value[k] = v
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
reviver.call(holder, key, value)
|
26
|
+
end
|
27
|
+
|
28
|
+
walker.call({"" => data}, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
{
|
2
|
+
"people": [
|
3
|
+
{
|
4
|
+
"type": "person",
|
5
|
+
"cars": [
|
6
|
+
{
|
7
|
+
"type": "car",
|
8
|
+
"brand": "ford"
|
9
|
+
}
|
10
|
+
]
|
11
|
+
},
|
12
|
+
|
13
|
+
{
|
14
|
+
"type": "person",
|
15
|
+
"cars": [
|
16
|
+
{
|
17
|
+
"type": "Car",
|
18
|
+
"brand": "ferrari"
|
19
|
+
},
|
20
|
+
|
21
|
+
{
|
22
|
+
"type": "car",
|
23
|
+
"brand": "bentley"
|
24
|
+
},
|
25
|
+
|
26
|
+
{
|
27
|
+
"type": "car",
|
28
|
+
"brand": "zonda"
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
]
|
33
|
+
}
|
34
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Person
|
2
|
+
extend Siren::Node
|
3
|
+
|
4
|
+
attr_reader :id, :cars, :favourite, :handle
|
5
|
+
|
6
|
+
def initialize(*cars)
|
7
|
+
@cars = cars.map { |brand| Car.new(brand) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
@cars.size == other.cars.size &&
|
12
|
+
@cars.all? { |car| other.cars.include?(car) }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": "mike",
|
4
|
+
"favourite": {"$ref": "mike"}
|
5
|
+
},
|
6
|
+
|
7
|
+
{
|
8
|
+
"type": "person",
|
9
|
+
"id": "bob",
|
10
|
+
"favourite": {"$ref": "bob"}
|
11
|
+
},
|
12
|
+
|
13
|
+
{
|
14
|
+
"type": "person",
|
15
|
+
"id": "romeo",
|
16
|
+
"handle": {"$ref": "@.id"},
|
17
|
+
"favourite": {"$ref": "juliet"}
|
18
|
+
},
|
19
|
+
|
20
|
+
{
|
21
|
+
"type": "person",
|
22
|
+
"id": "juliet",
|
23
|
+
"favourite": {"$ref": "romeo"}
|
24
|
+
}
|
25
|
+
]
|
26
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{ "store": {
|
2
|
+
"book": [
|
3
|
+
{ "category": "reference",
|
4
|
+
"author": "Nigel Rees",
|
5
|
+
"title": "Sayings of the Century",
|
6
|
+
"price": 8.95
|
7
|
+
},
|
8
|
+
{ "category": "fiction",
|
9
|
+
"author": "Evelyn Waugh",
|
10
|
+
"title": "Sword of Honour",
|
11
|
+
"price": 12.99
|
12
|
+
},
|
13
|
+
{ "category": "fiction",
|
14
|
+
"author": "Herman Melville",
|
15
|
+
"title": "Moby Dick",
|
16
|
+
"isbn": "0-553-21311-3",
|
17
|
+
"price": 8.99
|
18
|
+
},
|
19
|
+
{ "category": "fiction",
|
20
|
+
"author": "J. R. R. Tolkien",
|
21
|
+
"title": "The Lord of the Rings",
|
22
|
+
"isbn": "0-395-19395-8",
|
23
|
+
"price": 22.99
|
24
|
+
}
|
25
|
+
],
|
26
|
+
"bicycle": {
|
27
|
+
"color": "red",
|
28
|
+
"price": 19.95
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|