siren 0.2.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.
- 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
|
+
|