schemata-staging 0.0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/schemata/common/error.rb +12 -0
- data/lib/schemata/common/msgbase.rb +260 -0
- data/lib/schemata/common/msgtypebase.rb +122 -0
- data/lib/schemata/common/parsed_msg.rb +43 -0
- data/lib/schemata/helpers/hash_copy.rb +28 -0
- data/lib/schemata/staging.rb +9 -0
- data/lib/schemata/staging/message.rb +13 -0
- data/lib/schemata/staging/message/message_v1.rb +148 -0
- data/lib/schemata/staging/version.rb +5 -0
- data/spec/cloud_controller/cc_bar_spec.rb +140 -0
- data/spec/common/helpers_spec.rb +89 -0
- data/spec/common/parsed_msg_spec.rb +46 -0
- data/spec/component/aux_data_spec.rb +37 -0
- data/spec/component/component_bar_spec.rb +140 -0
- data/spec/component/component_foo_spec.rb +630 -0
- data/spec/component/foo_spec.rb +214 -0
- data/spec/staging/staging_message_spec.rb +186 -0
- data/spec/support/helpers.rb +27 -0
- metadata +153 -0
@@ -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
|