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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +35 -16
  3. data/README.adoc +274 -28
  4. data/lib/lutaml/model/attribute.rb +18 -8
  5. data/lib/lutaml/model/error/type_error.rb +9 -0
  6. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  7. data/lib/lutaml/model/error/validation_error.rb +0 -1
  8. data/lib/lutaml/model/error.rb +2 -0
  9. data/lib/lutaml/model/serialize.rb +7 -2
  10. data/lib/lutaml/model/type/boolean.rb +38 -0
  11. data/lib/lutaml/model/type/date.rb +35 -0
  12. data/lib/lutaml/model/type/date_time.rb +32 -4
  13. data/lib/lutaml/model/type/decimal.rb +42 -0
  14. data/lib/lutaml/model/type/float.rb +37 -0
  15. data/lib/lutaml/model/type/hash.rb +62 -0
  16. data/lib/lutaml/model/type/integer.rb +41 -0
  17. data/lib/lutaml/model/type/string.rb +49 -0
  18. data/lib/lutaml/model/type/time.rb +49 -0
  19. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  20. data/lib/lutaml/model/type/value.rb +52 -0
  21. data/lib/lutaml/model/type.rb +50 -114
  22. data/lib/lutaml/model/version.rb +1 -1
  23. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +5 -2
  24. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +2 -1
  25. data/lib/lutaml/model/xml_adapter/xml_document.rb +0 -2
  26. data/lutaml-model.gemspec +1 -1
  27. data/spec/address_spec.rb +170 -0
  28. data/spec/fixtures/address.rb +33 -0
  29. data/spec/fixtures/person.rb +73 -0
  30. data/spec/fixtures/sample_model.rb +40 -0
  31. data/spec/fixtures/vase.rb +38 -0
  32. data/spec/fixtures/xml/special_char.xml +13 -0
  33. data/spec/lutaml/model/attribute_spec.rb +112 -0
  34. data/spec/lutaml/model/collection_spec.rb +299 -0
  35. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  36. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  37. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  38. data/spec/lutaml/model/defaults_spec.rb +221 -0
  39. data/spec/lutaml/model/delegation_spec.rb +340 -0
  40. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  41. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  42. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  43. data/spec/lutaml/model/map_content_spec.rb +118 -0
  44. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  45. data/spec/lutaml/model/namespace_spec.rb +57 -0
  46. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  47. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  48. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  49. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  50. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  51. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  52. data/spec/lutaml/model/serializable_spec.rb +297 -0
  53. data/spec/lutaml/model/serializable_validation_spec.rb +85 -0
  54. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  55. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  56. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  57. data/spec/lutaml/model/type/date_spec.rb +118 -0
  58. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  59. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  60. data/spec/lutaml/model/type/float_spec.rb +191 -0
  61. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  62. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  63. data/spec/lutaml/model/type/string_spec.rb +150 -0
  64. data/spec/lutaml/model/type/time_spec.rb +142 -0
  65. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  66. data/spec/lutaml/model/type_spec.rb +276 -0
  67. data/spec/lutaml/model/utils_spec.rb +79 -0
  68. data/spec/lutaml/model/validation_spec.rb +83 -0
  69. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  70. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  71. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  72. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  73. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  74. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  75. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  76. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  77. data/spec/lutaml/model_spec.rb +1 -0
  78. data/spec/person_spec.rb +161 -0
  79. data/spec/spec_helper.rb +33 -0
  80. metadata +66 -2
@@ -0,0 +1,410 @@
1
+ require "spec_helper"
2
+
3
+ class CustomModelChild
4
+ attr_accessor :street, :city
5
+ end
6
+
7
+ class CustomModelParent
8
+ attr_accessor :first_name, :middle_name, :last_name, :child_mapper
9
+
10
+ def name
11
+ "#{first_name} #{last_name}"
12
+ end
13
+ end
14
+
15
+ class CustomModelChildMapper < Lutaml::Model::Serializable
16
+ model CustomModelChild
17
+
18
+ attribute :street, Lutaml::Model::Type::String
19
+ attribute :city, Lutaml::Model::Type::String
20
+
21
+ xml do
22
+ map_element :street, to: :street
23
+ map_element :city, to: :city
24
+ end
25
+ end
26
+
27
+ class CustomModelParentMapper < Lutaml::Model::Serializable
28
+ model CustomModelParent
29
+
30
+ attribute :first_name, Lutaml::Model::Type::String
31
+ attribute :middle_name, Lutaml::Model::Type::String
32
+ attribute :last_name, Lutaml::Model::Type::String
33
+ attribute :child_mapper, CustomModelChildMapper
34
+
35
+ xml do
36
+ map_element :first_name, to: :first_name
37
+ map_element :middle_name, to: :middle_name
38
+ map_element :last_name, to: :last_name
39
+ map_element :CustomModelChild,
40
+ with: { to: :child_to_xml, from: :child_from_xml }
41
+ end
42
+
43
+ def child_to_xml(model, parent, doc)
44
+ child_el = doc.create_element("CustomModelChild")
45
+ street_el = doc.create_element("street")
46
+ city_el = doc.create_element("city")
47
+
48
+ doc.add_text(street_el, model.child_mapper.street)
49
+ doc.add_text(city_el, model.child_mapper.city)
50
+
51
+ doc.add_element(child_el, street_el)
52
+ doc.add_element(child_el, city_el)
53
+ doc.add_element(parent, child_el)
54
+ end
55
+
56
+ def child_from_xml(model, value)
57
+ model.child_mapper ||= CustomModelChild.new
58
+
59
+ model.child_mapper.street = value["street"].text
60
+ model.child_mapper.city = value["city"].text
61
+ end
62
+ end
63
+
64
+ module CustomModelSpecs
65
+ class TextElement < Lutaml::Model::Serializable
66
+ attribute :sup, :string
67
+ attribute :sub, :string
68
+ attribute :text, :string
69
+
70
+ xml do
71
+ root "text-element"
72
+
73
+ map_content to: :text
74
+
75
+ map_element "sup", to: :sup
76
+ map_element "sub", to: :sub
77
+ end
78
+ end
79
+
80
+ class Id
81
+ attr_accessor :id
82
+ end
83
+
84
+ class Docid < Lutaml::Model::Serializable
85
+ model Id
86
+ attribute :id, TextElement
87
+
88
+ xml do
89
+ root "docid", mixed: true
90
+
91
+ map_content to: :text, delegate: :id
92
+ map_element :sub, to: :sub, delegate: :id
93
+ map_element :sup, to: :sup, delegate: :id
94
+ end
95
+ end
96
+
97
+ class BibliographicItem
98
+ attr_accessor :type, :title, :schema_version, :language
99
+
100
+ def initialize(**attr)
101
+ @type = attr["type"]
102
+ @title = attr["title"]
103
+ @schema_version = attr["schema_version"]
104
+ @language = attr["language"]
105
+ end
106
+ end
107
+
108
+ class Bibdata < Lutaml::Model::Serializable
109
+ model BibliographicItem
110
+ end
111
+
112
+ class MixedWithNestedContent < Lutaml::Model::Serializable
113
+ attribute :street, :string, raw: true
114
+ attribute :city, :string, raw: true
115
+ attribute :bibdata, Bibdata
116
+
117
+ xml do
118
+ root "MixedWithNestedContent", mixed: true
119
+
120
+ map_element "street", to: :street
121
+ map_element "city", to: :city
122
+ map_element "bibdata",
123
+ to: :bibdata,
124
+ with: { from: :bibdata_from_xml, to: :bibdata_to_xml }
125
+ end
126
+
127
+ def bibdata_from_xml(model, value)
128
+ model.bibdata = BibliographicItem.new(
129
+ "type" => value["type"],
130
+ "title" => value["title"],
131
+ "language" => value["title"]["language"],
132
+ "schema_version" => value["schema-version"],
133
+ )
134
+ end
135
+
136
+ def bibdata_to_xml(model, _parent, doc)
137
+ attributes = {
138
+ "type" => model.bibdata.type,
139
+ "schema-version" => model.bibdata.schema_version,
140
+ }
141
+ lang = model.bibdata.language
142
+
143
+ doc.create_and_add_element("bibdata", attributes: attributes) do
144
+ doc.create_and_add_element("title", attributes: { language: lang }) do
145
+ doc.add_text(doc, model.bibdata.title.text)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ RSpec.describe "CustomModel" do
153
+ let(:parent_mapper) { CustomModelParentMapper }
154
+ let(:child_mapper) { CustomModelChildMapper }
155
+ let(:parent_model) { CustomModelParent }
156
+ let(:child_model) { CustomModelChild }
157
+
158
+ context "with JSON mapping" do
159
+ let(:input_json) do
160
+ {
161
+ first_name: "John",
162
+ last_name: "Doe",
163
+ child_mapper: {
164
+ street: "Oxford Street",
165
+ city: "London",
166
+ },
167
+ }.to_json
168
+ end
169
+
170
+ describe ".from_json" do
171
+ it "maps JSON string to custom model" do
172
+ instance = parent_mapper.from_json(input_json)
173
+
174
+ expect(instance.class).to eq(parent_model)
175
+ expect(instance.first_name).to eq("John")
176
+ expect(instance.last_name).to eq("Doe")
177
+ expect(instance.name).to eq("John Doe")
178
+
179
+ expect(instance.child_mapper.class).to eq(child_model)
180
+ expect(instance.child_mapper.street).to eq("Oxford Street")
181
+ expect(instance.child_mapper.city).to eq("London")
182
+ end
183
+ end
184
+
185
+ describe ".to_json" do
186
+ it "with wrong model raises an exception" do
187
+ msg = /argument is a 'String' but should be a '#{parent_model.name}/
188
+
189
+ expect do
190
+ parent_mapper.to_json("")
191
+ end.to raise_error(Lutaml::Model::IncorrectModelError, msg)
192
+ end
193
+
194
+ it "with correct model converts objects to json" do
195
+ instance = parent_mapper.from_json(input_json)
196
+
197
+ expect(parent_mapper.to_json(instance)).to eq(input_json)
198
+ end
199
+ end
200
+ end
201
+
202
+ context "with YAML mapping" do
203
+ let(:input_yaml) do
204
+ {
205
+ "first_name" => "John",
206
+ "last_name" => "Doe",
207
+ "child_mapper" => {
208
+ "street" => "Oxford Street",
209
+ "city" => "London",
210
+ },
211
+ }.to_yaml
212
+ end
213
+
214
+ describe ".from_yaml" do
215
+ it "maps YAML to custom model" do
216
+ instance = parent_mapper.from_yaml(input_yaml)
217
+
218
+ expect(instance.class).to eq(parent_model)
219
+ expect(instance.first_name).to eq("John")
220
+ expect(instance.last_name).to eq("Doe")
221
+ expect(instance.name).to eq("John Doe")
222
+
223
+ expect(instance.child_mapper.class).to eq(child_model)
224
+ expect(instance.child_mapper.street).to eq("Oxford Street")
225
+ expect(instance.child_mapper.city).to eq("London")
226
+ end
227
+ end
228
+
229
+ describe ".to_yaml" do
230
+ it "with wrong model raises an exception" do
231
+ msg = /argument is a 'String' but should be a '#{parent_model.name}/
232
+
233
+ expect do
234
+ parent_mapper.to_yaml("")
235
+ end.to raise_error(Lutaml::Model::IncorrectModelError, msg)
236
+ end
237
+
238
+ it "with correct model converts objects to yaml" do
239
+ instance = parent_mapper.from_yaml(input_yaml)
240
+
241
+ expect(parent_mapper.to_yaml(instance)).to eq(input_yaml)
242
+ end
243
+ end
244
+ end
245
+
246
+ context "with TOML mapping" do
247
+ let(:input_toml) do
248
+ <<~TOML
249
+ first_name = "John"
250
+ last_name = "Doe"
251
+ [child_mapper]
252
+ city = "London"
253
+ street = "Oxford Street"
254
+ TOML
255
+ end
256
+
257
+ describe ".from_toml" do
258
+ it "maps TOML content to custom model" do
259
+ instance = parent_mapper.from_toml(input_toml)
260
+
261
+ expect(instance.class).to eq(parent_model)
262
+ expect(instance.first_name).to eq("John")
263
+ expect(instance.last_name).to eq("Doe")
264
+ expect(instance.name).to eq("John Doe")
265
+
266
+ expect(instance.child_mapper.class).to eq(child_model)
267
+ expect(instance.child_mapper.street).to eq("Oxford Street")
268
+ expect(instance.child_mapper.city).to eq("London")
269
+ end
270
+ end
271
+
272
+ describe ".to_toml" do
273
+ it "with wrong model raises an exception" do
274
+ msg = /argument is a 'String' but should be a '#{parent_model.name}/
275
+
276
+ expect do
277
+ parent_mapper.to_toml("")
278
+ end.to raise_error(Lutaml::Model::IncorrectModelError, msg)
279
+ end
280
+
281
+ it "with correct model converts objects to toml" do
282
+ instance = parent_mapper.from_toml(input_toml)
283
+
284
+ expect(parent_mapper.to_toml(instance)).to eq(input_toml)
285
+ end
286
+ end
287
+ end
288
+
289
+ context "with XML mapping" do
290
+ let(:input_xml) do
291
+ <<~XML
292
+ <CustomModelParent>
293
+ <first_name>John</first_name>
294
+ <last_name>Doe</last_name>
295
+ <CustomModelChild>
296
+ <street>Oxford Street</street>
297
+ <city>London</city>
298
+ </CustomModelChild>
299
+ </CustomModelParent>
300
+ XML
301
+ end
302
+
303
+ describe ".from_xml" do
304
+ it "maps XML content to custom model using custom methods" do
305
+ instance = parent_mapper.from_xml(input_xml)
306
+
307
+ expect(instance.class).to eq(parent_model)
308
+ expect(instance.first_name).to eq("John")
309
+ expect(instance.last_name).to eq("Doe")
310
+ expect(instance.name).to eq("John Doe")
311
+
312
+ expect(instance.child_mapper.class).to eq(child_model)
313
+ expect(instance.child_mapper.street).to eq("Oxford Street")
314
+ expect(instance.child_mapper.city).to eq("London")
315
+ end
316
+ end
317
+
318
+ describe ".to_xml" do
319
+ it "with wrong model raises an exception" do
320
+ msg = /argument is a 'String' but should be a '#{parent_model.name}/
321
+
322
+ expect do
323
+ parent_mapper.to_xml("")
324
+ end.to raise_error(Lutaml::Model::IncorrectModelError, msg)
325
+ end
326
+
327
+ it "with correct model converts objects to xml using custom methods" do
328
+ instance = parent_mapper.from_xml(input_xml)
329
+ result_xml = parent_mapper.to_xml(instance)
330
+
331
+ expect(result_xml.gsub(/\s+/, "")).to eq(input_xml.gsub(/\s+/, ""))
332
+ end
333
+ end
334
+
335
+ describe "custom serialization methods" do
336
+ it "uses custom to_xml method for child_mapper" do
337
+ instance = parent_model.new
338
+ instance.first_name = "John"
339
+ instance.last_name = "Doe"
340
+ instance.child_mapper = child_model.new
341
+ instance.child_mapper.street = "Custom Street"
342
+ instance.child_mapper.city = "Custom City"
343
+
344
+ result_xml = parent_mapper.to_xml(instance)
345
+ expect(result_xml).to include("<CustomModelChild>")
346
+ expect(result_xml).to include("<street>Custom Street</street>")
347
+ expect(result_xml).to include("<city>Custom City</city>")
348
+ end
349
+
350
+ it "uses custom from_xml method for child_mapper" do
351
+ custom_xml = <<~XML
352
+ <CustomModelParent>
353
+ <first_name>Jane</first_name>
354
+ <last_name>Smith</last_name>
355
+ <CustomModelChild>
356
+ <street>Custom Avenue</street>
357
+ <city>New City</city>
358
+ </CustomModelChild>
359
+ </CustomModelParent>
360
+ XML
361
+
362
+ instance = parent_mapper.from_xml(custom_xml)
363
+ expect(instance.child_mapper.street).to eq("Custom Avenue")
364
+ expect(instance.child_mapper.city).to eq("New City")
365
+ end
366
+
367
+ it "uses delegate to for child mapper class" do
368
+ xml = "<docid>Str<sub>2</sub>text<sup>1</sup>123</docid>"
369
+
370
+ docid = CustomModelSpecs::Docid.from_xml(xml)
371
+
372
+ expect(CustomModelSpecs::Docid.to_xml(docid)).to eq(xml)
373
+ end
374
+ end
375
+
376
+ describe "custom methods with custom model" do
377
+ it "uses delegate to for child mapper class" do
378
+ xml = <<~XML
379
+ <MixedWithNestedContent>
380
+ <street>
381
+ A <p>b</p> B <p>c</p> C
382
+ </street>
383
+ <bibdata type="collection" schema-version="v1.2.8">
384
+ <title language="en">
385
+ JCGM Collection 1
386
+ </title>
387
+ </bibdata>
388
+ </MixedWithNestedContent>
389
+ XML
390
+
391
+ expected_xml = <<~XML
392
+ <MixedWithNestedContent>
393
+ <street>
394
+ A &lt;p&gt;b&lt;/p&gt; B &lt;p&gt;c&lt;/p&gt; C
395
+ </street>
396
+ <bibdata type="collection" schema-version="v1.2.8">
397
+ <title language="en">
398
+ JCGM Collection 1
399
+ </title>
400
+ </bibdata>
401
+ </MixedWithNestedContent>
402
+ XML
403
+
404
+ bibdata = CustomModelSpecs::MixedWithNestedContent.from_xml(xml)
405
+
406
+ expect(bibdata.to_xml).to be_equivalent_to(expected_xml)
407
+ end
408
+ end
409
+ end
410
+ end
@@ -0,0 +1,170 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+
4
+ class CustomSerialization < Lutaml::Model::Serializable
5
+ attribute :full_name, :string
6
+ attribute :size, :integer
7
+ attribute :color, :string
8
+ attribute :description, :string
9
+
10
+ json do
11
+ map "name", with: { to: :name_to_json, from: :name_from_json }
12
+ map "color", with: { to: :color_to_json, from: :color_from_json }
13
+ map "size", with: { to: :size_to_json, from: :size_from_json }
14
+ map "description",
15
+ with: { to: :description_to_json, from: :description_from_json }
16
+ end
17
+
18
+ xml do
19
+ root "CustomSerialization"
20
+
21
+ # name, color are used to test XML elements with custom methods
22
+ map_element "Name", with: { to: :name_to_xml, from: :name_from_xml }
23
+ map_element "Color", with: { to: :color_to_xml, from: :color_from_xml }
24
+
25
+ # size is used to test XML attribute with custom methods
26
+ map_attribute "Size", with: { to: :size_to_xml, from: :size_from_xml }
27
+
28
+ # description is used to test XML textual content
29
+ map_content with: { to: :description_to_xml, from: :description_from_xml }
30
+ end
31
+
32
+ def name_to_json(model, doc)
33
+ doc["name"] = "JSON Masterpiece: #{model.full_name}"
34
+ end
35
+
36
+ def name_from_json(model, value)
37
+ model.full_name = value.sub(/^JSON Masterpiece: /, "")
38
+ end
39
+
40
+ def size_to_json(model, doc)
41
+ doc["size"] = model.size + 3
42
+ end
43
+
44
+ def size_from_json(model, value)
45
+ model.size = value - 3
46
+ end
47
+
48
+ def color_to_json(model, doc)
49
+ doc["color"] = model.color.upcase
50
+ end
51
+
52
+ def color_from_json(model, value)
53
+ model.color = value.downcase
54
+ end
55
+
56
+ def description_to_json(model, doc)
57
+ doc["description"] = "JSON Description: #{model.description}"
58
+ end
59
+
60
+ def description_from_json(model, value)
61
+ model.description = value.sub(/^JSON Description: /, "")
62
+ end
63
+
64
+ def name_to_xml(model, parent, doc)
65
+ el = doc.create_element("Name")
66
+ doc.add_text(el, "XML Masterpiece: #{model.full_name}")
67
+ doc.add_element(parent, el)
68
+ end
69
+
70
+ def name_from_xml(model, value)
71
+ model.full_name = value.sub(/^XML Masterpiece: /, "")
72
+ end
73
+
74
+ def size_to_xml(model, parent, doc)
75
+ doc.add_attribute(parent, "Size", model.size + 3)
76
+ end
77
+
78
+ def size_from_xml(model, value)
79
+ model.size = value.to_i - 3
80
+ end
81
+
82
+ def color_to_xml(model, parent, doc)
83
+ color_element = doc.create_element("Color")
84
+ doc.add_text(color_element, model.color.upcase)
85
+ doc.add_element(parent, color_element)
86
+ end
87
+
88
+ def color_from_xml(model, value)
89
+ model.color = value.downcase
90
+ end
91
+
92
+ def description_to_xml(model, parent, doc)
93
+ doc.add_text(parent, "XML Description: #{model.description}")
94
+ end
95
+
96
+ def description_from_xml(model, value)
97
+ model.description = value.join.strip.sub(/^XML Description: /, "")
98
+ end
99
+ end
100
+
101
+ RSpec.describe CustomSerialization do
102
+ let(:attributes) do
103
+ {
104
+ full_name: "Vase",
105
+ size: 12,
106
+ color: "blue",
107
+ description: "A beautiful ceramic vase",
108
+ }
109
+ end
110
+ let(:model) { described_class.new(attributes) }
111
+
112
+ context "with JSON serialization" do
113
+ it "serializes to JSON with custom methods" do
114
+ expected_json = {
115
+ name: "JSON Masterpiece: Vase",
116
+ color: "BLUE",
117
+ size: 15,
118
+ description: "JSON Description: A beautiful ceramic vase",
119
+ }.to_json
120
+
121
+ expect(model.to_json).to eq(expected_json)
122
+ end
123
+
124
+ it "deserializes from JSON with custom methods" do
125
+ json = {
126
+ name: "JSON Masterpiece: Vase",
127
+ color: "BLUE",
128
+ size: 15,
129
+ description: "JSON Description: A beautiful ceramic vase",
130
+ }.to_json
131
+
132
+ ceramic = described_class.from_json(json)
133
+
134
+ expect(ceramic.full_name).to eq(model.full_name)
135
+ expect(ceramic.size).to eq(model.size)
136
+ expect(ceramic.color).to eq(model.color)
137
+ expect(ceramic.description).to eq(model.description)
138
+ end
139
+ end
140
+
141
+ context "with XML serialization" do
142
+ it "serializes to XML with custom methods" do
143
+ expected_xml = <<~XML
144
+ <CustomSerialization Size="15">
145
+ <Name>XML Masterpiece: Vase</Name>
146
+ <Color>BLUE</Color>
147
+ XML Description: A beautiful ceramic vase
148
+ </CustomSerialization>
149
+ XML
150
+
151
+ expect(model.to_xml).to be_equivalent_to(expected_xml)
152
+ end
153
+
154
+ it "deserializes from XML with custom methods" do
155
+ xml = <<~XML
156
+ <CustomSerialization Size="15">
157
+ <Name>XML Masterpiece: Vase</Name>
158
+ <Color>BLUE</Color>
159
+ XML Description: A beautiful ceramic vase
160
+ </CustomSerialization>
161
+ XML
162
+
163
+ ceramic = described_class.from_xml(xml)
164
+ expect(ceramic.full_name).to eq(model.full_name)
165
+ expect(ceramic.size).to eq(model.size)
166
+ expect(ceramic.color).to eq(model.color)
167
+ expect(ceramic.description).to eq(model.description)
168
+ end
169
+ end
170
+ end