lutaml-model 0.8.30 → 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/json/adapter/document.rb +2 -0
- data/lib/lutaml/json/adapter/standard_adapter.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 +25 -1
- data/lib/lutaml/xml/mapping_rule.rb +8 -1
- data/lib/lutaml/xml/model_transform.rb +68 -12
- 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 +72 -9
- data/lib/lutaml/yaml/adapter/document.rb +2 -0
- data/lib/lutaml/yaml/adapter/standard_adapter.rb +2 -0
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +7 -3
- 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/raw_adapter_require_spec.rb +22 -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
- data/spec/lutaml/xml/cross_class_dispatch_spec.rb +79 -0
- data/spec/lutaml/xml/plain_model_accessors_spec.rb +50 -0
- data/spec/lutaml/xml/type_qualified_attribute_leniency_spec.rb +66 -0
- metadata +9 -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
|
#
|
|
@@ -140,9 +141,25 @@ parent_element_form_default)
|
|
|
140
141
|
actual_class = resolve_polymorphic_class(rule, value, is_polymorphic,
|
|
141
142
|
is_polymorphic_subtype)
|
|
142
143
|
|
|
144
|
+
# Dispatch on value's actual class when it is a Serializable that
|
|
145
|
+
# differs from the declared attribute_type. This covers ad-hoc
|
|
146
|
+
# polymorphism (caller assigns an unrelated Serializable to a typed
|
|
147
|
+
# slot) and prevents the wrong transformation being applied
|
|
148
|
+
# silently. Subsumes is_polymorphic_subtype; preserved here for
|
|
149
|
+
# the resolve_polymorphic_class call above.
|
|
150
|
+
union || is_polymorphic ||
|
|
151
|
+
(value.is_a?(Lutaml::Model::Serialize) &&
|
|
152
|
+
value.class != rule.attribute_type)
|
|
153
|
+
|
|
143
154
|
# Get transformation for the actual class. Unions resolve the member
|
|
144
155
|
# from the value's own class, like polymorphism.
|
|
145
|
-
|
|
156
|
+
# Unions and unrelated Serializables dispatch on the value's
|
|
157
|
+
# own class (subsumes the subtype case); polymorphic config
|
|
158
|
+
# keeps its resolved-class dispatch (the class_map target).
|
|
159
|
+
child_transformation = if union ||
|
|
160
|
+
(!is_polymorphic &&
|
|
161
|
+
value.is_a?(Lutaml::Model::Serialize) &&
|
|
162
|
+
value.class != rule.attribute_type)
|
|
146
163
|
value.class.transformation_for(:xml,
|
|
147
164
|
register)
|
|
148
165
|
elsif is_polymorphic || is_polymorphic_subtype
|
|
@@ -233,6 +250,9 @@ child_transformation)
|
|
|
233
250
|
prefixes = parent_model.xml_ns_prefixes
|
|
234
251
|
ns_prefix = prefixes[rule.attribute_name] if prefixes
|
|
235
252
|
end
|
|
253
|
+
ns_prefix_present = ns_prefix && !ns_prefix.empty?
|
|
254
|
+
nested_collection_attribute =
|
|
255
|
+
nested_collection_attribute_value?(rule, value)
|
|
236
256
|
child_self_declared_ns = child_ns_class &&
|
|
237
257
|
parent_ns_class &&
|
|
238
258
|
child_ns_class != parent_ns_class
|
|
@@ -252,18 +272,24 @@ child_transformation)
|
|
|
252
272
|
# This preserves the dual-namespace behavior (w:rPr inside m:r) while preventing
|
|
253
273
|
# stale prefixes (dcterms prefix leaking to children that should use default ns).
|
|
254
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)
|
|
255
282
|
# Parent has no ns OR child shares parent's URI (and child doesn't self-declare ns)
|
|
256
283
|
# OR dual-namespace case where child's ns_prefix matches its namespace expectation
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
262
290
|
# For dual-namespace case: set @__xml_namespace_prefix on model so it can be
|
|
263
291
|
# transferred to XmlElement at lines 282-289 for prefix preservation.
|
|
264
|
-
|
|
265
|
-
value.xml_namespace_prefix = ns_prefix
|
|
266
|
-
end
|
|
292
|
+
value.xml_namespace_prefix = ns_prefix if dual_namespace_prefix_applies
|
|
267
293
|
|
|
268
294
|
child_element = child_transformation.transform(value, child_options)
|
|
269
295
|
|
|
@@ -301,6 +327,16 @@ child_transformation)
|
|
|
301
327
|
child_element.xml_namespace_prefix = nil
|
|
302
328
|
end
|
|
303
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
|
+
|
|
304
340
|
# Use parent's mapping name, not child's root name
|
|
305
341
|
if rule.serialized_name != child_element.name
|
|
306
342
|
child_element.name = rule.serialized_name
|
|
@@ -314,6 +350,33 @@ child_transformation)
|
|
|
314
350
|
child_element
|
|
315
351
|
end
|
|
316
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
|
+
|
|
317
380
|
# Check if a prefix is valid for a namespace class by verifying it maps to the same URI.
|
|
318
381
|
# This ensures we only use ns_prefix from @__xml_ns_prefixes when it actually matches
|
|
319
382
|
# the child's expected namespace prefix.
|
|
@@ -36,9 +36,13 @@ module Lutaml
|
|
|
36
36
|
@attributes
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Yeptris::YAML.dump
|
|
39
|
+
# Generation must match the standard (Psych) adapter's output
|
|
40
|
+
# byte-for-byte: document separator, quoting of digit-leading
|
|
41
|
+
# strings, flow styles. The neutral Yeptris::YAML.dump is
|
|
42
|
+
# deliberately headerless, so parity goes through the
|
|
43
|
+
# Psych-compatible face of the same engine.
|
|
44
|
+
require "yeptris/psych"
|
|
45
|
+
Yeptris::Psych.dump(attributes_to_serialize)
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
48
|
end
|
|
@@ -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
|