schemata-router 0.0.1.beta1

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,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