schemata-staging 0.0.1.1

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