schemata-router 0.0.1.beta1
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 +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
|