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.
@@ -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
+
@@ -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
+
@@ -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,11 @@
1
+ {
2
+ "people": [
3
+ {"name": "John", "age": 23},
4
+ {"name": "Paul", "age": 21},
5
+ {"name": "George", "age": 12},
6
+ {"name": "Ringo", "age": 17}
7
+ ],
8
+
9
+ "adults": {"$ref": "$.people[? @.age >= 18 ]"}
10
+ }
11
+
@@ -0,0 +1,14 @@
1
+ class Car
2
+ extend Siren::Node
3
+
4
+ attr_reader :brand
5
+
6
+ def initialize(brand = nil)
7
+ @brand = brand
8
+ end
9
+
10
+ def ==(other)
11
+ @brand == other.brand
12
+ end
13
+ end
14
+
@@ -0,0 +1,7 @@
1
+ [
2
+ {"first": "Thom", "last": "Yorke"},
3
+ {"first": "Jonny", "last": "Greenwood"},
4
+ {"first": "Ed", "last": "O'Brien"},
5
+ {"first": "Colin", "last": "Greenwood"},
6
+ {"first": "Phil", "last": "Selway"}
7
+ ]
@@ -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
+