schemata-cloud_controller 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/lib/schemata/cloud_controller/droplet_updated_message/droplet_updated_message_v1.rb +35 -0
  2. data/lib/schemata/cloud_controller/droplet_updated_message.rb +13 -0
  3. data/lib/schemata/cloud_controller/hm_start_request/hm_start_request_v1.rb +41 -0
  4. data/lib/schemata/cloud_controller/hm_start_request.rb +13 -0
  5. data/lib/schemata/cloud_controller/hm_stop_request/hm_stop_request_v1.rb +39 -0
  6. data/lib/schemata/cloud_controller/hm_stop_request.rb +13 -0
  7. data/lib/schemata/cloud_controller/version.rb +5 -0
  8. data/lib/schemata/cloud_controller.rb +19 -0
  9. data/lib/schemata/common/error.rb +18 -0
  10. data/lib/schemata/common/msgbase.rb +282 -0
  11. data/lib/schemata/common/msgtypebase.rb +144 -0
  12. data/lib/schemata/common/parsed_msg.rb +43 -0
  13. data/lib/schemata/helpers/hash_copy.rb +28 -0
  14. data/lib/schemata/helpers/stringify.rb +26 -0
  15. data/spec/cloud_controller/cloud_controller_spec.rb +6 -0
  16. data/spec/cloud_controller/droplet_updated_message_spec.rb +10 -0
  17. data/spec/cloud_controller/hm_start_request_spec.rb +11 -0
  18. data/spec/cloud_controller/hm_stop_request_spec.rb +12 -0
  19. data/spec/common/helpers_spec.rb +115 -0
  20. data/spec/common/parsed_msg_spec.rb +46 -0
  21. data/spec/component/aux_data_spec.rb +37 -0
  22. data/spec/component/component_bar_spec.rb +140 -0
  23. data/spec/component/component_foo_spec.rb +630 -0
  24. data/spec/component/foo_spec.rb +214 -0
  25. data/spec/component2/component2_bar_spec.rb +140 -0
  26. data/spec/dea/advertise_message_spec.rb +10 -0
  27. data/spec/dea/dea_spec.rb +6 -0
  28. data/spec/dea/dea_status_response_spec.rb +10 -0
  29. data/spec/dea/discover_request_spec.rb +10 -0
  30. data/spec/dea/droplet_status_response_spec.rb +10 -0
  31. data/spec/dea/exit_message_spec.rb +10 -0
  32. data/spec/dea/find_droplet_request_spec.rb +27 -0
  33. data/spec/dea/find_droplet_response_spec.rb +10 -0
  34. data/spec/dea/heartbeat_response_spec.rb +10 -0
  35. data/spec/dea/hello_message_spec.rb +10 -0
  36. data/spec/dea/start_request_spec.rb +10 -0
  37. data/spec/dea/stop_request_spec.rb +10 -0
  38. data/spec/dea/update_request_spec.rb +0 -0
  39. data/spec/health_manager/health_manager_spec.rb +6 -0
  40. data/spec/health_manager/health_request_spec.rb +12 -0
  41. data/spec/health_manager/health_response_spec.rb +12 -0
  42. data/spec/health_manager/status_crashed_response_spec.rb +12 -0
  43. data/spec/health_manager/status_flapping_response_spec.rb +12 -0
  44. data/spec/health_manager/status_request_spec.rb +12 -0
  45. data/spec/router/register_request_spec.rb +10 -0
  46. data/spec/router/router_spec.rb +6 -0
  47. data/spec/router/start_message_spec.rb +10 -0
  48. data/spec/spec_helper.rb +1 -0
  49. data/spec/staging/staging_message_spec.rb +14 -0
  50. data/spec/staging/staging_spec.rb +6 -0
  51. data/spec/support/component_helpers.rb +51 -0
  52. data/spec/support/helpers.rb +102 -0
  53. data/spec/support/message_helpers.rb +135 -0
  54. data/spec/support/message_type_helpers.rb +171 -0
  55. metadata +220 -0
@@ -0,0 +1,35 @@
1
+ require 'vcap/common'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module DropletUpdatedMessage
6
+ version 1 do
7
+ include_preschemata
8
+
9
+ define_schema do
10
+ {
11
+ "droplet" => String,
12
+ optional("cc_partition") => String,
13
+ }
14
+ end
15
+
16
+ define_min_version 1
17
+
18
+ define_upvert do |old_data|
19
+ raise NotImplementedError.new
20
+ end
21
+
22
+ define_generate_old_fields do |msg_obj|
23
+ raise NotImplementedError.new
24
+ end
25
+
26
+ define_mock_values do
27
+ {
28
+ "droplet" => proc { VCAP.secure_uuid },
29
+ "cc_partition" => "default",
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ require 'schemata/common/msgtypebase'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module DropletUpdatedMessage
6
+ extend Schemata::MessageTypeBase
7
+ end
8
+ end
9
+ end
10
+
11
+ Dir[File.dirname(__FILE__) + '/droplet_updated_message/*.rb'].each do |file|
12
+ require file
13
+ end
@@ -0,0 +1,41 @@
1
+ require 'vcap/common'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module HmStartRequest
6
+ version 1 do
7
+ include_preschemata
8
+
9
+ define_schema do
10
+ {
11
+ "droplet" => String,
12
+ "op" => "START",
13
+ "last_updated" => Integer,
14
+ "version" => String,
15
+ "indices" => [Integer],
16
+ }
17
+ end
18
+
19
+ define_min_version 1
20
+
21
+ define_upvert do |old_data|
22
+ raise NotImplementedError.new
23
+ end
24
+
25
+ define_generate_old_fields do |msg_obj|
26
+ raise NotImplementedError.new
27
+ end
28
+
29
+ define_mock_values do
30
+ {
31
+ "droplet" => proc { VCAP.secure_uuid },
32
+ "op" => "START",
33
+ "last_updated" => proc { Time.now.to_i },
34
+ "version" => proc { VCAP.secure_uuid },
35
+ "indices" => [0, 1]
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ require 'schemata/common/msgtypebase'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module HmStartRequest
6
+ extend Schemata::MessageTypeBase
7
+ end
8
+ end
9
+ end
10
+
11
+ Dir[File.dirname(__FILE__) + '/hm_start_request/*.rb'].each do |file|
12
+ require file
13
+ end
@@ -0,0 +1,39 @@
1
+ require 'vcap/common'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module HmStopRequest
6
+ version 1 do
7
+ include_preschemata
8
+
9
+ define_schema do
10
+ {
11
+ "droplet" => String,
12
+ "op" => "STOP",
13
+ "last_updated" => Integer,
14
+ "instances" => [String]
15
+ }
16
+ end
17
+
18
+ define_min_version 1
19
+
20
+ define_upvert do |old_data|
21
+ raise NotImplementedError.new
22
+ end
23
+
24
+ define_generate_old_fields do |msg_obj|
25
+ raise NotImplementedError.new
26
+ end
27
+
28
+ define_mock_values do
29
+ {
30
+ "droplet" => proc { VCAP.secure_uuid },
31
+ "op" => "STOP",
32
+ "last_updated" => proc { Time.now.to_i},
33
+ "instances" => proc {[ VCAP.secure_uuid ]}
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ require 'schemata/common/msgtypebase'
2
+
3
+ module Schemata
4
+ module CloudController
5
+ module HmStopRequest
6
+ extend Schemata::MessageTypeBase
7
+ end
8
+ end
9
+ end
10
+
11
+ Dir[File.dirname(__FILE__) + '/hm_stop_request/*.rb'].each do |file|
12
+ require file
13
+ end
@@ -0,0 +1,5 @@
1
+ module Schemata
2
+ module CloudController
3
+ VERSION = "0.0.1.beta1"
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require 'schemata/cloud_controller/droplet_updated_message'
2
+ require 'schemata/cloud_controller/hm_start_request'
3
+ require 'schemata/cloud_controller/hm_stop_request'
4
+
5
+ module Schemata
6
+ module CloudController
7
+ def self.mock_droplet_updated_message(version=DropletUpdatedMessage.current_version)
8
+ DropletUpdatedMessage::const_get("V#{version}").mock
9
+ end
10
+
11
+ def self.mock_hm_start_request(version=HmStartRequest.current_version)
12
+ HmStartRequest::const_get("V#{version}").mock
13
+ end
14
+
15
+ def self.mock_hm_stop_request(version=HmStopRequest.current_version)
16
+ HmStopRequest::const_get("V#{version}").mock
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ module Schemata
2
+ class DecodeError < StandardError; end
3
+ class EncodeError < StandardError; end
4
+ class SchemaDefinitionError < StandardError; end
5
+
6
+ class UpdateAttributeError < StandardError
7
+ def initialize(key, message)
8
+ super("#{key}: #{message}")
9
+ end
10
+ end
11
+
12
+ class IncompatibleVersionError < DecodeError
13
+ def initialize(msg_version, component_version)
14
+ super("min message version #{msg_version} too high for component ver\
15
+ sion #{component_version}")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,282 @@
1
+ require 'yajl'
2
+ require 'membrane'
3
+ require 'schemata/common/error'
4
+ require 'schemata/helpers/stringify'
5
+ require 'membrane'
6
+
7
+ module Schemata
8
+ module MessageBase
9
+
10
+ class ValidatingContainer
11
+ def initialize(data = {})
12
+ data ||= {}
13
+ @schema = self.class.const_get(:SCHEMA)
14
+ @contents = {}
15
+
16
+ data.each do |key, field_value|
17
+ field_schema = @schema.schemas[key]
18
+ next unless field_schema
19
+
20
+ # TODO This call to stringify should be removed when cc/dea stop using
21
+ # Symbols.
22
+ #
23
+ # Currently, some fields (for example, 'states' in requests sent
24
+ # on dea.find.droplet), are are symbols, During Yajl decoding, however,
25
+ # they become strings. Thus, on the encoding side, Schemata should expect
26
+ # symbols, but on the decoding side, it should expect strings. To allow
27
+ # for this in the schema definition, Schemata stringifies all symbols during
28
+ # construction of Schemata objects.
29
+ field_value = Schemata::HashCopyHelpers.stringify(field_value)
30
+
31
+ begin
32
+ field_schema.validate(field_value)
33
+ rescue Membrane::SchemaValidationError => e
34
+ raise Schemata::UpdateAttributeError.new(key, e.message)
35
+ end
36
+
37
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
38
+ end
39
+ end
40
+
41
+ def self.define(schema)
42
+ vc_klass = Class.new(self)
43
+ vc_klass.const_set(:SCHEMA, schema)
44
+ schema.schemas.each do |key, field_schema|
45
+ vc_klass.send(:define_method, key) do
46
+ unless @contents[key].nil?
47
+ return Schemata::HashCopyHelpers.deep_copy(@contents[key])
48
+ end
49
+ nil
50
+ end
51
+
52
+ # TODO This call to stringify should be removed when cc/dea stops using
53
+ # symbols. See comment above for a better description.
54
+ vc_klass.send(:define_method, "#{key}=") do |field_value|
55
+ field_value = Schemata::HashCopyHelpers.stringify(field_value)
56
+ begin
57
+ field_schema.validate(field_value)
58
+ rescue Membrane::SchemaValidationError => e
59
+ raise Schemata::UpdateAttributeError.new(key, e.message)
60
+ end
61
+ @contents[key] = Schemata::HashCopyHelpers.deep_copy(field_value)
62
+ field_value
63
+ end
64
+ end
65
+ vc_klass
66
+ end
67
+
68
+ def contents
69
+ Schemata::HashCopyHelpers.deep_copy(@contents)
70
+ end
71
+
72
+ def empty?
73
+ @contents.empty?
74
+ end
75
+
76
+ def validate
77
+ @schema.validate(@contents)
78
+ end
79
+ end
80
+
81
+ def vc_klass
82
+ self.class.const_get(:VC_KLASS)
83
+ end
84
+
85
+ def aux_vc_klass
86
+ return self.class.const_get(:AUX_VC_KLASS) if self.class.aux_schema
87
+ end
88
+
89
+ def initialize(msg_data_hash = nil, aux_data_hash = nil)
90
+ @contents = vc_klass.new(msg_data_hash)
91
+ if self.class.aux_schema
92
+ @aux_contents = aux_vc_klass.new(aux_data_hash)
93
+ end
94
+ end
95
+
96
+ def encode
97
+ begin
98
+ validate_contents
99
+ validate_aux_data
100
+ rescue Membrane::SchemaValidationError => e
101
+ raise Schemata::EncodeError.new(e.message)
102
+ end
103
+
104
+ msg_type = message_type
105
+ curr_version = self.class.version
106
+ min_version = self.class::MIN_VERSION_ALLOWED
107
+
108
+ msg = { "V#{curr_version}" => contents }
109
+ curr_msg_obj = self
110
+ (min_version...curr_version).reverse_each do |v|
111
+ curr_msg_obj, old_fields =
112
+ curr_msg_obj.generate_old_fields
113
+ msg["V#{v}"] = old_fields
114
+ end
115
+ msg["min_version"] = min_version
116
+
117
+ if include_preschemata?
118
+ msg["V#{curr_version}"].each do |k, v|
119
+ msg[k] = v
120
+ end
121
+ end
122
+ Yajl::Encoder.encode(msg)
123
+ end
124
+
125
+ def include_preschemata?
126
+ self.class.const_get(:INCLUDE_PRESCHEMATA)
127
+ end
128
+
129
+ def validate_contents
130
+ @contents.validate
131
+ end
132
+
133
+ def validate_aux_data
134
+ @aux_contents.validate if self.class.aux_schema
135
+ end
136
+
137
+ def contents
138
+ @contents.contents
139
+ end
140
+
141
+ def aux_data
142
+ @aux_contents
143
+ end
144
+
145
+ def message_type
146
+ _, component, msg_type, version = self.class.name.split("::")
147
+ Schemata::const_get(component)::const_get(msg_type)
148
+ end
149
+
150
+ def component
151
+ _, component, msg_type, version = self.class.name.split("::")
152
+ Schemata::const_get(component)
153
+ end
154
+
155
+ def self.included(klass)
156
+ klass.extend(Schemata::ClassMethods)
157
+ klass.extend(Dsl)
158
+ end
159
+ end
160
+
161
+ module ClassMethods
162
+ def mock
163
+ mock = {}
164
+ mock_values.keys.each do |k|
165
+ value = mock_values[k]
166
+ mock[k] = value.respond_to?("call") ? value.call : value
167
+ end
168
+ self.new(mock)
169
+ end
170
+
171
+ def schema
172
+ self::SCHEMA
173
+ end
174
+
175
+ def aux_schema
176
+ return self::AUX_SCHEMA if defined?(self::AUX_SCHEMA)
177
+ end
178
+
179
+ def mock_values
180
+ self::MOCK_VALUES
181
+ end
182
+
183
+ def version
184
+ _, component, msg_type, version = self.name.split("::")
185
+ version[1..-1].to_i
186
+ end
187
+
188
+ def previous_version
189
+ _, component, msg_type, version = self.name.split("::")
190
+ version = version[1..-1].to_i - 1
191
+ Schemata::const_get(component)::const_get(msg_type)::
192
+ const_get("V#{version}")
193
+ end
194
+ end
195
+
196
+ module Dsl
197
+ def define_schema(&blk)
198
+ schema = Membrane::SchemaParser.parse(&blk)
199
+ unless schema.kind_of? Membrane::Schema::Record
200
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
201
+ end
202
+ self::const_set(:SCHEMA, schema)
203
+ end
204
+
205
+ def define_aux_schema(&blk)
206
+ aux_schema = Membrane::SchemaParser.parse(&blk)
207
+ unless aux_schema.kind_of? Membrane::Schema::Record
208
+ Schemata::SchemaDefinitionError.new("Schema must be a hash")
209
+ end
210
+
211
+ self::const_set(:AUX_SCHEMA, aux_schema)
212
+ end
213
+
214
+ def define_min_version(min_version)
215
+ unless min_version.is_a? Integer
216
+ raise SchemaDefinitionError.new("Min version must be an integer")
217
+ end
218
+ const_set(:MIN_VERSION_ALLOWED, min_version)
219
+ end
220
+
221
+ def define_upvert(&blk)
222
+ eigenclass.send(:define_method, :upvert) do |old_data|
223
+ # No need to validate aux_data because upvert is only called during
224
+ # decode, when aux_data is irrelevant
225
+ begin
226
+ previous_version::SCHEMA.validate(old_data)
227
+ rescue Membrane::SchemaValidationError => e
228
+ raise Schemata::DecodeError.new(e.message)
229
+ end
230
+
231
+ blk.call(old_data)
232
+ end
233
+ end
234
+
235
+ def define_generate_old_fields(&blk)
236
+ self.send(:define_method, :generate_old_fields) do
237
+ if self.class.aux_schema && aux_data.empty?
238
+ raise Schemata::DecodeError.new("Necessary aux_data missing")
239
+ end
240
+ old_fields = blk.call(self)
241
+
242
+ msg_contents = contents
243
+ msg_contents.update(old_fields)
244
+ msg_obj = self.class.previous_version.new(msg_contents)
245
+
246
+ msg_obj.validate_contents
247
+ return msg_obj, old_fields
248
+ end
249
+ end
250
+
251
+ def define_mock_values(hash=nil, &blk)
252
+ if (hash && blk) || (!hash && !blk)
253
+ # value defined twice or not at all
254
+ raise SchemaDefinitionError.new("Mock values incorrectly defined")
255
+ end
256
+
257
+ hash = blk.call if blk
258
+
259
+ # Validate a sample of the mock values.
260
+ mock = {}
261
+ hash.each do |key, value|
262
+ mock[key] = value.respond_to?("call") ? value.call : value
263
+ end
264
+
265
+ begin
266
+ self.schema.validate(mock)
267
+ define_constant(:MOCK_VALUES, hash)
268
+ rescue Membrane::SchemaValidationError => e
269
+ raise SchemaDefinitionError.new("Sample mock values do not match schema: #{e}")
270
+ end
271
+ end
272
+
273
+ def define_constant(constant_name, constant_value)
274
+ self.const_set(constant_name, constant_value)
275
+ end
276
+
277
+ def include_preschemata
278
+ define_constant(:INCLUDE_PRESCHEMATA, true)
279
+ end
280
+
281
+ end
282
+ 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
@@ -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