lutaml-model 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +36 -20
  3. data/README.adoc +1003 -192
  4. data/lib/lutaml/model/attribute.rb +6 -2
  5. data/lib/lutaml/model/error/collection_true_missing_error.rb +16 -0
  6. data/lib/lutaml/model/error/multiple_mappings_error.rb +6 -0
  7. data/lib/lutaml/model/error.rb +2 -0
  8. data/lib/lutaml/model/key_value_mapping.rb +25 -4
  9. data/lib/lutaml/model/key_value_mapping_rule.rb +16 -3
  10. data/lib/lutaml/model/loggable.rb +15 -0
  11. data/lib/lutaml/model/mapping_rule.rb +14 -2
  12. data/lib/lutaml/model/serialize.rb +114 -64
  13. data/lib/lutaml/model/type/decimal.rb +5 -0
  14. data/lib/lutaml/model/version.rb +1 -1
  15. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +1 -0
  16. data/lib/lutaml/model/xml_adapter/builder/oga.rb +180 -0
  17. data/lib/lutaml/model/xml_adapter/builder/ox.rb +1 -0
  18. data/lib/lutaml/model/xml_adapter/oga/document.rb +20 -0
  19. data/lib/lutaml/model/xml_adapter/oga/element.rb +117 -0
  20. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +77 -44
  21. data/lib/lutaml/model/xml_adapter/xml_document.rb +14 -12
  22. data/lib/lutaml/model/xml_mapping.rb +3 -0
  23. data/lib/lutaml/model/xml_mapping_rule.rb +13 -4
  24. data/lib/lutaml/model.rb +1 -0
  25. data/spec/address_spec.rb +1 -0
  26. data/spec/fixtures/sample_model.rb +7 -0
  27. data/spec/lutaml/model/custom_model_spec.rb +47 -1
  28. data/spec/lutaml/model/included_spec.rb +192 -0
  29. data/spec/lutaml/model/mixed_content_spec.rb +48 -32
  30. data/spec/lutaml/model/multiple_mapping_spec.rb +329 -0
  31. data/spec/lutaml/model/ordered_content_spec.rb +1 -1
  32. data/spec/lutaml/model/render_nil_spec.rb +3 -0
  33. data/spec/lutaml/model/root_mappings_spec.rb +297 -0
  34. data/spec/lutaml/model/serializable_spec.rb +42 -7
  35. data/spec/lutaml/model/type/boolean_spec.rb +62 -0
  36. data/spec/lutaml/model/with_child_mapping_spec.rb +182 -0
  37. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +11 -11
  38. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +67 -1
  39. data/spec/lutaml/model/xml_adapter_spec.rb +2 -2
  40. data/spec/lutaml/model/xml_mapping_spec.rb +32 -9
  41. data/spec/sample_model_spec.rb +114 -0
  42. metadata +12 -2
@@ -0,0 +1,329 @@
1
+ require "spec_helper"
2
+ require "lutaml/model"
3
+
4
+ module MultipleMapping
5
+ class Product < Lutaml::Model::Serializable
6
+ attribute :name, Lutaml::Model::Type::String
7
+ attribute :localized_name, Lutaml::Model::Type::String
8
+ attribute :description, Lutaml::Model::Type::String
9
+ attribute :status, Lutaml::Model::Type::String
10
+ attribute :content, Lutaml::Model::Type::String
11
+
12
+ yaml do
13
+ map ["name", "product_name"], to: :name
14
+ map ["desc", "description"], to: :description
15
+ end
16
+
17
+ json do
18
+ map ["name", "product_name"], to: :name
19
+ map ["desc", "description"], to: :description
20
+ end
21
+
22
+ toml do
23
+ map ["name", "product_name"], to: :name
24
+ map ["desc", "description"], to: :description
25
+ end
26
+
27
+ xml do
28
+ root "product"
29
+ map_element ["name", "product-name"], to: :name
30
+ map_element ["localized-name", "localized_name"], to: :localized_name
31
+ map_element ["desc", "description"], to: :description
32
+ map_attribute ["status", "product-status"], to: :status
33
+ map_content to: :content
34
+ end
35
+ end
36
+
37
+ class CustomModel < Lutaml::Model::Serializable
38
+ attribute :id, Lutaml::Model::Type::String
39
+ attribute :full_name, Lutaml::Model::Type::String
40
+ attribute :size, Lutaml::Model::Type::Integer
41
+ attribute :color, Lutaml::Model::Type::String
42
+ attribute :description, Lutaml::Model::Type::String
43
+
44
+ json do
45
+ map ["name", "custom_name"], with: { to: :name_to_json, from: :name_from_json }
46
+ map ["color", "shade"], with: { to: :color_to_json, from: :color_from_json }
47
+ map ["size", "dimension"], with: { to: :size_to_json, from: :size_from_json }
48
+ map ["desc", "description"], with: { to: :desc_to_json, from: :desc_from_json }
49
+ end
50
+
51
+ xml do
52
+ root "CustomModel"
53
+ map_attribute ["id", "identifier"], with: { to: :id_to_xml, from: :id_from_xml }
54
+ map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
55
+ map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
56
+ map_element ["size", "dimension"], with: { to: :size_to_xml, from: :size_from_xml }
57
+ map_element ["desc", "description"], with: { to: :desc_to_xml, from: :desc_from_xml }
58
+ end
59
+
60
+ # Custom methods for JSON
61
+ def name_to_json(model, doc)
62
+ doc["name"] = "JSON Model: #{model.full_name}"
63
+ end
64
+
65
+ def name_from_json(model, value)
66
+ model.full_name = value&.sub(/^JSON Model: /, "")
67
+ end
68
+
69
+ def color_to_json(model, doc)
70
+ doc["color"] = model.color.upcase
71
+ end
72
+
73
+ def color_from_json(model, value)
74
+ model.color = value&.downcase
75
+ end
76
+
77
+ def size_to_json(model, doc)
78
+ doc["size"] = model.size + 10
79
+ end
80
+
81
+ def size_from_json(model, value)
82
+ model.size = value - 10
83
+ end
84
+
85
+ def desc_to_json(model, doc)
86
+ doc["desc"] = "JSON Description: #{model.description}"
87
+ end
88
+
89
+ def desc_from_json(model, value)
90
+ model.description = value&.sub(/^JSON Description: /, "")
91
+ end
92
+
93
+ # Custom methods for XML
94
+ def id_to_xml(model, parent, doc)
95
+ doc.add_attribute(parent, "id", "XML-#{model.id}")
96
+ end
97
+
98
+ def id_from_xml(model, value)
99
+ model.id = value&.sub(/^XML-/, "")
100
+ end
101
+
102
+ def name_to_xml(model, parent, doc)
103
+ el = doc.create_element("name")
104
+ doc.add_text(el, "XML Model: #{model.full_name}")
105
+ doc.add_element(parent, el)
106
+ end
107
+
108
+ def name_from_xml(model, value)
109
+ model.full_name = value.sub(/^XML Model: /, "")
110
+ end
111
+
112
+ def color_to_xml(model, parent, doc)
113
+ el = doc.create_element("color")
114
+ doc.add_text(el, model.color.upcase)
115
+ doc.add_element(parent, el)
116
+ end
117
+
118
+ def color_from_xml(model, value)
119
+ model.color = value.downcase
120
+ end
121
+
122
+ def size_to_xml(model, parent, doc)
123
+ el = doc.create_element("size")
124
+ doc.add_text(el, (model.size + 10).to_s)
125
+ doc.add_element(parent, el)
126
+ end
127
+
128
+ def size_from_xml(model, value)
129
+ model.size = (value.to_i || 0) - 10
130
+ end
131
+
132
+ def desc_to_xml(model, parent, doc)
133
+ el = doc.create_element("desc")
134
+ doc.add_text(el, "XML Description: #{model.description}")
135
+ doc.add_element(parent, el)
136
+ end
137
+
138
+ def desc_from_xml(model, value)
139
+ model.description = value.sub(/^XML Description: /, "")
140
+ end
141
+ end
142
+ end
143
+
144
+ RSpec.describe MultipleMapping do
145
+ context "with key-value formats" do
146
+ context "with YAML format" do
147
+ let(:yaml_with_name) { "product_name: Coffee Maker\ndescription: Premium coffee maker" }
148
+ let(:yaml_with_desc) { "---\nname: Coffee Maker\ndesc: Premium coffee maker\n" }
149
+
150
+ it "handles bidirectional conversion" do
151
+ product1 = MultipleMapping::Product.from_yaml(yaml_with_name)
152
+ product2 = MultipleMapping::Product.from_yaml(yaml_with_desc)
153
+
154
+ # keys for name and description are :name and :desc respectively since
155
+ # they are first element in their respective mapping array
156
+
157
+ expected_yaml = "---\nname: Coffee Maker\ndesc: Premium coffee maker\n"
158
+ expect(product1.to_yaml).to eq(expected_yaml)
159
+ expect(product2.to_yaml).to eq(yaml_with_desc)
160
+ end
161
+ end
162
+
163
+ context "with JSON format" do
164
+ let(:json_with_name) { '{"product_name":"Coffee Maker","description":"Premium coffee maker"}' }
165
+ let(:json_with_desc) { '{"name":"Coffee Maker","desc":"Premium coffee maker"}' }
166
+
167
+ it "handles bidirectional conversion" do
168
+ product1 = MultipleMapping::Product.from_json(json_with_name)
169
+ product2 = MultipleMapping::Product.from_json(json_with_desc)
170
+
171
+ # keys for name and description are :name and :desc respectively since
172
+ # they are first element in their respective mapping array
173
+ expected_json = '{"name":"Coffee Maker","desc":"Premium coffee maker"}'
174
+
175
+ expect(product1.to_json).to eq(expected_json)
176
+ expect(product2.to_json).to eq(json_with_desc)
177
+ end
178
+ end
179
+ end
180
+
181
+ context "with XML format" do
182
+ shared_examples "xml adapter with multiple mappings" do |adapter_class|
183
+ before do
184
+ Lutaml::Model::Config.xml_adapter = adapter_class
185
+ end
186
+
187
+ around do |example|
188
+ old_adapter = Lutaml::Model::Config.xml_adapter
189
+ Lutaml::Model::Config.xml_adapter = adapter_class
190
+
191
+ example.run
192
+ ensure
193
+ Lutaml::Model::Config.xml_adapter = old_adapter
194
+ end
195
+
196
+ let(:xml_with_attributes) do
197
+ <<~XML
198
+ <product status="active">
199
+ Some content here
200
+ <name>Coffee Maker</name>
201
+ <description>Premium coffee maker</description>
202
+ </product>
203
+ XML
204
+ end
205
+
206
+ let(:xml_with_alternate_attributes) do
207
+ <<~XML
208
+ <product product-status="in-stock">
209
+ Different content
210
+ <product-name>Coffee Maker</product-name>
211
+ <desc>Premium coffee maker</desc>
212
+ </product>
213
+ XML
214
+ end
215
+
216
+ it "handles bidirectional conversion with attributes and content" do
217
+ product1 = MultipleMapping::Product.from_xml(xml_with_attributes)
218
+ product2 = MultipleMapping::Product.from_xml(xml_with_alternate_attributes)
219
+
220
+ # Key for element name is :name since it is first element in mapping array and same for status attribute
221
+ expected_xml_product1 = <<~XML
222
+ <product status="active">
223
+ <name>Coffee Maker</name>
224
+ <desc>Premium coffee maker</desc>
225
+ Some content here
226
+ </product>
227
+ XML
228
+
229
+ expected_xml_product2 = <<~XML
230
+ <product status="in-stock">
231
+ <name>Coffee Maker</name>
232
+ <desc>Premium coffee maker</desc>
233
+ Different content
234
+ </product>
235
+ XML
236
+
237
+ expect(product1.name).to eq("Coffee Maker")
238
+ expect(product1.status).to eq("active")
239
+ expect(product2.status).to eq("in-stock")
240
+
241
+ expect(product1.to_xml).to be_equivalent_to(expected_xml_product1)
242
+ expect(product2.to_xml).to be_equivalent_to(expected_xml_product2)
243
+ end
244
+ end
245
+
246
+ context "with Nokogiri adapter" do
247
+ it_behaves_like "xml adapter with multiple mappings", Lutaml::Model::XmlAdapter::NokogiriAdapter
248
+ end
249
+
250
+ context "with Ox adapter" do
251
+ it_behaves_like "xml adapter with multiple mappings", Lutaml::Model::XmlAdapter::OxAdapter
252
+ end
253
+ end
254
+
255
+ context "with CustomModel" do
256
+ context "with JSON format" do
257
+ let(:json_with_alternate) { '{"custom_name":"JSON Model: Vase","shade":"BLUE","dimension":22,"description":"JSON Description: A beautiful ceramic vase"}' }
258
+ let(:json_with_standard) { '{"name":"JSON Model: Vase","color":"BLUE","size":22,"desc":"JSON Description: A beautiful ceramic vase"}' }
259
+
260
+ it "handles bidirectional conversion with custom methods" do
261
+ model1 = MultipleMapping::CustomModel.from_json(json_with_alternate)
262
+ model2 = MultipleMapping::CustomModel.from_json(json_with_standard)
263
+
264
+ # keys are 'name', 'color', 'size', 'desc' respectively since
265
+ # they are first element in their respective mapping array
266
+ expected_json = '{"name":"JSON Model: Vase","color":"BLUE","size":22,"desc":"JSON Description: A beautiful ceramic vase"}'
267
+
268
+ expect(model1.to_json).to eq(expected_json)
269
+ expect(model2.to_json).to eq(expected_json)
270
+ end
271
+ end
272
+
273
+ context "with XML format" do
274
+ shared_examples "xml adapter with custom methods" do |_adapter_class|
275
+ before do
276
+ Lutaml::Model::Config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
277
+ end
278
+
279
+ let(:xml_with_alternate) do
280
+ <<~XML
281
+ <CustomModel identifier="123">
282
+ <custom-name>XML Model: Vase</custom-name>
283
+ <shade>BLUE</shade>
284
+ <dimension>22</dimension>
285
+ <description>XML Description: A beautiful ceramic vase</description>
286
+ </CustomModel>
287
+ XML
288
+ end
289
+
290
+ let(:xml_with_standard) do
291
+ <<~XML
292
+ <CustomModel identifier="123">
293
+ <name>XML Model: Vase</name>
294
+ <color>BLUE</color>
295
+ <size>22</size>
296
+ <desc>XML Description: A beautiful ceramic vase</desc>
297
+ </CustomModel>
298
+ XML
299
+ end
300
+
301
+ it "handles bidirectional conversion with custom methods" do
302
+ model1 = MultipleMapping::CustomModel.from_xml(xml_with_alternate)
303
+ model2 = MultipleMapping::CustomModel.from_xml(xml_with_standard)
304
+
305
+ # Element names are 'name', 'color', 'size', 'desc' respectively since
306
+ # they are first element in their respective mapping array
307
+ expected_xml = <<~XML
308
+ <CustomModel id="XML-123">
309
+ <name>XML Model: Vase</name>
310
+ <color>BLUE</color>
311
+ <size>22</size>
312
+ <desc>XML Description: A beautiful ceramic vase</desc>
313
+ </CustomModel>
314
+ XML
315
+ expect(model1.to_xml).to be_equivalent_to(expected_xml)
316
+ expect(model2.to_xml).to be_equivalent_to(expected_xml)
317
+ end
318
+ end
319
+
320
+ context "with Nokogiri adapter" do
321
+ it_behaves_like "xml adapter with custom methods", Lutaml::Model::XmlAdapter::NokogiriAdapter
322
+ end
323
+
324
+ context "with Ox adapter" do
325
+ it_behaves_like "xml adapter with custom methods", Lutaml::Model::XmlAdapter::OxAdapter
326
+ end
327
+ end
328
+ end
329
+ end
@@ -77,7 +77,7 @@ RSpec.describe "OrderedContent" do
77
77
  it_behaves_like "ordered content behavior", described_class
78
78
  end
79
79
 
80
- describe Lutaml::Model::XmlAdapter::OgaAdapter, skip: "Not implemented yet" do
80
+ describe Lutaml::Model::XmlAdapter::OgaAdapter do
81
81
  it_behaves_like "ordered content behavior", described_class
82
82
  end
83
83
  end
@@ -42,6 +42,7 @@ class RenderNil < Lutaml::Model::Serializable
42
42
  map "clay_type", to: :clay_type, render_nil: false
43
43
  map "glaze", to: :glaze, render_nil: true
44
44
  map "dimensions", to: :dimensions, render_nil: false
45
+ map "render_nil_nested", to: :render_nil_nested, render_nil: false
45
46
  end
46
47
 
47
48
  toml do
@@ -59,8 +60,10 @@ RSpec.describe RenderNil do
59
60
  clay_type: nil,
60
61
  glaze: nil,
61
62
  dimensions: nil,
63
+ render_nil_nested: RenderNilNested.new,
62
64
  }
63
65
  end
66
+
64
67
  let(:model) { described_class.new(attributes) }
65
68
 
66
69
  it "serializes to JSON with render_nil option" do
@@ -0,0 +1,297 @@
1
+ module RootMappingSpec
2
+ class CeramicDetails < Lutaml::Model::Serializable
3
+ attribute :name, :string
4
+ attribute :insignia, :string
5
+
6
+ key_value do
7
+ map "name", to: :name
8
+ map "insignia", to: :insignia
9
+ end
10
+ end
11
+
12
+ class CeramicWithDetails < Lutaml::Model::Serializable
13
+ attribute :ceramic_id, :string
14
+ attribute :ceramic_type, :string
15
+ attribute :ceramic_details, CeramicDetails
16
+ attribute :ceramic_urn, :string
17
+
18
+ key_value do
19
+ map "id", to: :ceramic_id
20
+ map "type", to: :ceramic_type
21
+ map "details", to: :ceramic_details
22
+ map "urn", to: :ceramic_urn
23
+ end
24
+ end
25
+
26
+ class CeramicCollectionWithKeyAndValue < Lutaml::Model::Serializable
27
+ attribute :ceramics, CeramicWithDetails, collection: true
28
+
29
+ key_value do
30
+ map to: :ceramics,
31
+ root_mappings: {
32
+ ceramic_id: :key,
33
+ ceramic_details: :value,
34
+ }
35
+ end
36
+ end
37
+
38
+ class CeramicCollectionWithKeyAndComplexValue < Lutaml::Model::Serializable
39
+ attribute :ceramics, CeramicWithDetails, collection: true
40
+
41
+ key_value do
42
+ map to: :ceramics,
43
+ root_mappings: {
44
+ ceramic_id: :key,
45
+ ceramic_type: :type,
46
+ ceramic_details: "details",
47
+ ceramic_urn: ["urn", "primary"],
48
+ }
49
+ end
50
+ end
51
+
52
+ class Ceramic < Lutaml::Model::Serializable
53
+ attribute :ceramic_id, :string
54
+ attribute :ceramic_name, :string
55
+
56
+ key_value do
57
+ map "id", to: :ceramic_id
58
+ map "name", to: :ceramic_name
59
+ end
60
+ end
61
+
62
+ class CeramicCollectionWithKeyOnly < Lutaml::Model::Serializable
63
+ attribute :ceramics, Ceramic, collection: true
64
+
65
+ key_value do
66
+ map to: :ceramics, root_mappings: { ceramic_id: :key }
67
+ end
68
+ end
69
+
70
+ class CeramicCollectionWithoutCollectionTrue < Lutaml::Model::Serializable
71
+ attribute :ceramics, Ceramic
72
+
73
+ key_value do
74
+ map to: :ceramics, root_mappings: { ceramic_id: :key }
75
+ end
76
+ end
77
+ end
78
+
79
+ RSpec.describe "RootMapping" do
80
+ shared_examples "having root mappings" do |format|
81
+ let(:adapter) do
82
+ Lutaml::Model::Config.public_send(:"#{format}_adapter")
83
+ end
84
+
85
+ let(:input) do
86
+ adapter.new(input_hash).public_send(:"to_#{format}")
87
+ end
88
+
89
+ # 1. Only map to `:key`. Then only override key, the rest of the mappings stay.
90
+ context "when only `key` is mapped" do
91
+ let(:parsed) do
92
+ RootMappingSpec::CeramicCollectionWithKeyOnly.public_send(:"from_#{format}", input)
93
+ end
94
+
95
+ let(:input_hash) do
96
+ {
97
+ "vase1" => { "name" => "Imperial Vase" },
98
+ "bowl2" => { "name" => "18th Century Bowl" },
99
+ }
100
+ end
101
+
102
+ let(:ceramic_vase) do
103
+ RootMappingSpec::Ceramic.new(
104
+ ceramic_id: "vase1",
105
+ ceramic_name: "Imperial Vase",
106
+ )
107
+ end
108
+
109
+ let(:ceramic_bowl) do
110
+ RootMappingSpec::Ceramic.new(
111
+ ceramic_id: "bowl2",
112
+ ceramic_name: "18th Century Bowl",
113
+ )
114
+ end
115
+
116
+ it "parses" do
117
+ expect(parsed.ceramics.count).to eq(2)
118
+ # Because Tomlib reverses the order of the hash, so can not check based on position
119
+ expect(parsed.ceramics).to include(ceramic_vase)
120
+ expect(parsed.ceramics).to include(ceramic_bowl)
121
+ end
122
+
123
+ describe "serialize" do
124
+ let(:collection) do
125
+ RootMappingSpec::CeramicCollectionWithKeyOnly.new(ceramics: [
126
+ ceramic_vase,
127
+ ceramic_bowl,
128
+ ])
129
+ end
130
+
131
+ it "serializes correctly" do
132
+ expect(collection.public_send(:"to_#{format}")).to eq(input)
133
+ end
134
+ end
135
+ end
136
+
137
+ # 2. Maps `:key` and another attribute, then we override all the other mappings (clean slate)
138
+ context "when `key` and `value` are mapped" do
139
+ let(:parsed) do
140
+ RootMappingSpec::CeramicCollectionWithKeyAndValue.public_send(:"from_#{format}", input)
141
+ end
142
+
143
+ let(:input_hash) do
144
+ {
145
+ "vase1" => { "name" => "Imperial Vase", "insignia" => "Tang Tianbao" },
146
+ "bowl2" => { "name" => "18th Century Bowl", "insignia" => "Ming Wanli" },
147
+ }
148
+ end
149
+
150
+ let(:vase_with_details) do
151
+ RootMappingSpec::CeramicWithDetails.new(
152
+ ceramic_id: "vase1",
153
+ ceramic_details: RootMappingSpec::CeramicDetails.new(
154
+ name: "Imperial Vase",
155
+ insignia: "Tang Tianbao",
156
+ ),
157
+ )
158
+ end
159
+
160
+ let(:bowl_with_details) do
161
+ RootMappingSpec::CeramicWithDetails.new(
162
+ ceramic_id: "bowl2",
163
+ ceramic_details: RootMappingSpec::CeramicDetails.new(
164
+ name: "18th Century Bowl",
165
+ insignia: "Ming Wanli",
166
+ ),
167
+ )
168
+ end
169
+
170
+ it "parses" do
171
+ expect(parsed.ceramics.count).to eq(2)
172
+ # Because Tomlib reverses the order of the hash, so can not check based on position
173
+ expect(parsed.ceramics).to include(vase_with_details)
174
+ expect(parsed.ceramics).to include(bowl_with_details)
175
+ end
176
+
177
+ describe "serialize" do
178
+ let(:collection) do
179
+ RootMappingSpec::CeramicCollectionWithKeyAndValue.new(ceramics: [
180
+ vase_with_details,
181
+ bowl_with_details,
182
+ ])
183
+ end
184
+
185
+ it "serializes correctly" do
186
+ expect(collection.public_send(:"to_#{format}")).to eq(input)
187
+ end
188
+ end
189
+ end
190
+
191
+ # 3. Maps `:key` and `:value`, then we map the key and the value body to the new mappings.
192
+ context "when `key` and complex value structure is mapped" do
193
+ let(:parsed) do
194
+ RootMappingSpec::CeramicCollectionWithKeyAndComplexValue.public_send(:"from_#{format}", input)
195
+ end
196
+
197
+ let(:input_hash) do
198
+ {
199
+ "vase1" => {
200
+ "type" => "vase",
201
+ "details" => {
202
+ "name" => "Imperial Vase",
203
+ "insignia" => "Tang Tianbao",
204
+ },
205
+ "urn" => {
206
+ "primary" => "urn:ceramic:vase:vase1",
207
+ },
208
+ },
209
+ "bowl2" => {
210
+ "type" => "bowl",
211
+ "details" => {
212
+ "name" => "18th Century Bowl",
213
+ "insignia" => "Ming Wanli",
214
+ },
215
+ "urn" => {
216
+ "primary" => "urn:ceramic:bowl:bowl2",
217
+ },
218
+ },
219
+ }
220
+ end
221
+
222
+ let(:vase_with_details) do
223
+ RootMappingSpec::CeramicWithDetails.new(
224
+ ceramic_id: "vase1",
225
+ ceramic_type: "vase",
226
+ ceramic_urn: "urn:ceramic:vase:vase1",
227
+ ceramic_details: RootMappingSpec::CeramicDetails.new(
228
+ name: "Imperial Vase",
229
+ insignia: "Tang Tianbao",
230
+ ),
231
+ )
232
+ end
233
+
234
+ let(:bowl_with_details) do
235
+ RootMappingSpec::CeramicWithDetails.new(
236
+ ceramic_id: "bowl2",
237
+ ceramic_type: "bowl",
238
+ ceramic_urn: "urn:ceramic:bowl:bowl2",
239
+ ceramic_details: RootMappingSpec::CeramicDetails.new(
240
+ name: "18th Century Bowl",
241
+ insignia: "Ming Wanli",
242
+ ),
243
+ )
244
+ end
245
+
246
+ it "parses" do
247
+ expect(parsed.ceramics.count).to eq(2)
248
+ # Because Tomlib reverses the order of the hash, so can not check based on position
249
+ expect(parsed.ceramics).to include(vase_with_details)
250
+ expect(parsed.ceramics).to include(bowl_with_details)
251
+ end
252
+
253
+ describe "serialize from object" do
254
+ let(:collection) do
255
+ RootMappingSpec::CeramicCollectionWithKeyAndComplexValue.new(ceramics: [
256
+ vase_with_details,
257
+ bowl_with_details,
258
+ ])
259
+ end
260
+
261
+ it "serializes correctly" do
262
+ expect(collection.public_send(:"to_#{format}")).to eq(input)
263
+ end
264
+ end
265
+ end
266
+
267
+ context "when `collection: true` is missing" do
268
+ let(:input_hash) do
269
+ {
270
+ "vase1" => { "name" => "Imperial Vase" },
271
+ "bowl2" => { "name" => "18th Century Bowl" },
272
+ }
273
+ end
274
+
275
+ it "raises error" do
276
+ expect do
277
+ RootMappingSpec::CeramicCollectionWithoutCollectionTrue.public_send(:"from_#{format}", input)
278
+ end.to raise_error(
279
+ Lutaml::Model::CollectionTrueMissingError,
280
+ "May be `collection: true` is missing for `ceramics` in RootMappingSpec::CeramicCollectionWithoutCollectionTrue",
281
+ )
282
+ end
283
+ end
284
+ end
285
+
286
+ describe Lutaml::Model::YamlAdapter::StandardYamlAdapter do
287
+ it_behaves_like "having root mappings", :yaml
288
+ end
289
+
290
+ describe Lutaml::Model::JsonAdapter::StandardJsonAdapter do
291
+ it_behaves_like "having root mappings", :json
292
+ end
293
+
294
+ describe Lutaml::Model::TomlAdapter::TomlRbAdapter do
295
+ it_behaves_like "having root mappings", :toml
296
+ end
297
+ end