schemata-staging 0.0.1.1

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,260 @@
1
+ require 'yajl'
2
+ require 'schemata/common/error'
3
+
4
+ module Schemata
5
+ module MessageBase
6
+
7
+ class ValidatingContainer
8
+ def initialize(data = {})
9
+ data ||= {}
10
+ @schema = self.class.const_get(:SCHEMA)
11
+ @contents = {}
12
+
13
+ data.each do |key, field_value|
14
+ field_schema = @schema.schemas[key]
15
+ next unless field_schema
16
+
17
+ begin
18
+ field_schema.validate(field_value)
19
+ rescue Membrane::SchemaValidationError => e
20
+ raise Schemata::UpdateAttributeError.new(e.message)
21
+ end
22
+
23
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
24
+ end
25
+ end
26
+
27
+ def self.define(schema)
28
+ vc_klass = Class.new(self)
29
+ vc_klass.const_set(:SCHEMA, schema)
30
+ schema.schemas.each do |key, field_schema|
31
+ vc_klass.send(:define_method, key) do
32
+ if @contents[key]
33
+ return Schemata::HashCopyHelpers.deep_copy(@contents[key])
34
+ end
35
+ nil
36
+ end
37
+
38
+ vc_klass.send(:define_method, "#{key}=") do |field_value|
39
+ begin
40
+ field_schema.validate(field_value)
41
+ rescue Membrane::SchemaValidationError => e
42
+ raise Schemata::UpdateAttributeError.new(e.message)
43
+ end
44
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
45
+ field_value
46
+ end
47
+ end
48
+ vc_klass
49
+ end
50
+
51
+ def contents
52
+ Schemata::HashCopyHelpers.deep_copy(@contents)
53
+ end
54
+
55
+ def empty?
56
+ @contents.empty?
57
+ end
58
+
59
+ def validate
60
+ @schema.validate(@contents)
61
+ end
62
+ end
63
+
64
+ def vc_klass
65
+ self.class.const_get(:VC_KLASS)
66
+ end
67
+
68
+ def aux_vc_klass
69
+ return self.class.const_get(:AUX_VC_KLASS) if self.class.aux_schema
70
+ end
71
+
72
+ def initialize(msg_data_hash = nil, aux_data_hash = nil)
73
+ @contents = vc_klass.new(msg_data_hash)
74
+ if self.class.aux_schema
75
+ @aux_contents = aux_vc_klass.new(aux_data_hash)
76
+ end
77
+ end
78
+
79
+ def encode
80
+ begin
81
+ validate_contents
82
+ validate_aux_data
83
+ rescue Membrane::SchemaValidationError => e
84
+ raise Schemata::EncodeError.new(e.message)
85
+ end
86
+
87
+ msg_type = message_type
88
+ curr_version = self.class.version
89
+ min_version = self.class::MIN_VERSION_ALLOWED
90
+
91
+ msg = { "V#{curr_version}" => contents }
92
+ curr_msg_obj = self
93
+ (min_version...curr_version).reverse_each do |v|
94
+ curr_msg_obj, old_fields =
95
+ curr_msg_obj.generate_old_fields
96
+ msg["V#{v}"] = old_fields
97
+ end
98
+ msg["min_version"] = min_version
99
+
100
+ if include_preschemata?
101
+ msg["V#{curr_version}"].each do |k, v|
102
+ msg[k] = v
103
+ end
104
+ end
105
+ Yajl::Encoder.encode(msg)
106
+ end
107
+
108
+ def include_preschemata?
109
+ self.class.const_get(:INCLUDE_PRESCHEMATA)
110
+ end
111
+
112
+ def validate_contents
113
+ @contents.validate
114
+ end
115
+
116
+ def validate_aux_data
117
+ @aux_contents.validate if self.class.aux_schema
118
+ end
119
+
120
+ def contents
121
+ @contents.contents
122
+ end
123
+
124
+ def aux_data
125
+ @aux_contents
126
+ end
127
+
128
+ def message_type
129
+ _, component, msg_type, version = self.class.name.split("::")
130
+ Schemata::const_get(component)::const_get(msg_type)
131
+ end
132
+
133
+ def self.included(klass)
134
+ klass.extend(Schemata::ClassMethods)
135
+ klass.extend(Dsl)
136
+ end
137
+ end
138
+
139
+ module ClassMethods
140
+ def mock
141
+ mock = {}
142
+ mock_values.keys.each do |k|
143
+ value = mock_values[k]
144
+ mock[k] = value.respond_to?("call") ? value.call : value
145
+ end
146
+ self.new(mock)
147
+ end
148
+
149
+ def schema
150
+ self::SCHEMA
151
+ end
152
+
153
+ def aux_schema
154
+ return self::AUX_SCHEMA if defined?(self::AUX_SCHEMA)
155
+ end
156
+
157
+ def mock_values
158
+ self::MOCK_VALUES
159
+ end
160
+
161
+ def version
162
+ _, component, msg_type, version = self.name.split("::")
163
+ version[1..-1].to_i
164
+ end
165
+
166
+ def previous_version
167
+ _, component, msg_type, version = self.name.split("::")
168
+ version = version[1..-1].to_i - 1
169
+ Schemata::const_get(component)::const_get(msg_type)::
170
+ const_get("V#{version}")
171
+ end
172
+ end
173
+
174
+ module Dsl
175
+ def define_schema(&blk)
176
+ schema = Membrane::SchemaParser.parse(&blk)
177
+ unless schema.kind_of? Membrane::Schema::Record
178
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
179
+ end
180
+ self::const_set(:SCHEMA, schema)
181
+ end
182
+
183
+ def define_aux_schema(&blk)
184
+ aux_schema = Membrane::SchemaParser.parse(&blk)
185
+ unless aux_schema.kind_of? Membrane::Schema::Record
186
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
187
+ end
188
+
189
+ self::const_set(:AUX_SCHEMA, aux_schema)
190
+ end
191
+
192
+ def define_min_version(min_version)
193
+ unless min_version.is_a? Integer
194
+ raise SchemaDefinitionError.new("Min version must be an integer")
195
+ end
196
+ const_set(:MIN_VERSION_ALLOWED, min_version)
197
+ end
198
+
199
+ def define_upvert(&blk)
200
+ eigenclass.send(:define_method, :upvert) do |old_data|
201
+ # No need to validate aux_data because upvert is only called during
202
+ # decode, when aux_data is irrelevant
203
+ begin
204
+ previous_version::SCHEMA.validate(old_data)
205
+ rescue Membrane::SchemaValidationError => e
206
+ raise Schemata::DecodeError.new(e.message)
207
+ end
208
+
209
+ blk.call(old_data)
210
+ end
211
+ end
212
+
213
+ def define_generate_old_fields(&blk)
214
+ self.send(:define_method, :generate_old_fields) do
215
+ if self.class.aux_schema && aux_data.empty?
216
+ raise Schemata::DecodeError.new("Necessary aux_data missing")
217
+ end
218
+ old_fields = blk.call(self)
219
+
220
+ msg_contents = contents
221
+ msg_contents.update(old_fields)
222
+ msg_obj = self.class.previous_version.new(msg_contents)
223
+
224
+ msg_obj.validate_contents
225
+ return msg_obj, old_fields
226
+ end
227
+ end
228
+
229
+ def define_mock_values(hash=nil, &blk)
230
+ if (hash && blk) || (!hash && !blk)
231
+ # value defined twice or not at all
232
+ raise SchemaDefinitionError.new("Mock values incorrectly defined")
233
+ end
234
+
235
+ hash = blk.call if blk
236
+
237
+ # Validate a sample of the mock values.
238
+ mock = {}
239
+ hash.each do |key, value|
240
+ mock[key] = value.respond_to?("call") ? value.call : value
241
+ end
242
+
243
+ begin
244
+ self.schema.validate(mock)
245
+ define_constant(:MOCK_VALUES, hash)
246
+ rescue Membrane::SchemaValidationError => e
247
+ raise SchemaDefinitionError.new("Sample mock values do not match schema: #{e}")
248
+ end
249
+ end
250
+
251
+ def define_constant(constant_name, constant_value)
252
+ self.const_set(constant_name, constant_value)
253
+ end
254
+
255
+ def include_preschemata
256
+ define_constant(:INCLUDE_PRESCHEMATA, true)
257
+ end
258
+
259
+ end
260
+ end
@@ -0,0 +1,122 @@
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
+ parsed = Schemata::ParsedMessage.new(json_msg)
23
+ rescue Schemata::DecodeError => e
24
+ raise e unless versions.size == 1
25
+ return decode_raw_payload(json_msg)
26
+ end
27
+ message_version = parsed.version
28
+
29
+ curr_version = current_version
30
+ curr_class = current_class
31
+
32
+ if curr_version < parsed.min_version
33
+ # TODO - Perhaps we should add
34
+ # || message_version < msg_type::Current::MIN_VERSION_ALLOWED
35
+ raise IncompatibleVersionError.new(
36
+ parsed.min_version,
37
+ curr_version)
38
+ end
39
+
40
+ msg_contents = parsed.contents["V#{message_version}"]
41
+ if curr_version <= message_version
42
+ (message_version - 1).downto(curr_version) do |v|
43
+ msg_contents.update(parsed.contents["V#{v}"])
44
+ end
45
+ else
46
+ (message_version + 1).upto(curr_version) do |v|
47
+ msg_contents = const_get("V#{v}")
48
+ .upvert(msg_contents)
49
+ end
50
+ end
51
+
52
+ begin
53
+ msg_obj = curr_class.new(msg_contents)
54
+ msg_obj.validate_contents
55
+ # We don't validate aux data in decode.
56
+ return msg_obj
57
+ rescue Schemata::UpdateAttributeError => e
58
+ raise Schemata::DecodeError.new(e.message)
59
+ rescue Membrane::SchemaValidationError => e
60
+ raise Schemata::DecodeError.new(e.message)
61
+ end
62
+ end
63
+
64
+ def self.extended(o)
65
+ o.extend Dsl
66
+ end
67
+
68
+ module Dsl
69
+ def version(v, &blk)
70
+ klass = Class.new
71
+ klass.instance_eval do
72
+ def eigenclass
73
+ class << self; self; end
74
+ end
75
+ end
76
+ klass.send(:include, Schemata::MessageBase)
77
+ klass.instance_eval(&blk)
78
+
79
+ if !defined? klass::INCLUDE_PRESCHEMATA
80
+ klass.const_set(:INCLUDE_PRESCHEMATA, false)
81
+ end
82
+
83
+ # Create the necessary ValidatingContainer subclasses (one for schema
84
+ # and, optionally, one for aux_schema
85
+ klass.instance_eval do
86
+ vc_klass = self::ValidatingContainer.define(self.schema)
87
+ self.const_set(:VC_KLASS, vc_klass)
88
+ if self.aux_schema
89
+ aux_vc_klass = self::ValidatingContainer.define(self.aux_schema)
90
+ self.const_set(:AUX_VC_KLASS, aux_vc_klass)
91
+ end
92
+ end
93
+
94
+ # Define attribute accessors for the message class
95
+ klass.schema.schemas.each do |key, field_schema|
96
+ klass.send(:define_method, key) do
97
+ @contents.send(key)
98
+ end
99
+ klass.send(:define_method, "#{key}=") do |field_value|
100
+ @contents.send("#{key}=", field_value)
101
+ end
102
+ end
103
+ self::const_set("V#{v}", klass)
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def decode_raw_payload(json)
110
+ begin
111
+ msg_contents = Yajl::Parser.parse(json)
112
+ msg_obj = current_class.new(msg_contents)
113
+ msg_obj.validate_contents
114
+ return msg_obj
115
+ rescue Schemata::UpdateAttributeError,
116
+ Membrane::SchemaValidationError => e
117
+ raise Schemata::DecodeError.new(e.message)
118
+ end
119
+ end
120
+
121
+ end
122
+ 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
@@ -0,0 +1,28 @@
1
+ module Schemata
2
+ module HashCopyHelpers
3
+ class CopyError < StandardError; end
4
+
5
+ def self.deep_copy(node)
6
+ case node
7
+ when String
8
+ return node.dup
9
+ when Numeric, TrueClass, FalseClass
10
+ return node
11
+ when Hash
12
+ copy = {}
13
+ # XXX NB: The 'to_s' below was included because some components use
14
+ # symbols as keys instead of strings. This fix is temporary; in the
15
+ # long term, we should change all components to use the same type for
16
+ # their keys
17
+ node.each { |k, v| copy[k.to_s] = deep_copy(v) }
18
+ return copy
19
+ when Array
20
+ return node.map { |v| deep_copy(v) }
21
+ when NilClass
22
+ return nil
23
+ else
24
+ raise CopyError.new("Unexpected class: #{node.class}")
25
+ end
26
+ end
27
+ end
28
+ end