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.
- data/lib/schemata/common/error.rb +12 -0
- data/lib/schemata/common/msgbase.rb +281 -0
- data/lib/schemata/common/msgtypebase.rb +144 -0
- data/lib/schemata/common/parsed_msg.rb +43 -0
- data/lib/schemata/helpers/hash_copy.rb +28 -0
- data/lib/schemata/helpers/stringify.rb +26 -0
- data/lib/schemata/router.rb +14 -0
- data/lib/schemata/router/register_request.rb +13 -0
- data/lib/schemata/router/register_request/register_request_v1.rb +53 -0
- data/lib/schemata/router/start_message.rb +13 -0
- data/lib/schemata/router/start_message/start_message_v1.rb +35 -0
- data/lib/schemata/router/version.rb +5 -0
- data/spec/common/helpers_spec.rb +115 -0
- data/spec/common/parsed_msg_spec.rb +46 -0
- data/spec/router/register_request_spec.rb +10 -0
- data/spec/router/router_spec.rb +6 -0
- data/spec/router/start_message_spec.rb +10 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/component_helpers.rb +51 -0
- data/spec/support/helpers.rb +54 -0
- data/spec/support/message_helpers.rb +138 -0
- data/spec/support/message_type_helpers.rb +171 -0
- metadata +158 -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,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
|