lutaml-model 0.6.7 → 0.7.2

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos-todo.json +7 -0
  3. data/.github/workflows/dependent-repos.json +17 -9
  4. data/.rubocop.yml +1 -1
  5. data/.rubocop_todo.yml +51 -65
  6. data/Gemfile +4 -1
  7. data/README.adoc +5083 -2612
  8. data/RELEASE_NOTES.adoc +346 -0
  9. data/docs/custom_adapters.adoc +144 -0
  10. data/lib/lutaml/model/attribute.rb +101 -16
  11. data/lib/lutaml/model/choice.rb +7 -0
  12. data/lib/lutaml/model/comparable_model.rb +48 -9
  13. data/lib/lutaml/model/config.rb +48 -42
  14. data/lib/lutaml/model/error/collection_count_out_of_range_error.rb +1 -1
  15. data/lib/lutaml/model/error/polymorphic_error.rb +14 -0
  16. data/lib/lutaml/model/error.rb +1 -0
  17. data/lib/lutaml/model/format_registry.rb +41 -0
  18. data/lib/lutaml/model/hash/document.rb +11 -0
  19. data/lib/lutaml/model/hash/mapping.rb +19 -0
  20. data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
  21. data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
  22. data/lib/lutaml/model/hash/transform.rb +8 -0
  23. data/lib/lutaml/model/hash.rb +21 -0
  24. data/lib/lutaml/model/json/document.rb +11 -0
  25. data/lib/lutaml/model/json/mapping.rb +19 -0
  26. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  27. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  28. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  29. data/lib/lutaml/model/json/transform.rb +8 -0
  30. data/lib/lutaml/model/json.rb +21 -0
  31. data/lib/lutaml/model/key_value_document.rb +27 -0
  32. data/lib/lutaml/model/{key_value_mapping.rb → mapping/key_value_mapping.rb} +64 -16
  33. data/lib/lutaml/model/{key_value_mapping_rule.rb → mapping/key_value_mapping_rule.rb} +18 -2
  34. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  35. data/lib/lutaml/model/mapping/mapping_rule.rb +300 -0
  36. data/lib/lutaml/model/schema/xml_compiler.rb +15 -15
  37. data/lib/lutaml/model/sequence.rb +2 -2
  38. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  39. data/lib/lutaml/model/serialize.rb +219 -444
  40. data/lib/lutaml/model/services/logger.rb +54 -0
  41. data/lib/lutaml/model/services/transformer.rb +48 -0
  42. data/lib/lutaml/model/services.rb +2 -0
  43. data/lib/lutaml/model/toml/document.rb +11 -0
  44. data/lib/lutaml/model/toml/mapping.rb +27 -0
  45. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  46. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  47. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  48. data/lib/lutaml/model/toml/transform.rb +8 -0
  49. data/lib/lutaml/model/toml.rb +30 -0
  50. data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
  51. data/lib/lutaml/model/transform/xml_transform.rb +239 -0
  52. data/lib/lutaml/model/transform.rb +78 -0
  53. data/lib/lutaml/model/type/date.rb +1 -1
  54. data/lib/lutaml/model/type/date_time.rb +2 -2
  55. data/lib/lutaml/model/type/hash.rb +1 -1
  56. data/lib/lutaml/model/type/time.rb +2 -2
  57. data/lib/lutaml/model/type/time_without_date.rb +2 -2
  58. data/lib/lutaml/model/type/value.rb +6 -9
  59. data/lib/lutaml/model/uninitialized_class.rb +64 -0
  60. data/lib/lutaml/model/utils.rb +44 -0
  61. data/lib/lutaml/model/validation.rb +1 -0
  62. data/lib/lutaml/model/version.rb +1 -1
  63. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  64. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  65. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  66. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +41 -21
  67. data/lib/lutaml/model/xml/element.rb +32 -0
  68. data/lib/lutaml/model/xml/mapping.rb +410 -0
  69. data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
  70. data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
  71. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  72. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
  73. data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
  74. data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
  75. data/lib/lutaml/model/xml/transform.rb +8 -0
  76. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  77. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +23 -10
  78. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  79. data/lib/lutaml/model/xml.rb +31 -0
  80. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  81. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  82. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  83. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  84. data/lib/lutaml/model/yaml/document.rb +10 -0
  85. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  86. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  87. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  88. data/lib/lutaml/model/yaml/transform.rb +8 -0
  89. data/lib/lutaml/model/yaml.rb +21 -0
  90. data/lib/lutaml/model.rb +40 -4
  91. data/lutaml-model.gemspec +0 -4
  92. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +7 -7
  93. data/spec/fixtures/person.rb +5 -5
  94. data/spec/lutaml/model/attribute_spec.rb +37 -1
  95. data/spec/lutaml/model/cdata_spec.rb +9 -9
  96. data/spec/lutaml/model/collection_spec.rb +50 -2
  97. data/spec/lutaml/model/comparable_model_spec.rb +92 -27
  98. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  99. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  100. data/spec/lutaml/model/defaults_spec.rb +1 -1
  101. data/spec/lutaml/model/enum_spec.rb +1 -1
  102. data/spec/lutaml/model/group_spec.rb +333 -20
  103. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  104. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  105. data/spec/lutaml/model/key_value_mapping_spec.rb +65 -3
  106. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  107. data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
  108. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  109. data/spec/lutaml/model/polymorphic_spec.rb +526 -0
  110. data/spec/lutaml/model/render_empty_spec.rb +194 -0
  111. data/spec/lutaml/model/render_nil_spec.rb +206 -22
  112. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  113. data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
  114. data/spec/lutaml/model/serializable_spec.rb +179 -103
  115. data/spec/lutaml/model/simple_model_spec.rb +9 -9
  116. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  117. data/spec/lutaml/model/toml_spec.rb +51 -0
  118. data/spec/lutaml/model/transformation_spec.rb +72 -15
  119. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  120. data/spec/lutaml/model/value_map_spec.rb +240 -0
  121. data/spec/lutaml/model/xml/namespace/nested_with_explicit_namespace_spec.rb +85 -0
  122. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  123. data/spec/lutaml/model/xml/xml_element_spec.rb +93 -0
  124. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  125. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  126. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  127. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  128. data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
  129. data/spec/lutaml/model/xml_mapping_rule_spec.rb +105 -5
  130. data/spec/lutaml/model/xml_mapping_spec.rb +70 -16
  131. data/spec/lutaml/model/xml_spec.rb +63 -0
  132. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  133. data/spec/sample_model_spec.rb +3 -3
  134. data/spec/spec_helper.rb +3 -3
  135. metadata +76 -59
  136. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  137. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  138. data/lib/lutaml/model/loggable.rb +0 -15
  139. data/lib/lutaml/model/mapping_rule.rb +0 -109
  140. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  141. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  142. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  143. data/lib/lutaml/model/toml_adapter.rb +0 -6
  144. data/lib/lutaml/model/xml_mapping.rb +0 -307
  145. data/lib/lutaml/model/xml_mapping_rule.rb +0 -122
  146. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  147. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  148. data/lib/lutaml/model/yaml_adapter.rb +0 -8
@@ -143,7 +143,7 @@ RSpec.describe DefaultsSpec::Glaze do
143
143
  expect(default_model.temperature).to eq(1050)
144
144
  expect(default_model.firing_time).to eq(60)
145
145
  expect(default_model.balance).to eq(BigDecimal("0.0"))
146
- expect(default_model.tags).to eq([])
146
+ expect(default_model.tags).to be_nil
147
147
  expect(default_model.properties).to eq({ food_safe: true })
148
148
  expect(default_model.status).to eq("active")
149
149
  expect(default_model.batch_number).to eq(0)
@@ -5,7 +5,7 @@ module EnumSpec
5
5
  class WithEnum < Lutaml::Model::Serializable
6
6
  attribute :without_enum, :string
7
7
  attribute :single_value, :string, values: %w[user admin super_admin]
8
- attribute :multi_value, :string, values: %w[singular dual plural], collection: true
8
+ attribute :multi_value, :string, values: %w[singular dual plural], collection: true, initialize_empty: true
9
9
  end
10
10
  end
11
11
 
@@ -3,14 +3,19 @@ require "lutaml/model"
3
3
 
4
4
  module GroupSpec
5
5
  class Ceramic < Lutaml::Model::Serializable
6
- attribute :type, :string
7
- attribute :name, :string
6
+ attribute :type, :string, default: "Data"
7
+ attribute :name, :string, default: "Starc"
8
8
 
9
9
  xml do
10
10
  no_root
11
11
  map_element :type, to: :type
12
12
  map_element :name, to: :name
13
13
  end
14
+
15
+ key_value do
16
+ map :type, to: :type
17
+ map :name, to: :name
18
+ end
14
19
  end
15
20
 
16
21
  class CeramicCollection < Lutaml::Model::Serializable
@@ -75,9 +80,139 @@ module GroupSpec
75
80
  map_element :name, to: :name
76
81
  end
77
82
  end
83
+
84
+ class CommonAttributes < Lutaml::Model::Serializable
85
+ choice do
86
+ attribute :mstyle, :string
87
+ attribute :mcol, :string
88
+ attribute :mr, :string
89
+ end
90
+
91
+ xml do
92
+ no_root
93
+ sequence do
94
+ map_element :mstyle, to: :mstyle
95
+ map_element :mr, to: :mr
96
+ end
97
+ map_attribute :mcol, to: :mcol
98
+ end
99
+ end
100
+
101
+ class Mrow < Lutaml::Model::Serializable
102
+ attribute :mi, :string
103
+ import_model CommonAttributes
104
+
105
+ xml do
106
+ root "mrow"
107
+ map_element :mi, to: :mi
108
+ end
109
+
110
+ import_model GroupOfItems
111
+
112
+ key_value do
113
+ map :mcol, to: :mcol
114
+ end
115
+
116
+ import_model Ceramic
117
+ end
118
+
119
+ class Mfrac < Lutaml::Model::Serializable
120
+ attribute :num, :string
121
+ import_model CommonAttributes
122
+
123
+ xml do
124
+ root "mfrac"
125
+ map_element :num, to: :num
126
+ end
127
+ end
128
+
129
+ class ContributionInfo < Lutaml::Model::Serializable
130
+ attribute :person, :string
131
+ attribute :organization, :string
132
+
133
+ xml do
134
+ no_root
135
+ map_element "person", to: :person
136
+ map_element "organization", to: :organization
137
+ end
138
+ end
139
+
140
+ class Contributor < Lutaml::Model::Serializable
141
+ attribute :role, :string
142
+ import_model_attributes ContributionInfo
143
+
144
+ xml do
145
+ root "contributor"
146
+ map_element "role", to: :role
147
+ map_element "person", to: :person
148
+ map_element "organization", to: :organization
149
+ end
150
+ end
151
+
152
+ class Identifier < Lutaml::Model::Serializable
153
+ attribute :id, :string
154
+ attribute :remarks, :string, collection: true
155
+ attribute :remark_items, :string, collection: true
156
+
157
+ key_value do
158
+ map "id", to: :id
159
+ map "remarks", to: :remarks
160
+ map "remark_items", to: :remark_items
161
+ end
162
+ end
163
+
164
+ class ModelElement < Lutaml::Model::Serializable
165
+ import_model Identifier
166
+ end
78
167
  end
79
168
 
80
169
  RSpec.describe "Group" do
170
+ context "when serializing and deserializing import model having no_root" do
171
+ let(:xml) do
172
+ <<~XML
173
+ <mrow xmlns:ex1="http://www.example.com" xmlns:GML="http://www.sparxsystems.com/profiles/GML/1.0">
174
+ <mstyle>italic</mstyle>
175
+ <mr>y</mr>
176
+ <mi>x</mi>
177
+ <name>Smith</name>
178
+ <type>product</type>
179
+ <GML:description>Item</GML:description>
180
+ </mrow>
181
+ XML
182
+ end
183
+
184
+ let(:input_xml) do
185
+ <<~XML
186
+ <contributor>
187
+ <role>author</role>
188
+ <person>John Doe</person>
189
+ <organization>ACME</organization>
190
+ </contributor>
191
+ XML
192
+ end
193
+
194
+ it "parse the imported model correctly" do
195
+ parsed = GroupSpec::Mrow.from_xml(xml)
196
+ expect(parsed.mi).to eq("x")
197
+ expect(parsed.mstyle).to eq("italic")
198
+ expect(parsed.name).to eq("Smith")
199
+ expect(parsed.type).to eq("product")
200
+ expect(parsed.description).to eq("Item")
201
+ end
202
+
203
+ it "parse the imported model attributes correctly" do
204
+ parsed = GroupSpec::Contributor.from_xml(input_xml)
205
+ expect(parsed.person).to eq("John Doe")
206
+ expect(parsed.role).to eq("author")
207
+ expect(parsed.organization).to eq("ACME")
208
+ end
209
+
210
+ it "serialize the imported model correctly" do
211
+ instance = GroupSpec::Mrow.new(mi: "x", mstyle: "italic", mr: "y", name: "Smith", type: "product", description: "Item")
212
+ expect(instance.to_xml).to be_equivalent_to(xml)
213
+ end
214
+ end
215
+
81
216
  context "with no_root" do
82
217
  let(:mapper) { GroupSpec::CeramicCollection }
83
218
 
@@ -93,13 +228,24 @@ RSpec.describe "Group" do
93
228
  )
94
229
  end
95
230
 
96
- it "raises error if root_less class used for deserializing" do
97
- ceramic = GroupSpec::Ceramic.new(type: "Data", name: "Starc")
231
+ context "deserializing XML" do
232
+ let(:ceramic) { GroupSpec::Ceramic.new(type: "Data", name: "Starc") }
98
233
 
99
- expect { ceramic.to_xml }.to raise_error(
100
- Lutaml::Model::NoRootMappingError,
101
- "GroupSpec::Ceramic has `no_root`, it allowed only for reusable models",
102
- )
234
+ it "raises error for root_less class" do
235
+ expect { ceramic.to_xml }.to raise_error(
236
+ Lutaml::Model::NoRootMappingError,
237
+ "GroupSpec::Ceramic has `no_root`, it allowed only for reusable models",
238
+ )
239
+ end
240
+ end
241
+
242
+ context "deserializing key-value formats" do
243
+ let(:ceramic) { GroupSpec::Ceramic.new(type: "Data", name: "Starc") }
244
+
245
+ it "does not raises error for root_less class" do
246
+ expect { ceramic.to_yaml }
247
+ .not_to raise_error(Lutaml::Model::NoRootMappingError)
248
+ end
103
249
  end
104
250
 
105
251
  it "correctly get the element of root-less class" do
@@ -116,26 +262,193 @@ RSpec.describe "Group" do
116
262
  end
117
263
 
118
264
  context "with model" do
119
- it "import attributes" do
120
- expect(GroupSpec::ComplexType.attributes).to include(GroupSpec::GroupOfItems.attributes)
265
+ shared_examples "imports attributes from" do |source_class, target_class|
266
+ it "#{source_class.name} correctly" do
267
+ source_attributes = source_class.attributes
268
+ target_attributes = target_class.attributes
269
+
270
+ source_attributes.each do |name, attr|
271
+ expect(target_attributes[name].name).to eq(attr.name)
272
+ expect(target_attributes[name].type).to eq(attr.type)
273
+ expect(target_attributes[name].options).to eq(attr.options)
274
+ end
275
+ end
276
+ end
277
+
278
+ shared_examples "imports mappings from" do |source_class, target_class|
279
+ it "#{source_class.name} correctly" do
280
+ source_elements = source_class.mappings_for(:xml).elements
281
+ target_elements = target_class.mappings_for(:xml).elements
282
+
283
+ source_elements.each do |element|
284
+ matching_element = target_elements.find { |e| e.name == element.name }
285
+ expect(matching_element).not_to be_nil
286
+ expect(matching_element.to).to eq(element.to)
287
+ end
288
+ end
289
+ end
290
+
291
+ describe GroupSpec::ComplexType do
292
+ it_behaves_like "imports attributes from", GroupSpec::GroupOfItems, described_class
293
+ it_behaves_like "imports mappings from", GroupSpec::GroupOfItems, described_class
294
+ end
295
+
296
+ describe GroupSpec::GenericType do
297
+ it_behaves_like "imports mappings from", GroupSpec::GroupOfItems, described_class
298
+ end
299
+
300
+ describe GroupSpec::SimpleType do
301
+ it_behaves_like "imports attributes from", GroupSpec::GroupOfItems, described_class
302
+ it_behaves_like "imports mappings from", GroupSpec::GroupOfItems, described_class
303
+ end
304
+
305
+ describe GroupSpec::ModelElement do
306
+ it_behaves_like "imports attributes from", GroupSpec::Identifier, described_class
307
+ it_behaves_like "imports mappings from", GroupSpec::Identifier, described_class
308
+ end
309
+
310
+ describe GroupSpec::Mrow do
311
+ it_behaves_like "imports attributes from", GroupSpec::CommonAttributes, described_class
312
+ it_behaves_like "imports attributes from", GroupSpec::Ceramic, described_class
313
+ it_behaves_like "imports mappings from", GroupSpec::CommonAttributes, described_class
314
+ it_behaves_like "imports mappings from", GroupSpec::Ceramic, described_class
315
+ end
316
+
317
+ describe GroupSpec::Contributor do
318
+ it_behaves_like "imports attributes from", GroupSpec::ContributionInfo, described_class
121
319
  end
122
320
 
123
- it "import mappings in xml block" do
124
- expect(GroupSpec::ComplexType.mappings_for(:xml).elements).to include(*GroupSpec::GroupOfItems.mappings_for(:xml).elements)
321
+ context "when importing multiple models with overlapping attributes" do
322
+ let(:mrow_instance) do
323
+ GroupSpec::Mrow.new
324
+ end
325
+
326
+ it "uses Ceramic's default values for overlapping attributes" do
327
+ expect(mrow_instance.type).to eq("Data")
328
+ expect(mrow_instance.name).to eq("Starc")
329
+ end
330
+
331
+ it "maintains the correct XML serialization order from last import" do
332
+ xml = mrow_instance.to_xml
333
+ expected_xml = "<mrow xmlns:ex1='http://www.example.com' xmlns:GML='http://www.sparxsystems.com/profiles/GML/1.0'/>"
334
+
335
+ expect(xml).to be_equivalent_to(expected_xml)
336
+ end
125
337
  end
126
338
 
127
- it "import mappings outside xml block" do
128
- expect(GroupSpec::GenericType.mappings_for(:xml).elements).to include(*GroupSpec::GroupOfItems.mappings_for(:xml).elements)
339
+ context "when update the imported attribute" do
340
+ it "updates the attribute `mstyle` only in `Mrow`" do
341
+ GroupSpec::Mrow.attributes[:mstyle].instance_variable_set(:@type, :integer)
342
+ expect(GroupSpec::Mrow.attributes[:mstyle].type).to eq(:integer)
343
+ end
344
+
345
+ it "maintains original type for the attribute `mstyle` in `Mfrac`" do
346
+ expect(GroupSpec::Mfrac.attributes[:mstyle].type).to eq(Lutaml::Model::Type::String)
347
+ end
348
+
349
+ it "maintains original type for the attribute `mstyle` in importable class `CommonAttributes`" do
350
+ expect(GroupSpec::CommonAttributes.attributes[:mstyle].type).to eq(Lutaml::Model::Type::String)
351
+ end
129
352
  end
130
353
 
131
- it "import attributes and mappings in xml block" do
132
- expect(GroupSpec::ComplexType.attributes).to include(GroupSpec::GroupOfItems.attributes)
133
- expect(GroupSpec::ComplexType.mappings_for(:xml).elements).to include(*GroupSpec::GroupOfItems.mappings_for(:xml).elements)
354
+ context "when updating imported choice" do
355
+ it "updates choice min/max only in Mrow" do
356
+ choice = GroupSpec::Mrow.choice_attributes.first
357
+ choice.instance_variable_set(:@min, 2)
358
+ choice.instance_variable_set(:@max, 3)
359
+
360
+ expect(choice.min).to eq(2)
361
+ expect(choice.max).to eq(3)
362
+ end
363
+
364
+ it "maintains original choice min/max in Mfrac" do
365
+ choice = GroupSpec::Mfrac.choice_attributes.first
366
+ expect(choice.min).to eq(1)
367
+ expect(choice.max).to eq(1)
368
+ end
369
+
370
+ it "maintains original choice min/max in CommonAttributes" do
371
+ choice = GroupSpec::CommonAttributes.choice_attributes.first
372
+ expect(choice.min).to eq(1)
373
+ expect(choice.max).to eq(1)
374
+ end
134
375
  end
135
376
 
136
- it "import attributes and mappings outside the xml block" do
137
- expect(GroupSpec::SimpleType.attributes).to include(GroupSpec::GroupOfItems.attributes)
138
- expect(GroupSpec::SimpleType.mappings_for(:xml).elements).to include(*GroupSpec::GroupOfItems.mappings_for(:xml).elements)
377
+ context "when updating imported mappings" do
378
+ let(:new_namespace) { "http://www.example.com/new" }
379
+ let(:new_prefix) { "test" }
380
+
381
+ context "with element mappings" do
382
+ it "updates the mapping namespace only in `Mrow`" do
383
+ mapping = GroupSpec::Mrow.mappings_for(:xml).elements.find { |e| e.name == :mstyle }
384
+ mapping.instance_variable_set(:@namespace, new_namespace)
385
+ mapping.instance_variable_set(:@prefix, new_prefix)
386
+
387
+ expect(mapping.namespace).to eq(new_namespace)
388
+ expect(mapping.prefix).to eq(new_prefix)
389
+ end
390
+
391
+ it "maintains original namespace for `mstyle` mapping in `Mfrac`" do
392
+ mapping = GroupSpec::Mfrac.mappings_for(:xml).elements.find { |e| e.name == :mstyle }
393
+ expect(mapping.namespace).to be_nil
394
+ expect(mapping.prefix).to be_nil
395
+ end
396
+
397
+ it "maintains original namespace for `mstyle` mapping in `CommonAttributes`" do
398
+ mapping = GroupSpec::CommonAttributes.mappings_for(:xml).elements.find { |e| e.name == :mstyle }
399
+ expect(mapping.namespace).to be_nil
400
+ expect(mapping.prefix).to be_nil
401
+ end
402
+ end
403
+
404
+ context "with attribute mappings" do
405
+ it "updates attribute mapping only in `Mrow`" do
406
+ mapping = GroupSpec::Mrow.mappings_for(:xml).attributes.find { |a| a.name == :mcol }
407
+ mapping.instance_variable_set(:@namespace, new_namespace)
408
+
409
+ expect(mapping.namespace).to eq(new_namespace)
410
+ end
411
+
412
+ it "maintains original attribute mapping in `Mfrac`" do
413
+ mapping = GroupSpec::Mfrac.mappings_for(:xml).attributes.find { |a| a.name == :mcol }
414
+ expect(mapping.namespace).to be_nil
415
+ end
416
+
417
+ it "maintains original attribute mapping in `CommonAttributes`" do
418
+ mapping = GroupSpec::CommonAttributes.mappings_for(:xml).attributes.find { |a| a.name == :mcol }
419
+ expect(mapping.namespace).to be_nil
420
+ end
421
+ end
422
+
423
+ context "with sequence elements" do
424
+ it "updates sequence elements only in `Mrow`" do
425
+ sequence = GroupSpec::Mrow.mappings_for(:xml).element_sequence.first
426
+ sequence.attributes << Lutaml::Model::Xml::MappingRule.new(
427
+ "new_element",
428
+ to: :new_element,
429
+ namespace: "http://example.com",
430
+ prefix: "test",
431
+ )
432
+
433
+ expect(sequence.attributes.map(&:name)).to include("new_element")
434
+ end
435
+
436
+ it "maintains original sequence elements in `Mfrac`" do
437
+ original_sequence = GroupSpec::Mfrac.mappings_for(:xml).element_sequence.first
438
+ expect(original_sequence.attributes.map(&:name)).not_to include("new_element")
439
+ end
440
+
441
+ it "maintains original sequence elements in `CommonAttributes`" do
442
+ original_sequence = GroupSpec::CommonAttributes.mappings_for(:xml).element_sequence.first
443
+ expect(original_sequence.attributes.map(&:name)).not_to include("new_element")
444
+ end
445
+
446
+ it "creates new sequence object with the new mapping object as model of sequence in `Mrow`" do
447
+ mrow_sequence_model = GroupSpec::Mrow.mappings_for(:xml).element_sequence[0].model
448
+ common_attributes_sequence_model = GroupSpec::CommonAttributes.mappings_for(:xml).element_sequence[0].model
449
+ expect(mrow_sequence_model).not_to be(common_attributes_sequence_model)
450
+ end
451
+ end
139
452
  end
140
453
 
141
454
  it "raises error if root is defined on imported class" do
@@ -0,0 +1,255 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe "HashAdapter" do
4
+ before do
5
+ stub_const("HashMappingSpec", Module.new)
6
+
7
+ simple_class = Class.new(Lutaml::Model::Serializable) do
8
+ attribute :name, :string
9
+ attribute :age, :integer
10
+ attribute :active, :boolean
11
+
12
+ hsh do
13
+ map "name", to: :name
14
+ map "age", to: :age
15
+ map "is_active", to: :active
16
+ end
17
+ end
18
+
19
+ stub_const("HashMappingSpec::SimpleClass", simple_class)
20
+
21
+ nested_class = Class.new(Lutaml::Model::Serializable) do
22
+ attribute :title, :string
23
+ attribute :simple, HashMappingSpec::SimpleClass
24
+ attribute :items, :string, collection: true
25
+
26
+ hsh do
27
+ map "title", to: :title
28
+ map "simple", to: :simple
29
+ map "items", to: :items
30
+ end
31
+ end
32
+
33
+ stub_const("HashMappingSpec::NestedClass", nested_class)
34
+
35
+ collection_class = Class.new(Lutaml::Model::Serializable) do
36
+ attribute :name, :string
37
+ attribute :simples, HashMappingSpec::SimpleClass, collection: true
38
+
39
+ hsh do
40
+ map "name", to: :name
41
+ map "simples", to: :simples
42
+ end
43
+ end
44
+
45
+ stub_const("HashMappingSpec::CollectionClass", collection_class)
46
+ end
47
+
48
+ describe "from_hash" do
49
+ context "with simple attributes" do
50
+ let(:parsed) do
51
+ HashMappingSpec::SimpleClass.from_hash(hash_data)
52
+ end
53
+
54
+ let(:hash_data) do
55
+ {
56
+ "name" => "John Doe",
57
+ "age" => 30,
58
+ "is_active" => true,
59
+ }
60
+ end
61
+
62
+ let(:expected) do
63
+ HashMappingSpec::SimpleClass.new(
64
+ name: "John Doe",
65
+ age: 30,
66
+ active: true,
67
+ )
68
+ end
69
+
70
+ it "deserializes hash to object" do
71
+ expect(parsed).to eq(expected)
72
+ end
73
+ end
74
+
75
+ context "with nested objects" do
76
+ let(:parsed) do
77
+ HashMappingSpec::NestedClass.from_hash(hash_data)
78
+ end
79
+
80
+ let(:hash_data) do
81
+ {
82
+ "title" => "Test Title",
83
+ "simple" => {
84
+ "name" => "Jane Doe",
85
+ "age" => 25,
86
+ "is_active" => false,
87
+ },
88
+ "items" => ["item1", "item2", "item3"],
89
+ }
90
+ end
91
+
92
+ let(:expected) do
93
+ HashMappingSpec::NestedClass.new(
94
+ title: "Test Title",
95
+ simple: HashMappingSpec::SimpleClass.new(
96
+ name: "Jane Doe",
97
+ age: 25,
98
+ active: false,
99
+ ),
100
+ items: ["item1", "item2", "item3"],
101
+ )
102
+ end
103
+
104
+ it "deserializes nested objects" do
105
+ expect(parsed).to eq(expected)
106
+ end
107
+ end
108
+
109
+ context "with collections of objects" do
110
+ let(:parsed) do
111
+ HashMappingSpec::CollectionClass.from_hash(hash_data)
112
+ end
113
+
114
+ let(:hash_data) do
115
+ {
116
+ "name" => "Collection",
117
+ "simples" => [
118
+ { "name" => "First", "age" => 10, "is_active" => true },
119
+ { "name" => "Second", "age" => 20, "is_active" => false },
120
+ ],
121
+ }
122
+ end
123
+
124
+ let(:expected) do
125
+ HashMappingSpec::CollectionClass.new(
126
+ name: "Collection",
127
+ simples: [
128
+ HashMappingSpec::SimpleClass.new(
129
+ name: "First",
130
+ age: 10,
131
+ active: true,
132
+ ),
133
+ HashMappingSpec::SimpleClass.new(
134
+ name: "Second",
135
+ age: 20,
136
+ active: false,
137
+ ),
138
+ ],
139
+ )
140
+ end
141
+
142
+ it "deserializes collections of objects" do
143
+ expect(parsed).to eq(expected)
144
+ end
145
+ end
146
+ end
147
+
148
+ describe "to_hash" do
149
+ context "with simple attributes" do
150
+ let(:instance) do
151
+ HashMappingSpec::SimpleClass.new(
152
+ name: "John Doe",
153
+ age: 30,
154
+ active: true,
155
+ )
156
+ end
157
+
158
+ let(:expected_hash) do
159
+ {
160
+ "name" => "John Doe",
161
+ "age" => 30,
162
+ "is_active" => true,
163
+ }
164
+ end
165
+
166
+ it "serializes object to hash" do
167
+ expect(instance.to_hash).to eq(expected_hash)
168
+ end
169
+ end
170
+
171
+ context "with nested objects" do
172
+ let(:simple) do
173
+ HashMappingSpec::SimpleClass.new(
174
+ name: "Jane Doe",
175
+ age: 25,
176
+ active: false,
177
+ )
178
+ end
179
+
180
+ let(:instance) do
181
+ HashMappingSpec::NestedClass.new(
182
+ title: "Test Title",
183
+ simple: simple,
184
+ items: ["item1", "item2", "item3"],
185
+ )
186
+ end
187
+
188
+ let(:expected_hash) do
189
+ {
190
+ "title" => "Test Title",
191
+ "simple" => {
192
+ "name" => "Jane Doe",
193
+ "age" => 25,
194
+ "is_active" => false,
195
+ },
196
+ "items" => ["item1", "item2", "item3"],
197
+ }
198
+ end
199
+
200
+ it "serializes nested objects to hash" do
201
+ expect(instance.to_hash).to eq(expected_hash)
202
+ end
203
+ end
204
+
205
+ context "with collections of objects" do
206
+ let(:simples) do
207
+ [
208
+ HashMappingSpec::SimpleClass.new(name: "First", age: 10, active: true),
209
+ HashMappingSpec::SimpleClass.new(name: "Second", age: 20, active: false),
210
+ ]
211
+ end
212
+
213
+ let(:instance) do
214
+ HashMappingSpec::CollectionClass.new(
215
+ name: "Collection",
216
+ simples: simples,
217
+ )
218
+ end
219
+
220
+ let(:expected_hash) do
221
+ {
222
+ "name" => "Collection",
223
+ "simples" => [
224
+ { "name" => "First", "age" => 10, "is_active" => true },
225
+ { "name" => "Second", "age" => 20, "is_active" => false },
226
+ ],
227
+ }
228
+ end
229
+
230
+ it "serializes collections of objects to hash" do
231
+ expect(instance.to_hash).to eq(expected_hash)
232
+ end
233
+ end
234
+ end
235
+
236
+ describe "round-trip serialization" do
237
+ let(:original_hash) do
238
+ {
239
+ "name" => "Collection",
240
+ "simples" => [
241
+ { "name" => "First", "age" => 10, "is_active" => true },
242
+ { "name" => "Second", "age" => 20, "is_active" => false },
243
+ ],
244
+ }
245
+ end
246
+
247
+ let(:parsed) do
248
+ HashMappingSpec::CollectionClass.from_hash(original_hash)
249
+ end
250
+
251
+ it "maintains data integrity through serialization and deserialization" do
252
+ expect(parsed.to_hash).to eq(original_hash)
253
+ end
254
+ end
255
+ end