lutaml-model 0.8.31 → 0.8.32

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +25 -23
  3. data/docs/_pages/breaking-changes.adoc +58 -0
  4. data/docs/_pages/collections.adoc +25 -24
  5. data/docs/_pages/validation.adoc +48 -1
  6. data/lib/compat/opal/lutaml_model_boot.rb +1 -0
  7. data/lib/lutaml/key_value/transform.rb +7 -1
  8. data/lib/lutaml/model/attribute.rb +67 -24
  9. data/lib/lutaml/model/collection.rb +5 -2
  10. data/lib/lutaml/model/collection_handler.rb +45 -2
  11. data/lib/lutaml/model/serialize/enum_handling.rb +5 -1
  12. data/lib/lutaml/model/validation.rb +49 -4
  13. data/lib/lutaml/model/version.rb +1 -1
  14. data/lib/lutaml/xml/format.rb +2 -0
  15. data/lib/lutaml/xml/model_transform.rb +33 -9
  16. data/lib/lutaml/xml/nested_collection_attribute.rb +95 -0
  17. data/lib/lutaml/xml/schema/xsd_schema.rb +396 -128
  18. data/lib/lutaml/xml/serialization/instance_methods.rb +2 -2
  19. data/lib/lutaml/xml/transformation/element_builder.rb +55 -8
  20. data/spec/lutaml/model/attribute_collection_spec.rb +156 -0
  21. data/spec/lutaml/model/attribute_spec.rb +1 -1
  22. data/spec/lutaml/model/cardinality_strict_spec.rb +614 -0
  23. data/spec/lutaml/model/cdata_spec.rb +1 -1
  24. data/spec/lutaml/model/collection_register_spec.rb +119 -0
  25. data/spec/lutaml/model/collection_single_value_coercion_spec.rb +222 -0
  26. data/spec/lutaml/model/root_mappings_spec.rb +10 -7
  27. data/spec/lutaml/model/schema/xsd_schema_spec.rb +785 -25
  28. data/spec/lutaml/model/serializable_validation_spec.rb +293 -0
  29. data/spec/lutaml/model/value_policy_spec.rb +6 -3
  30. data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +12 -12
  31. data/spec/lutaml/model/xsd_patterns_spec.rb +29 -29
  32. data/spec/lutaml/model/xsd_type_validation_spec.rb +1 -1
  33. data/spec/lutaml/turtle/transform_spec.rb +2 -2
  34. metadata +5 -1
@@ -266,8 +266,8 @@ module Lutaml
266
266
  # Append XSD schema errors (validate_xml_with macro) to the
267
267
  # standard validation errors. No-op unless schemas are configured.
268
268
  #
269
- # @see Lutaml::Model::Validation#validate
270
- def validate(register: Lutaml::Model::Config.default_register)
269
+ # @see Lutaml::Model::Validation#collect_validation_errors
270
+ def collect_validation_errors(register)
271
271
  errors = super
272
272
  return errors if self.class.xml_schema_paths.empty?
273
273
 
@@ -12,6 +12,7 @@ module Lutaml
12
12
  # - Simple value serialization
13
13
  module ElementBuilder
14
14
  include ValueSerializer
15
+ include ::Lutaml::Xml::NestedCollectionAttribute
15
16
 
16
17
  # Create an element for a value
17
18
  #
@@ -249,6 +250,9 @@ child_transformation)
249
250
  prefixes = parent_model.xml_ns_prefixes
250
251
  ns_prefix = prefixes[rule.attribute_name] if prefixes
251
252
  end
253
+ ns_prefix_present = ns_prefix && !ns_prefix.empty?
254
+ nested_collection_attribute =
255
+ nested_collection_attribute_value?(rule, value)
252
256
  child_self_declared_ns = child_ns_class &&
253
257
  parent_ns_class &&
254
258
  child_ns_class != parent_ns_class
@@ -268,18 +272,24 @@ child_transformation)
268
272
  # This preserves the dual-namespace behavior (w:rPr inside m:r) while preventing
269
273
  # stale prefixes (dcterms prefix leaking to children that should use default ns).
270
274
  dual_namespace_applies = child_self_declared_ns && ns_prefix_valid
275
+ parent_prefix_applies_to_child =
276
+ !nested_collection_attribute &&
277
+ ns_prefix_present &&
278
+ !child_self_declared_ns &&
279
+ (parent_ns_class.nil? ||
280
+ (child_ns_class && child_ns_class.uri == parent_ns_class.uri) ||
281
+ dual_namespace_applies)
271
282
  # Parent has no ns OR child shares parent's URI (and child doesn't self-declare ns)
272
283
  # OR dual-namespace case where child's ns_prefix matches its namespace expectation
273
- if (parent_ns_class.nil? || (child_ns_class && child_ns_class.uri == parent_ns_class.uri) ||
274
- dual_namespace_applies) &&
275
- !child_self_declared_ns && ns_prefix && !ns_prefix.empty?
276
- value.xml_namespace_prefix = ns_prefix
277
- end
284
+ value.xml_namespace_prefix = ns_prefix if parent_prefix_applies_to_child
285
+
286
+ dual_namespace_prefix_applies =
287
+ !nested_collection_attribute &&
288
+ dual_namespace_applies &&
289
+ ns_prefix_present
278
290
  # For dual-namespace case: set @__xml_namespace_prefix on model so it can be
279
291
  # transferred to XmlElement at lines 282-289 for prefix preservation.
280
- if dual_namespace_applies && ns_prefix && !ns_prefix.empty?
281
- value.xml_namespace_prefix = ns_prefix
282
- end
292
+ value.xml_namespace_prefix = ns_prefix if dual_namespace_prefix_applies
283
293
 
284
294
  child_element = child_transformation.transform(value, child_options)
285
295
 
@@ -317,6 +327,16 @@ child_transformation)
317
327
  child_element.xml_namespace_prefix = nil
318
328
  end
319
329
 
330
+ if nested_collection_attribute_element?(rule, value, child_element)
331
+ return create_nested_collection_wrapper_element(
332
+ rule,
333
+ child_element,
334
+ options,
335
+ parent_ns_class,
336
+ parent_element_form_default,
337
+ )
338
+ end
339
+
320
340
  # Use parent's mapping name, not child's root name
321
341
  if rule.serialized_name != child_element.name
322
342
  child_element.name = rule.serialized_name
@@ -330,6 +350,33 @@ child_transformation)
330
350
  child_element
331
351
  end
332
352
 
353
+ def create_nested_collection_wrapper_element(
354
+ rule,
355
+ child_element,
356
+ options,
357
+ parent_ns_class,
358
+ parent_element_form_default
359
+ )
360
+ element_namespace_class = determine_element_namespace(
361
+ rule,
362
+ parent_ns_class,
363
+ parent_element_form_default,
364
+ )
365
+ element = ::Lutaml::Xml::DataModel::XmlElement.new(
366
+ rule.serialized_name,
367
+ element_namespace_class,
368
+ )
369
+
370
+ ns_prefix = nested_collection_wrapper_prefix(rule, options)
371
+ element.xml_namespace_prefix = ns_prefix if ns_prefix
372
+
373
+ propagate_form(element, rule)
374
+ apply_unqualified_form_default(
375
+ element, rule, parent_ns_class, parent_element_form_default
376
+ )
377
+ element.add_child(child_element)
378
+ end
379
+
333
380
  # Check if a prefix is valid for a namespace class by verifying it maps to the same URI.
334
381
  # This ensures we only use ns_prefix from @__xml_ns_prefixes when it actually matches
335
382
  # the child's expected namespace prefix.
@@ -27,6 +27,68 @@ module AttributeCollection
27
27
  title_parts.to_s
28
28
  end
29
29
  end
30
+
31
+ class NestedTitle < Lutaml::Model::Serializable
32
+ attribute :content, :string
33
+
34
+ xml do
35
+ element "artifact"
36
+ map_element "content", to: :content
37
+ end
38
+ end
39
+
40
+ class NestedTitleCollection < Lutaml::Model::Collection
41
+ instances :items, NestedTitle
42
+
43
+ xml do
44
+ element "title-group"
45
+ map_element "artifact", to: :items
46
+ end
47
+ end
48
+
49
+ class NestedBibliographicItem < Lutaml::Model::Serializable
50
+ attribute :titles, NestedTitleCollection
51
+
52
+ xml do
53
+ element "bibitem"
54
+ map_element "titles", to: :titles
55
+ end
56
+ end
57
+
58
+ class NestedTitleNamespace < Lutaml::Xml::Namespace
59
+ uri "http://example.com/nested-title"
60
+ uri_aliases "http://example.com/nested-title-alias"
61
+ prefix_default "nt"
62
+ end
63
+
64
+ class NamespacedNestedTitle < Lutaml::Model::Serializable
65
+ attribute :content, :string
66
+
67
+ xml do
68
+ element "artifact"
69
+ namespace NestedTitleNamespace
70
+ map_element "content", to: :content
71
+ end
72
+ end
73
+
74
+ class NamespacedNestedTitleCollection < Lutaml::Model::Collection
75
+ instances :items, NamespacedNestedTitle
76
+
77
+ xml do
78
+ element "title-group"
79
+ namespace NestedTitleNamespace
80
+ map_element "artifact", to: :items
81
+ end
82
+ end
83
+
84
+ class NamespacedNestedBibliographicItem < Lutaml::Model::Serializable
85
+ attribute :titles, NamespacedNestedTitleCollection
86
+
87
+ xml do
88
+ element "bibitem"
89
+ map_element "titles", to: :titles
90
+ end
91
+ end
30
92
  end
31
93
 
32
94
  RSpec.describe AttributeCollection do
@@ -101,4 +163,98 @@ RSpec.describe AttributeCollection do
101
163
  expect(bib_item.title_parts.parts).to eq(["Custom One", "Custom Two"])
102
164
  end
103
165
  end
166
+
167
+ describe "BibliographicItem with nested collection element" do
168
+ let(:nested_xml_data) do
169
+ <<~XML
170
+ <bibitem>
171
+ <titles>
172
+ <title-group>
173
+ <artifact>
174
+ <content>Title One</content>
175
+ </artifact>
176
+ <artifact>
177
+ <content>Title Two</content>
178
+ </artifact>
179
+ <artifact>
180
+ <content>Title Three</content>
181
+ </artifact>
182
+ </title-group>
183
+ </titles>
184
+ </bibitem>
185
+ XML
186
+ end
187
+
188
+ it "deserializes the collection from its nested root element" do
189
+ bib_item = AttributeCollection::NestedBibliographicItem.from_xml(
190
+ nested_xml_data,
191
+ )
192
+
193
+ expect(bib_item.titles).to be_a(AttributeCollection::NestedTitleCollection)
194
+ expect(bib_item.titles.items.map(&:content)).to eq(
195
+ ["Title One", "Title Two", "Title Three"],
196
+ )
197
+ end
198
+
199
+ it "serializes the collection under the mapped parent element" do
200
+ titles = AttributeCollection::NestedTitleCollection.new(
201
+ [
202
+ AttributeCollection::NestedTitle.new(content: "Title One"),
203
+ AttributeCollection::NestedTitle.new(content: "Title Two"),
204
+ AttributeCollection::NestedTitle.new(content: "Title Three"),
205
+ ],
206
+ )
207
+ bib_item = AttributeCollection::NestedBibliographicItem.new(
208
+ titles: titles,
209
+ )
210
+
211
+ expect(bib_item.to_xml).to be_xml_equivalent_to(nested_xml_data)
212
+ end
213
+
214
+ it "does not unwrap a matching collection root from the wrong namespace" do
215
+ xml = <<~XML
216
+ <bibitem xmlns:nt="http://example.com/nested-title"
217
+ xmlns:other="http://example.com/other-title">
218
+ <titles>
219
+ <other:title-group>
220
+ <other:artifact>
221
+ <other:content>Wrong namespace</other:content>
222
+ </other:artifact>
223
+ </other:title-group>
224
+ <nt:title-group>
225
+ <nt:artifact>
226
+ <nt:content>Right namespace</nt:content>
227
+ </nt:artifact>
228
+ </nt:title-group>
229
+ </titles>
230
+ </bibitem>
231
+ XML
232
+
233
+ bib_item = AttributeCollection::NamespacedNestedBibliographicItem.from_xml(
234
+ xml,
235
+ )
236
+
237
+ expect(bib_item.titles.items.map(&:content)).to eq(["Right namespace"])
238
+ end
239
+
240
+ it "accepts namespace aliases for the nested collection root" do
241
+ xml = <<~XML
242
+ <bibitem xmlns:alias="http://example.com/nested-title-alias">
243
+ <titles>
244
+ <alias:title-group>
245
+ <alias:artifact>
246
+ <alias:content>Alias namespace</alias:content>
247
+ </alias:artifact>
248
+ </alias:title-group>
249
+ </titles>
250
+ </bibitem>
251
+ XML
252
+
253
+ bib_item = AttributeCollection::NamespacedNestedBibliographicItem.from_xml(
254
+ xml,
255
+ )
256
+
257
+ expect(bib_item.titles.items.map(&:content)).to eq(["Alias namespace"])
258
+ end
259
+ end
104
260
  end
@@ -43,7 +43,7 @@ RSpec.describe Lutaml::Model::Attribute do
43
43
  expect { obj.image = Pathname.new("avatar.png") }
44
44
  .to change { obj.image }
45
45
  .from(nil)
46
- .to("avatar.png")
46
+ .to(["avatar.png"])
47
47
  end
48
48
 
49
49
  it "raises error if both type and method_name are not given" do