schemata-router 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ module Schemata
2
+ class DecodeError < StandardError; end
3
+ class EncodeError < StandardError; end
4
+ class UpdateAttributeError < StandardError; end
5
+ class IncompatibleVersionError < DecodeError
6
+ def initialize(msg_version, component_version)
7
+ super("min message version #{msg_version} too high for component ver\
8
+ sion #{component_version}")
9
+ end
10
+ end
11
+ class SchemaDefinitionError < StandardError; end
12
+ end
@@ -0,0 +1,281 @@
1
+ require 'yajl'
2
+ require 'schemata/common/error'
3
+ require 'schemata/helpers/stringify'
4
+ require 'membrane'
5
+
6
+ module Schemata
7
+ module MessageBase
8
+
9
+ class ValidatingContainer
10
+ def initialize(data = {})
11
+ data ||= {}
12
+ @schema = self.class.const_get(:SCHEMA)
13
+ @contents = {}
14
+
15
+ data.each do |key, field_value|
16
+ field_schema = @schema.schemas[key]
17
+ next unless field_schema
18
+
19
+ # TODO This call to stringify should be removed when cc/dea stop using
20
+ # Symbols.
21
+ #
22
+ # Currently, some fields (for example, 'states' in requests sent
23
+ # on dea.find.droplet), are are symbols, During Yajl decoding, however,
24
+ # they become strings. Thus, on the encoding side, Schemata should expect
25
+ # symbols, but on the decoding side, it should expect strings. To allow
26
+ # for this in the schema definition, Schemata stringifies all symbols during
27
+ # construction of Schemata objects.
28
+ field_value = Schemata::HashCopyHelpers.stringify(field_value)
29
+
30
+ begin
31
+ field_schema.validate(field_value)
32
+ rescue ::Membrane::SchemaValidationError => e
33
+ raise Schemata::UpdateAttributeError.new(e.message)
34
+ end
35
+
36
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
37
+ end
38
+ end
39
+
40
+ def self.define(schema)
41
+ vc_klass = Class.new(self)
42
+ vc_klass.const_set(:SCHEMA, schema)
43
+ schema.schemas.each do |key, field_schema|
44
+ vc_klass.send(:define_method, key) do
45
+ unless @contents[key].nil?
46
+ return Schemata::HashCopyHelpers.deep_copy(@contents[key])
47
+ end
48
+ nil
49
+ end
50
+
51
+ # TODO This call to stringify should be removed when cc/dea stops using
52
+ # symbols. See comment above for a better description.
53
+ vc_klass.send(:define_method, "#{key}=") do |field_value|
54
+ field_value = Schemata::HashCopyHelpers.stringify(field_value)
55
+ begin
56
+ field_schema.validate(field_value)
57
+ rescue ::Membrane::SchemaValidationError => e
58
+ raise Schemata::UpdateAttributeError.new(e.message)
59
+ end
60
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
61
+ field_value
62
+ end
63
+ end
64
+ vc_klass
65
+ end
66
+
67
+ def contents
68
+ Schemata::HashCopyHelpers.deep_copy(@contents)
69
+ end
70
+
71
+ def empty?
72
+ @contents.empty?
73
+ end
74
+
75
+ def validate
76
+ @schema.validate(@contents)
77
+ end
78
+ end
79
+
80
+ def vc_klass
81
+ self.class.const_get(:VC_KLASS)
82
+ end
83
+
84
+ def aux_vc_klass
85
+ return self.class.const_get(:AUX_VC_KLASS) if self.class.aux_schema
86
+ end
87
+
88
+ def initialize(msg_data_hash = nil, aux_data_hash = nil)
89
+ @contents = vc_klass.new(msg_data_hash)
90
+ if self.class.aux_schema
91
+ @aux_contents = aux_vc_klass.new(aux_data_hash)
92
+ end
93
+ end
94
+
95
+ def encode
96
+ begin
97
+ validate_contents
98
+ validate_aux_data
99
+ rescue ::Membrane::SchemaValidationError => e
100
+ raise Schemata::EncodeError.new(e.message)
101
+ end
102
+
103
+ msg_type = message_type
104
+ curr_version = self.class.version
105
+ min_version = self.class::MIN_VERSION_ALLOWED
106
+
107
+ msg = { "V#{curr_version}" => contents }
108
+ curr_msg_obj = self
109
+ (min_version...curr_version).reverse_each do |v|
110
+ curr_msg_obj, old_fields =
111
+ curr_msg_obj.generate_old_fields
112
+ msg["V#{v}"] = old_fields
113
+ end
114
+ msg["min_version"] = min_version
115
+
116
+ if include_preschemata?
117
+ msg["V#{curr_version}"].each do |k, v|
118
+ msg[k] = v
119
+ end
120
+ end
121
+ Yajl::Encoder.encode(msg)
122
+ end
123
+
124
+ def include_preschemata?
125
+ self.class.const_get(:INCLUDE_PRESCHEMATA)
126
+ end
127
+
128
+ def validate_contents
129
+ @contents.validate
130
+ end
131
+
132
+ def validate_aux_data
133
+ @aux_contents.validate if self.class.aux_schema
134
+ end
135
+
136
+ def contents
137
+ @contents.contents
138
+ end
139
+
140
+ def aux_data
141
+ @aux_contents
142
+ end
143
+
144
+ def message_type
145
+ _, component, msg_type, version = self.class.name.split("::")
146
+ Schemata::const_get(component)::const_get(msg_type)
147
+ end
148
+
149
+ def component
150
+ _, component, msg_type, version = self.class.name.split("::")
151
+ Schemata::const_get(component)
152
+ end
153
+
154
+ def self.included(klass)
155
+ klass.extend(Schemata::ClassMethods)
156
+ klass.extend(Dsl)
157
+ end
158
+ end
159
+
160
+ module ClassMethods
161
+ def mock
162
+ mock = {}
163
+ mock_values.keys.each do |k|
164
+ value = mock_values[k]
165
+ mock[k] = value.respond_to?("call") ? value.call : value
166
+ end
167
+ self.new(mock)
168
+ end
169
+
170
+ def schema
171
+ self::SCHEMA
172
+ end
173
+
174
+ def aux_schema
175
+ return self::AUX_SCHEMA if defined?(self::AUX_SCHEMA)
176
+ end
177
+
178
+ def mock_values
179
+ self::MOCK_VALUES
180
+ end
181
+
182
+ def version
183
+ _, component, msg_type, version = self.name.split("::")
184
+ version[1..-1].to_i
185
+ end
186
+
187
+ def previous_version
188
+ _, component, msg_type, version = self.name.split("::")
189
+ version = version[1..-1].to_i - 1
190
+ Schemata::const_get(component)::const_get(msg_type)::
191
+ const_get("V#{version}")
192
+ end
193
+ end
194
+
195
+ module Dsl
196
+ def define_schema(&blk)
197
+ schema = ::Membrane::SchemaParser.parse(&blk)
198
+ unless schema.kind_of? ::Membrane::Schema::Record
199
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
200
+ end
201
+ self::const_set(:SCHEMA, schema)
202
+ end
203
+
204
+ def define_aux_schema(&blk)
205
+ aux_schema = ::Membrane::SchemaParser.parse(&blk)
206
+ unless aux_schema.kind_of? ::Membrane::Schema::Record
207
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
208
+ end
209
+
210
+ self::const_set(:AUX_SCHEMA, aux_schema)
211
+ end
212
+
213
+ def define_min_version(min_version)
214
+ unless min_version.is_a? Integer
215
+ raise SchemaDefinitionError.new("Min version must be an integer")
216
+ end
217
+ const_set(:MIN_VERSION_ALLOWED, min_version)
218
+ end
219
+
220
+ def define_upvert(&blk)
221
+ eigenclass.send(:define_method, :upvert) do |old_data|
222
+ # No need to validate aux_data because upvert is only called during
223
+ # decode, when aux_data is irrelevant
224
+ begin
225
+ previous_version::SCHEMA.validate(old_data)
226
+ rescue ::Membrane::SchemaValidationError => e
227
+ raise Schemata::DecodeError.new(e.message)
228
+ end
229
+
230
+ blk.call(old_data)
231
+ end
232
+ end
233
+
234
+ def define_generate_old_fields(&blk)
235
+ self.send(:define_method, :generate_old_fields) do
236
+ if self.class.aux_schema && aux_data.empty?
237
+ raise Schemata::DecodeError.new("Necessary aux_data missing")
238
+ end
239
+ old_fields = blk.call(self)
240
+
241
+ msg_contents = contents
242
+ msg_contents.update(old_fields)
243
+ msg_obj = self.class.previous_version.new(msg_contents)
244
+
245
+ msg_obj.validate_contents
246
+ return msg_obj, old_fields
247
+ end
248
+ end
249
+
250
+ def define_mock_values(hash=nil, &blk)
251
+ if (hash && blk) || (!hash && !blk)
252
+ # value defined twice or not at all
253
+ raise SchemaDefinitionError.new("Mock values incorrectly defined")
254
+ end
255
+
256
+ hash = blk.call if blk
257
+
258
+ # Validate a sample of the mock values.
259
+ mock = {}
260
+ hash.each do |key, value|
261
+ mock[key] = value.respond_to?("call") ? value.call : value
262
+ end
263
+
264
+ begin
265
+ self.schema.validate(mock)
266
+ define_constant(:MOCK_VALUES, hash)
267
+ rescue ::Membrane::SchemaValidationError => e
268
+ raise SchemaDefinitionError.new("Sample mock values do not match schema: #{e}")
269
+ end
270
+ end
271
+
272
+ def define_constant(constant_name, constant_value)
273
+ self.const_set(constant_name, constant_value)
274
+ end
275
+
276
+ def include_preschemata
277
+ define_constant(:INCLUDE_PRESCHEMATA, true)
278
+ end
279
+
280
+ end
281
+ end
@@ -0,0 +1,144 @@
1
+ require 'schemata/common/msgbase'
2
+ require 'schemata/common/parsed_msg'
3
+
4
+ module Schemata
5
+ module MessageTypeBase
6
+ def current_version
7
+ return @current_version if @current_version
8
+ @current_version = versions.max
9
+ end
10
+
11
+ def versions
12
+ str_versions = self.constants.select { |x| x =~ /^V[0-9]+$/ }
13
+ str_versions.map { |x| x[1..-1].to_i}
14
+ end
15
+
16
+ def current_class
17
+ self::const_get("V#{current_version}")
18
+ end
19
+
20
+ def decode(json_msg)
21
+ begin
22
+ if versions.size == 2
23
+ parsed = Schemata::ParsedMessage.new(cleanup(json_msg))
24
+ else
25
+ parsed = Schemata::ParsedMessage.new(json_msg)
26
+ end
27
+ rescue Schemata::DecodeError => e
28
+ return decode_raw_payload(json_msg) if versions.size == 1
29
+ raise e
30
+ end
31
+ message_version = parsed.version
32
+
33
+ curr_version = current_version
34
+ curr_class = current_class
35
+
36
+ if curr_version < parsed.min_version
37
+ # TODO - Perhaps we should add
38
+ # || message_version < msg_type::Current::MIN_VERSION_ALLOWED
39
+ raise IncompatibleVersionError.new(
40
+ parsed.min_version,
41
+ curr_version)
42
+ end
43
+
44
+ msg_contents = parsed.contents["V#{message_version}"]
45
+ if curr_version <= message_version
46
+ (message_version - 1).downto(curr_version) do |v|
47
+ msg_contents.update(parsed.contents["V#{v}"])
48
+ end
49
+ else
50
+ (message_version + 1).upto(curr_version) do |v|
51
+ msg_contents = const_get("V#{v}")
52
+ .upvert(msg_contents)
53
+ end
54
+ end
55
+
56
+ begin
57
+ msg_obj = curr_class.new(msg_contents)
58
+ msg_obj.validate_contents
59
+ # We don't validate aux data in decode.
60
+ return msg_obj
61
+ rescue Schemata::UpdateAttributeError => e
62
+ raise Schemata::DecodeError.new(e.message)
63
+ rescue Membrane::SchemaValidationError => e
64
+ raise Schemata::DecodeError.new(e.message)
65
+ end
66
+ end
67
+
68
+ def component
69
+ _, component, message_type = self.name.split("::")
70
+ Schemata::const_get(component)
71
+ end
72
+
73
+ def self.extended(o)
74
+ o.extend Dsl
75
+ end
76
+
77
+ module Dsl
78
+ def version(v, &blk)
79
+ klass = Class.new
80
+ klass.instance_eval do
81
+ def eigenclass
82
+ class << self; self; end
83
+ end
84
+ end
85
+ klass.send(:include, Schemata::MessageBase)
86
+ klass.instance_eval(&blk)
87
+
88
+ if !defined? klass::INCLUDE_PRESCHEMATA
89
+ klass.const_set(:INCLUDE_PRESCHEMATA, false)
90
+ end
91
+
92
+ # Create the necessary ValidatingContainer subclasses (one for schema
93
+ # and, optionally, one for aux_schema
94
+ klass.instance_eval do
95
+ vc_klass = self::ValidatingContainer.define(self.schema)
96
+ self.const_set(:VC_KLASS, vc_klass)
97
+ if self.aux_schema
98
+ aux_vc_klass = self::ValidatingContainer.define(self.aux_schema)
99
+ self.const_set(:AUX_VC_KLASS, aux_vc_klass)
100
+ end
101
+ end
102
+
103
+ # Define attribute accessors for the message class
104
+ klass.schema.schemas.each do |key, field_schema|
105
+ klass.send(:define_method, key) do
106
+ @contents.send(key)
107
+ end
108
+ klass.send(:define_method, "#{key}=") do |field_value|
109
+ @contents.send("#{key}=", field_value)
110
+ end
111
+ end
112
+ self::const_set("V#{v}", klass)
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def decode_raw_payload(json)
119
+ begin
120
+ msg_contents = Yajl::Parser.parse(json)
121
+ msg_obj = current_class.new(msg_contents)
122
+ msg_obj.validate_contents
123
+ return msg_obj
124
+ rescue Schemata::UpdateAttributeError,
125
+ Membrane::SchemaValidationError => e
126
+ raise Schemata::DecodeError.new(e.message)
127
+ end
128
+ end
129
+
130
+ def cleanup(json)
131
+ msg_contents = Yajl::Parser.parse(json)
132
+ clean_msg = {}
133
+
134
+ msg_contents.keys.each do |key|
135
+ if key == "min_version" || key =~ /^V[0-9]+$/
136
+ clean_msg[key] = msg_contents[key]
137
+ end
138
+ end
139
+
140
+ Yajl::Encoder.encode(clean_msg)
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,43 @@
1
+ require 'schemata/helpers/hash_copy'
2
+ require 'set'
3
+ require 'yajl'
4
+
5
+ module Schemata
6
+ class ParsedMessage
7
+
8
+ attr_reader :version, :min_version
9
+
10
+ def initialize(json)
11
+ @contents = Yajl::Parser.parse(json)
12
+
13
+ @min_version = @contents['min_version']
14
+ if !@min_version
15
+ raise DecodeError.new("Field 'min_version' abset from message")
16
+ end
17
+
18
+ versions = []
19
+ @contents.keys.each do |k|
20
+ next if k == 'min_version'
21
+ unless k =~ /^V[0-9]+$/
22
+ raise DecodeError.new("Invalid key: #{k}")
23
+ end
24
+ versions << k[1..-1].to_i
25
+ end
26
+
27
+ if versions.empty?
28
+ raise DecodeError.new("Message contains no versioned hashes")
29
+ end
30
+
31
+ if Set.new(versions.min..versions.max) != Set.new(versions)
32
+ raise DecodeError.new("There are versions missing between\
33
+ #{versions.min} and #{versions.max}")
34
+ end
35
+
36
+ @version = versions.max
37
+ end
38
+
39
+ def contents
40
+ Schemata::HashCopyHelpers.deep_copy(@contents)
41
+ end
42
+ end
43
+ end