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