lutaml-model 0.3.23 → 0.3.25
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +35 -16
- data/README.adoc +274 -28
- data/lib/lutaml/model/attribute.rb +18 -8
- data/lib/lutaml/model/error/type_error.rb +9 -0
- data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
- data/lib/lutaml/model/error/validation_error.rb +0 -1
- data/lib/lutaml/model/error.rb +2 -0
- data/lib/lutaml/model/serialize.rb +7 -2
- data/lib/lutaml/model/type/boolean.rb +38 -0
- data/lib/lutaml/model/type/date.rb +35 -0
- data/lib/lutaml/model/type/date_time.rb +32 -4
- data/lib/lutaml/model/type/decimal.rb +42 -0
- data/lib/lutaml/model/type/float.rb +37 -0
- data/lib/lutaml/model/type/hash.rb +62 -0
- data/lib/lutaml/model/type/integer.rb +41 -0
- data/lib/lutaml/model/type/string.rb +49 -0
- data/lib/lutaml/model/type/time.rb +49 -0
- data/lib/lutaml/model/type/time_without_date.rb +37 -5
- data/lib/lutaml/model/type/value.rb +52 -0
- data/lib/lutaml/model/type.rb +50 -114
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +5 -2
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +2 -1
- data/lib/lutaml/model/xml_adapter/xml_document.rb +0 -2
- data/lutaml-model.gemspec +1 -1
- data/spec/address_spec.rb +170 -0
- data/spec/fixtures/address.rb +33 -0
- data/spec/fixtures/person.rb +73 -0
- data/spec/fixtures/sample_model.rb +40 -0
- data/spec/fixtures/vase.rb +38 -0
- data/spec/fixtures/xml/special_char.xml +13 -0
- data/spec/lutaml/model/attribute_spec.rb +112 -0
- data/spec/lutaml/model/collection_spec.rb +299 -0
- data/spec/lutaml/model/comparable_model_spec.rb +106 -0
- data/spec/lutaml/model/custom_model_spec.rb +410 -0
- data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
- data/spec/lutaml/model/defaults_spec.rb +221 -0
- data/spec/lutaml/model/delegation_spec.rb +340 -0
- data/spec/lutaml/model/inheritance_spec.rb +92 -0
- data/spec/lutaml/model/json_adapter_spec.rb +37 -0
- data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
- data/spec/lutaml/model/map_content_spec.rb +118 -0
- data/spec/lutaml/model/mixed_content_spec.rb +625 -0
- data/spec/lutaml/model/namespace_spec.rb +57 -0
- data/spec/lutaml/model/ordered_content_spec.rb +83 -0
- data/spec/lutaml/model/render_nil_spec.rb +138 -0
- data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
- data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
- data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
- data/spec/lutaml/model/serializable_spec.rb +297 -0
- data/spec/lutaml/model/serializable_validation_spec.rb +85 -0
- data/spec/lutaml/model/simple_model_spec.rb +314 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
- data/spec/lutaml/model/type/boolean_spec.rb +54 -0
- data/spec/lutaml/model/type/date_spec.rb +118 -0
- data/spec/lutaml/model/type/date_time_spec.rb +127 -0
- data/spec/lutaml/model/type/decimal_spec.rb +125 -0
- data/spec/lutaml/model/type/float_spec.rb +191 -0
- data/spec/lutaml/model/type/hash_spec.rb +63 -0
- data/spec/lutaml/model/type/integer_spec.rb +145 -0
- data/spec/lutaml/model/type/string_spec.rb +150 -0
- data/spec/lutaml/model/type/time_spec.rb +142 -0
- data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
- data/spec/lutaml/model/type_spec.rb +276 -0
- data/spec/lutaml/model/utils_spec.rb +79 -0
- data/spec/lutaml/model/validation_spec.rb +83 -0
- data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
- data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
- data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
- data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
- data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
- data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
- data/spec/lutaml/model_spec.rb +1 -0
- data/spec/person_spec.rb +161 -0
- data/spec/spec_helper.rb +33 -0
- metadata +66 -2
@@ -0,0 +1,297 @@
|
|
1
|
+
module SerializeableSpec
|
2
|
+
class TestModel
|
3
|
+
attr_accessor :name, :age
|
4
|
+
|
5
|
+
def initialize(name: nil, age: nil)
|
6
|
+
@name = name
|
7
|
+
@age = age
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class TestModelMapper < Lutaml::Model::Serializable
|
12
|
+
model TestModel
|
13
|
+
|
14
|
+
attribute :name, Lutaml::Model::Type::String
|
15
|
+
attribute :age, Lutaml::Model::Type::String
|
16
|
+
end
|
17
|
+
|
18
|
+
class TestMapper < Lutaml::Model::Serializable
|
19
|
+
attribute :name, Lutaml::Model::Type::String
|
20
|
+
attribute :age, Lutaml::Model::Type::String
|
21
|
+
|
22
|
+
yaml do
|
23
|
+
map :na, to: :name
|
24
|
+
map :ag, to: :age
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
### XML root mapping
|
29
|
+
|
30
|
+
class RecordDate < Lutaml::Model::Serializable
|
31
|
+
attribute :content, :string
|
32
|
+
|
33
|
+
xml do
|
34
|
+
root "recordDate"
|
35
|
+
map_content to: :content
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class OriginInfo < Lutaml::Model::Serializable
|
40
|
+
attribute :date_issued, RecordDate, collection: true
|
41
|
+
|
42
|
+
xml do
|
43
|
+
root "originInfo"
|
44
|
+
map_element "dateIssued", to: :date_issued
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
### Enumeration
|
49
|
+
|
50
|
+
class Ceramic < Lutaml::Model::Serializable
|
51
|
+
attribute :type, :string
|
52
|
+
attribute :firing_temperature, :integer
|
53
|
+
end
|
54
|
+
|
55
|
+
class CeramicCollection < Lutaml::Model::Serializable
|
56
|
+
attribute :featured_piece,
|
57
|
+
Ceramic,
|
58
|
+
values: [
|
59
|
+
Ceramic.new(type: "Porcelain", firing_temperature: 1300),
|
60
|
+
Ceramic.new(type: "Stoneware", firing_temperature: 1200),
|
61
|
+
Ceramic.new(type: "Earthenware", firing_temperature: 1000),
|
62
|
+
]
|
63
|
+
end
|
64
|
+
|
65
|
+
class GlazeTechnique < Lutaml::Model::Serializable
|
66
|
+
attribute :name, :string, values: ["Celadon", "Raku", "Majolica"]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
RSpec.describe Lutaml::Model::Serializable do
|
71
|
+
describe ".model" do
|
72
|
+
it "sets the model for the class" do
|
73
|
+
expect do
|
74
|
+
described_class.model(SerializeableSpec::TestModel)
|
75
|
+
end.to change(
|
76
|
+
described_class, :model
|
77
|
+
)
|
78
|
+
.from(nil)
|
79
|
+
.to(SerializeableSpec::TestModel)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe ".attribute" do
|
84
|
+
subject(:mapper) { described_class.new }
|
85
|
+
|
86
|
+
it "adds the attribute and getter setter for that attribute" do
|
87
|
+
expect { described_class.attribute("foo", Lutaml::Model::Type::String) }
|
88
|
+
.to change { described_class.attributes.keys }.from([]).to(["foo"])
|
89
|
+
.and change { mapper.respond_to?(:foo) }.from(false).to(true)
|
90
|
+
.and change { mapper.respond_to?(:foo=) }.from(false).to(true)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe ".hash_representation" do
|
95
|
+
context "when model is separate" do
|
96
|
+
let(:instance) do
|
97
|
+
SerializeableSpec::TestModel.new(name: "John", age: 18)
|
98
|
+
end
|
99
|
+
|
100
|
+
let(:expected_hash) do
|
101
|
+
{
|
102
|
+
"name" => "John",
|
103
|
+
"age" => "18",
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
it "return hash representation" do
|
108
|
+
generate_hash = SerializeableSpec::TestModelMapper.hash_representation(
|
109
|
+
instance, :yaml
|
110
|
+
)
|
111
|
+
expect(generate_hash).to eq(expected_hash)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
context "when model is self" do
|
116
|
+
let(:instance) do
|
117
|
+
SerializeableSpec::TestMapper.new(name: "John", age: 18)
|
118
|
+
end
|
119
|
+
|
120
|
+
let(:expected_hash) do
|
121
|
+
{
|
122
|
+
na: "John",
|
123
|
+
ag: "18",
|
124
|
+
}
|
125
|
+
end
|
126
|
+
|
127
|
+
it "return hash representation" do
|
128
|
+
generate_hash = SerializeableSpec::TestMapper.hash_representation(
|
129
|
+
instance, :yaml
|
130
|
+
)
|
131
|
+
expect(generate_hash).to eq(expected_hash)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe ".mappings_for" do
|
137
|
+
context "when mapping is defined" do
|
138
|
+
it "returns the defined mapping" do
|
139
|
+
actual_mappings = SerializeableSpec::TestMapper.mappings_for(:yaml).mappings
|
140
|
+
|
141
|
+
expect(actual_mappings[0].name).to eq(:na)
|
142
|
+
expect(actual_mappings[0].to).to eq(:name)
|
143
|
+
|
144
|
+
expect(actual_mappings[1].name).to eq(:ag)
|
145
|
+
expect(actual_mappings[1].to).to eq(:age)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "when mapping is not defined" do
|
150
|
+
it "maps attributes to mappings" do
|
151
|
+
allow(SerializeableSpec::TestMapper.mappings).to receive(:[]).with(:yaml).and_return(nil)
|
152
|
+
|
153
|
+
actual_mappings = SerializeableSpec::TestMapper.mappings_for(:yaml).mappings
|
154
|
+
|
155
|
+
expect(actual_mappings[0].name).to eq("name")
|
156
|
+
expect(actual_mappings[0].to).to eq(:name)
|
157
|
+
|
158
|
+
expect(actual_mappings[1].name).to eq("age")
|
159
|
+
expect(actual_mappings[1].to).to eq(:age)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe ".apply_child_mappings" do
|
165
|
+
let(:child_mappings) do
|
166
|
+
{
|
167
|
+
id: :key,
|
168
|
+
path: %i[path link],
|
169
|
+
name: %i[path name],
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
let(:hash) do
|
174
|
+
{
|
175
|
+
"foo" => {
|
176
|
+
"path" => {
|
177
|
+
"link" => "link one",
|
178
|
+
"name" => "one",
|
179
|
+
},
|
180
|
+
},
|
181
|
+
"abc" => {
|
182
|
+
"path" => {
|
183
|
+
"link" => "link two",
|
184
|
+
"name" => "two",
|
185
|
+
},
|
186
|
+
},
|
187
|
+
"hello" => {
|
188
|
+
"path" => {
|
189
|
+
"link" => "link three",
|
190
|
+
"name" => "three",
|
191
|
+
},
|
192
|
+
},
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
let(:expected_value) do
|
197
|
+
[
|
198
|
+
{ id: "foo", path: "link one", name: "one" },
|
199
|
+
{ id: "abc", path: "link two", name: "two" },
|
200
|
+
{ id: "hello", path: "link three", name: "three" },
|
201
|
+
]
|
202
|
+
end
|
203
|
+
|
204
|
+
it "generates hash based on child_mappings" do
|
205
|
+
expect(described_class.apply_child_mappings(hash,
|
206
|
+
child_mappings)).to eq(expected_value)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "XML root name override" do
|
211
|
+
it "uses root name defined at the component class" do
|
212
|
+
record_date = SerializeableSpec::RecordDate.new(content: "2021-01-01")
|
213
|
+
expected_xml = "<recordDate>2021-01-01</recordDate>"
|
214
|
+
expect(record_date.to_xml).to eq(expected_xml)
|
215
|
+
end
|
216
|
+
|
217
|
+
it "uses mapped element name at the aggregating class, overriding root name" do
|
218
|
+
origin_info = SerializeableSpec::OriginInfo.new(date_issued: [SerializeableSpec::RecordDate.new(content: "2021-01-01")])
|
219
|
+
expected_xml = <<~XML
|
220
|
+
<originInfo><dateIssued>2021-01-01</dateIssued></originInfo>
|
221
|
+
XML
|
222
|
+
expect(origin_info.to_xml).to be_equivalent_to(expected_xml)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "String enumeration" do
|
227
|
+
context "when assigning an invalid value" do
|
228
|
+
it "raises an error after creation after validate" do
|
229
|
+
glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon")
|
230
|
+
glaze.name = "Tenmoku"
|
231
|
+
expect do
|
232
|
+
glaze.validate!
|
233
|
+
end.to raise_error(Lutaml::Model::ValidationError) do |error|
|
234
|
+
expect(error).to include(Lutaml::Model::InvalidValueError)
|
235
|
+
expect(error.error_messages).to include("name is `Tenmoku`, must be one of the following [Celadon, Raku, Majolica]")
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context "when assigning a valid value" do
|
241
|
+
it "changes the value after creation" do
|
242
|
+
glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon")
|
243
|
+
glaze.name = "Raku"
|
244
|
+
expect(glaze.name).to eq("Raku")
|
245
|
+
end
|
246
|
+
|
247
|
+
it "assigns the value during creation" do
|
248
|
+
glaze = SerializeableSpec::GlazeTechnique.new(name: "Majolica")
|
249
|
+
expect(glaze.name).to eq("Majolica")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
describe "Serializable object enumeration" do
|
255
|
+
context "when assigning an invalid value" do
|
256
|
+
it "raises ValidationError containing InvalidValueError after creation" do
|
257
|
+
glaze = SerializeableSpec::GlazeTechnique.new(name: "Celadon")
|
258
|
+
glaze.name = "Tenmoku"
|
259
|
+
expect do
|
260
|
+
glaze.validate!
|
261
|
+
end.to raise_error(Lutaml::Model::ValidationError) do |error|
|
262
|
+
expect(error).to include(Lutaml::Model::InvalidValueError)
|
263
|
+
expect(error.error_messages).to include(a_string_matching(/name is `Tenmoku`, must be one of the following/))
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
it "raises ValidationError containing InvalidValueError during creation" do
|
268
|
+
expect do
|
269
|
+
SerializeableSpec::GlazeTechnique.new(name: "Crystalline").validate!
|
270
|
+
end.to raise_error(Lutaml::Model::ValidationError) do |error|
|
271
|
+
expect(error).to include(Lutaml::Model::InvalidValueError)
|
272
|
+
expect(error.error_messages).to include(a_string_matching(/name is `Crystalline`, must be one of the following/))
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
context "when assigning a valid value" do
|
278
|
+
it "changes the value after creation" do
|
279
|
+
collection = SerializeableSpec::CeramicCollection.new(
|
280
|
+
featured_piece: SerializeableSpec::Ceramic.new(type: "Porcelain",
|
281
|
+
firing_temperature: 1300),
|
282
|
+
)
|
283
|
+
collection.featured_piece = SerializeableSpec::Ceramic.new(type: "Stoneware",
|
284
|
+
firing_temperature: 1200)
|
285
|
+
expect(collection.featured_piece.type).to eq("Stoneware")
|
286
|
+
end
|
287
|
+
|
288
|
+
it "assigns the value during creation" do
|
289
|
+
collection = SerializeableSpec::CeramicCollection.new(
|
290
|
+
featured_piece: SerializeableSpec::Ceramic.new(type: "Earthenware",
|
291
|
+
firing_temperature: 1000),
|
292
|
+
)
|
293
|
+
expect(collection.featured_piece.type).to eq("Earthenware")
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class TestSerializable < Lutaml::Model::Serializable
|
4
|
+
attribute :name, :string, values: ["Alice", "Bob", "Charlie"]
|
5
|
+
attribute :age, :integer, collection: 1..3
|
6
|
+
|
7
|
+
xml do
|
8
|
+
root "test"
|
9
|
+
map_element "name", to: :name
|
10
|
+
map_element "age", to: :age
|
11
|
+
end
|
12
|
+
|
13
|
+
json do
|
14
|
+
map "name", to: :name
|
15
|
+
map "age", to: :age
|
16
|
+
end
|
17
|
+
|
18
|
+
yaml do
|
19
|
+
map "name", to: :name
|
20
|
+
map "age", to: :age
|
21
|
+
end
|
22
|
+
|
23
|
+
toml do
|
24
|
+
map "name", to: :name
|
25
|
+
map "age", to: :age
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.describe Lutaml::Model::Serializable do
|
30
|
+
let(:valid_instance) { TestSerializable.new(name: "Alice", age: [30]) }
|
31
|
+
let(:invalid_instance) do
|
32
|
+
TestSerializable.new(name: "David", age: [25, 30, 35, 40])
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "serialization methods" do
|
36
|
+
it "does not raise validation errors when calling to_xml" do
|
37
|
+
expect { invalid_instance.to_xml }.not_to raise_error
|
38
|
+
end
|
39
|
+
|
40
|
+
it "does not raise validation errors when calling to_json" do
|
41
|
+
expect { invalid_instance.to_json }.not_to raise_error
|
42
|
+
end
|
43
|
+
|
44
|
+
it "does not raise validation errors when calling to_yaml" do
|
45
|
+
expect { invalid_instance.to_yaml }.not_to raise_error
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not raise validation errors when calling to_toml" do
|
49
|
+
expect { invalid_instance.to_toml }.not_to raise_error
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "setting attributes" do
|
54
|
+
it "does not raise validation errors when setting valid attributes" do
|
55
|
+
expect { valid_instance.name = "Bob" }.not_to raise_error
|
56
|
+
expect { valid_instance.age = [25, 30] }.not_to raise_error
|
57
|
+
end
|
58
|
+
|
59
|
+
it "does not raise validation errors when setting invalid attributes" do
|
60
|
+
expect { invalid_instance.name = "David" }.not_to raise_error
|
61
|
+
expect { invalid_instance.age = [25, 30, 35, 40] }.not_to raise_error
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "validate method" do
|
66
|
+
it "returns errors for invalid attributes" do
|
67
|
+
errors = invalid_instance.validate
|
68
|
+
expect(errors).not_to be_empty
|
69
|
+
expect(errors.first).to be_a(Lutaml::Model::InvalidValueError)
|
70
|
+
expect(errors.last).to be_a(Lutaml::Model::CollectionCountOutOfRangeError)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "validate! method" do
|
75
|
+
it "raises ValidationError for invalid attributes" do
|
76
|
+
expect do
|
77
|
+
invalid_instance.validate!
|
78
|
+
end.to raise_error(Lutaml::Model::ValidationError)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "does not raise ValidationError for valid attributes" do
|
82
|
+
expect { valid_instance.validate! }.not_to raise_error
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "lutaml/model"
|
3
|
+
|
4
|
+
module SimpleModel
|
5
|
+
class Address < Lutaml::Model::Serializable
|
6
|
+
attribute :street, :string
|
7
|
+
attribute :city, :string
|
8
|
+
|
9
|
+
xml do
|
10
|
+
root "address"
|
11
|
+
map_element "street", to: :street
|
12
|
+
map_element "city", to: :city
|
13
|
+
end
|
14
|
+
|
15
|
+
yaml do
|
16
|
+
map "street", to: :street
|
17
|
+
map "city", to: :city
|
18
|
+
end
|
19
|
+
|
20
|
+
json do
|
21
|
+
map "street", to: :street
|
22
|
+
map "city", to: :city
|
23
|
+
end
|
24
|
+
|
25
|
+
toml do
|
26
|
+
map "street", to: :street
|
27
|
+
map "city", to: :city
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Room < Lutaml::Model::Serializable
|
32
|
+
attribute :name, :string, default: -> { "Unnamed room" }
|
33
|
+
attribute :size, :integer
|
34
|
+
|
35
|
+
xml do
|
36
|
+
root "room"
|
37
|
+
map_element "name", to: :name
|
38
|
+
map_element "size", to: :size
|
39
|
+
end
|
40
|
+
|
41
|
+
yaml do
|
42
|
+
map "name", to: :name
|
43
|
+
map "size", to: :size
|
44
|
+
end
|
45
|
+
|
46
|
+
json do
|
47
|
+
map "name", to: :name
|
48
|
+
map "size", to: :size
|
49
|
+
end
|
50
|
+
|
51
|
+
toml do
|
52
|
+
map "name", to: :name
|
53
|
+
map "size", to: :size
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class Building < Lutaml::Model::Serializable
|
58
|
+
attribute :name, :string, default: -> { "Unnamed building" }
|
59
|
+
attribute :address, Address
|
60
|
+
attribute :rooms, Room, collection: true
|
61
|
+
|
62
|
+
xml do
|
63
|
+
root "building"
|
64
|
+
map_attribute "name", to: :name
|
65
|
+
map_element "address", to: :address
|
66
|
+
map_element "room", to: :rooms
|
67
|
+
end
|
68
|
+
|
69
|
+
yaml do
|
70
|
+
map "name", to: :name
|
71
|
+
map "address", to: :address
|
72
|
+
map "rooms", to: :rooms
|
73
|
+
end
|
74
|
+
|
75
|
+
json do
|
76
|
+
map "name", to: :name
|
77
|
+
map "address", to: :address
|
78
|
+
map "rooms", to: :rooms
|
79
|
+
end
|
80
|
+
|
81
|
+
toml do
|
82
|
+
map "name", to: :name
|
83
|
+
map "address", to: :address
|
84
|
+
map "rooms", to: :rooms
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
RSpec.describe SimpleModel do
|
90
|
+
let(:address) do
|
91
|
+
SimpleModel::Address.new(street: "123 Main St", city: "Anytown")
|
92
|
+
end
|
93
|
+
let(:rooms) do
|
94
|
+
[SimpleModel::Room.new(name: "Conference Room", size: 30),
|
95
|
+
SimpleModel::Room.new(name: "Office", size: 20)]
|
96
|
+
end
|
97
|
+
let(:attributes) { { name: "Headquarters", address: address, rooms: rooms } }
|
98
|
+
let(:model) { SimpleModel::Building.new(attributes) }
|
99
|
+
|
100
|
+
let(:model_xml) do
|
101
|
+
<<~XML
|
102
|
+
<building name="Headquarters">
|
103
|
+
<address>
|
104
|
+
<street>123 Main St</street>
|
105
|
+
<city>Anytown</city>
|
106
|
+
</address>
|
107
|
+
<room>
|
108
|
+
<name>Conference Room</name>
|
109
|
+
<size>30</size>
|
110
|
+
</room>
|
111
|
+
<room>
|
112
|
+
<name>Office</name>
|
113
|
+
<size>20</size>
|
114
|
+
</room>
|
115
|
+
</building>
|
116
|
+
XML
|
117
|
+
end
|
118
|
+
|
119
|
+
let(:model_yaml) do
|
120
|
+
<<~YAML
|
121
|
+
---
|
122
|
+
name: Headquarters
|
123
|
+
address:
|
124
|
+
street: 123 Main St
|
125
|
+
city: Anytown
|
126
|
+
rooms:
|
127
|
+
- name: Conference Room
|
128
|
+
size: 30
|
129
|
+
- name: Office
|
130
|
+
size: 20
|
131
|
+
YAML
|
132
|
+
end
|
133
|
+
|
134
|
+
let(:model_json) do
|
135
|
+
{
|
136
|
+
name: "Headquarters",
|
137
|
+
address: {
|
138
|
+
street: "123 Main St",
|
139
|
+
city: "Anytown",
|
140
|
+
},
|
141
|
+
rooms: [
|
142
|
+
{
|
143
|
+
name: "Conference Room",
|
144
|
+
size: 30,
|
145
|
+
},
|
146
|
+
{
|
147
|
+
name: "Office",
|
148
|
+
size: 20,
|
149
|
+
},
|
150
|
+
],
|
151
|
+
}.to_json
|
152
|
+
end
|
153
|
+
|
154
|
+
let(:model_toml) do
|
155
|
+
<<~TOML
|
156
|
+
name = "Headquarters"
|
157
|
+
[address]
|
158
|
+
city = "Anytown"
|
159
|
+
street = "123 Main St"
|
160
|
+
[[rooms]]
|
161
|
+
name = "Conference Room"
|
162
|
+
size = 30
|
163
|
+
[[rooms]]
|
164
|
+
name = "Office"
|
165
|
+
size = 20
|
166
|
+
TOML
|
167
|
+
end
|
168
|
+
|
169
|
+
let(:model_yaml_missing_element) do
|
170
|
+
<<~YAML
|
171
|
+
---
|
172
|
+
name: Headquarters
|
173
|
+
rooms:
|
174
|
+
- name: Conference Room
|
175
|
+
size: 30
|
176
|
+
- name: Office
|
177
|
+
size: 20
|
178
|
+
YAML
|
179
|
+
end
|
180
|
+
|
181
|
+
it "initializes with default values" do
|
182
|
+
default_model = SimpleModel::Building.new
|
183
|
+
expect(default_model.name).to eq("Unnamed building")
|
184
|
+
expect(default_model.address).to be_nil
|
185
|
+
expect(default_model.rooms).to eq([])
|
186
|
+
end
|
187
|
+
|
188
|
+
it "serializes to XML" do
|
189
|
+
expected_xml = model_xml.strip
|
190
|
+
expect(model.to_xml.strip).to eq(expected_xml)
|
191
|
+
end
|
192
|
+
|
193
|
+
it "deserializes from XML" do
|
194
|
+
sample = SimpleModel::Building.from_xml(model_xml)
|
195
|
+
expect(sample.name).to eq("Headquarters")
|
196
|
+
expect(sample.address.street).to eq("123 Main St")
|
197
|
+
expect(sample.address.city).to eq("Anytown")
|
198
|
+
expect(sample.rooms.size).to eq(2)
|
199
|
+
expect(sample.rooms[0].name).to eq("Conference Room")
|
200
|
+
expect(sample.rooms[0].size).to eq(30)
|
201
|
+
expect(sample.rooms[1].name).to eq("Office")
|
202
|
+
expect(sample.rooms[1].size).to eq(20)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "round-trips XML" do
|
206
|
+
xml = model.to_xml
|
207
|
+
new_model = SimpleModel::Building.from_xml(xml)
|
208
|
+
expect(new_model.name).to eq(model.name)
|
209
|
+
expect(new_model.address.street).to eq(model.address.street)
|
210
|
+
expect(new_model.address.city).to eq(model.address.city)
|
211
|
+
expect(new_model.rooms.size).to eq(model.rooms.size)
|
212
|
+
model.rooms.each_with_index do |room, index|
|
213
|
+
expect(new_model.rooms[index].name).to eq(room.name)
|
214
|
+
expect(new_model.rooms[index].size).to eq(room.size)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
it "serializes to YAML" do
|
219
|
+
expected_yaml = model_yaml.strip
|
220
|
+
expect(model.to_yaml.strip).to eq(expected_yaml)
|
221
|
+
end
|
222
|
+
|
223
|
+
it "deserializes from YAML" do
|
224
|
+
sample = SimpleModel::Building.from_yaml(model_yaml)
|
225
|
+
expect(sample.name).to eq("Headquarters")
|
226
|
+
expect(sample.address.street).to eq("123 Main St")
|
227
|
+
expect(sample.address.city).to eq("Anytown")
|
228
|
+
expect(sample.rooms.size).to eq(2)
|
229
|
+
expect(sample.rooms[0].name).to eq("Conference Room")
|
230
|
+
expect(sample.rooms[0].size).to eq(30)
|
231
|
+
expect(sample.rooms[1].name).to eq("Office")
|
232
|
+
expect(sample.rooms[1].size).to eq(20)
|
233
|
+
end
|
234
|
+
|
235
|
+
it "round-trips YAML" do
|
236
|
+
yaml = model.to_yaml
|
237
|
+
new_model = SimpleModel::Building.from_yaml(yaml)
|
238
|
+
expect(new_model.name).to eq(model.name)
|
239
|
+
expect(new_model.address.street).to eq(model.address.street)
|
240
|
+
expect(new_model.address.city).to eq(model.address.city)
|
241
|
+
expect(new_model.rooms.size).to eq(model.rooms.size)
|
242
|
+
model.rooms.each_with_index do |room, index|
|
243
|
+
expect(new_model.rooms[index].name).to eq(room.name)
|
244
|
+
expect(new_model.rooms[index].size).to eq(room.size)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
it "serializes to YAML without the omitted element" do
|
249
|
+
model_without_address = SimpleModel::Building.new(name: "Headquarters",
|
250
|
+
rooms: rooms)
|
251
|
+
expected_yaml = model_yaml_missing_element.strip
|
252
|
+
expect(model_without_address.to_yaml.strip).to eq(expected_yaml)
|
253
|
+
end
|
254
|
+
|
255
|
+
it "serializes to JSON" do
|
256
|
+
expected_json = model_json.strip
|
257
|
+
expect(model.to_json.strip).to eq(expected_json)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "deserializes from JSON" do
|
261
|
+
sample = SimpleModel::Building.from_json(model_json)
|
262
|
+
expect(sample.name).to eq("Headquarters")
|
263
|
+
expect(sample.address.street).to eq("123 Main St")
|
264
|
+
expect(sample.address.city).to eq("Anytown")
|
265
|
+
expect(sample.rooms.size).to eq(2)
|
266
|
+
expect(sample.rooms[0].name).to eq("Conference Room")
|
267
|
+
expect(sample.rooms[0].size).to eq(30)
|
268
|
+
expect(sample.rooms[1].name).to eq("Office")
|
269
|
+
expect(sample.rooms[1].size).to eq(20)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "round-trips JSON" do
|
273
|
+
json = model.to_json
|
274
|
+
new_model = SimpleModel::Building.from_json(json)
|
275
|
+
expect(new_model.name).to eq(model.name)
|
276
|
+
expect(new_model.address.street).to eq(model.address.street)
|
277
|
+
expect(new_model.address.city).to eq(model.address.city)
|
278
|
+
expect(new_model.rooms.size).to eq(model.rooms.size)
|
279
|
+
model.rooms.each_with_index do |room, index|
|
280
|
+
expect(new_model.rooms[index].name).to eq(room.name)
|
281
|
+
expect(new_model.rooms[index].size).to eq(room.size)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
it "serializes to TOML" do
|
286
|
+
expected_toml = model_toml.strip
|
287
|
+
expect(model.to_toml.strip).to eq(expected_toml)
|
288
|
+
end
|
289
|
+
|
290
|
+
it "deserializes from TOML" do
|
291
|
+
sample = SimpleModel::Building.from_toml(model_toml)
|
292
|
+
expect(sample.name).to eq("Headquarters")
|
293
|
+
expect(sample.address.street).to eq("123 Main St")
|
294
|
+
expect(sample.address.city).to eq("Anytown")
|
295
|
+
expect(sample.rooms.size).to eq(2)
|
296
|
+
expect(sample.rooms[0].name).to eq("Conference Room")
|
297
|
+
expect(sample.rooms[0].size).to eq(30)
|
298
|
+
expect(sample.rooms[1].name).to eq("Office")
|
299
|
+
expect(sample.rooms[1].size).to eq(20)
|
300
|
+
end
|
301
|
+
|
302
|
+
it "round-trips TOML" do
|
303
|
+
toml = model.to_toml
|
304
|
+
new_model = SimpleModel::Building.from_toml(toml)
|
305
|
+
expect(new_model.name).to eq(model.name)
|
306
|
+
expect(new_model.address.street).to eq(model.address.street)
|
307
|
+
expect(new_model.address.city).to eq(model.address.city)
|
308
|
+
expect(new_model.rooms.size).to eq(model.rooms.size)
|
309
|
+
model.rooms.each_with_index do |room, index|
|
310
|
+
expect(new_model.rooms[index].name).to eq(room.name)
|
311
|
+
expect(new_model.rooms[index].size).to eq(room.size)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|