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.
- checksums.yaml +4 -4
- data/README.adoc +25 -23
- data/docs/_pages/breaking-changes.adoc +58 -0
- data/docs/_pages/collections.adoc +25 -24
- data/docs/_pages/validation.adoc +48 -1
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/transform.rb +7 -1
- data/lib/lutaml/model/attribute.rb +67 -24
- data/lib/lutaml/model/collection.rb +5 -2
- data/lib/lutaml/model/collection_handler.rb +45 -2
- data/lib/lutaml/model/serialize/enum_handling.rb +5 -1
- data/lib/lutaml/model/validation.rb +49 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +2 -0
- data/lib/lutaml/xml/model_transform.rb +33 -9
- data/lib/lutaml/xml/nested_collection_attribute.rb +95 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +396 -128
- data/lib/lutaml/xml/serialization/instance_methods.rb +2 -2
- data/lib/lutaml/xml/transformation/element_builder.rb +55 -8
- data/spec/lutaml/model/attribute_collection_spec.rb +156 -0
- data/spec/lutaml/model/attribute_spec.rb +1 -1
- data/spec/lutaml/model/cardinality_strict_spec.rb +614 -0
- data/spec/lutaml/model/cdata_spec.rb +1 -1
- data/spec/lutaml/model/collection_register_spec.rb +119 -0
- data/spec/lutaml/model/collection_single_value_coercion_spec.rb +222 -0
- data/spec/lutaml/model/root_mappings_spec.rb +10 -7
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +785 -25
- data/spec/lutaml/model/serializable_validation_spec.rb +293 -0
- data/spec/lutaml/model/value_policy_spec.rb +6 -3
- data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +12 -12
- data/spec/lutaml/model/xsd_patterns_spec.rb +29 -29
- data/spec/lutaml/model/xsd_type_validation_spec.rb +1 -1
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- 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#
|
|
270
|
-
def
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|