lutaml-model 0.3.24 → 0.3.25
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.
- 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 +6 -1
- 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
|