lutaml-model 0.4.0 → 0.5.1

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.
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