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
|
@@ -12,6 +12,8 @@ module Lutaml
|
|
|
12
12
|
# - Used by model's serialization pipeline via Transform.for(:xml)
|
|
13
13
|
#
|
|
14
14
|
class ModelTransform < ::Lutaml::Model::Transform
|
|
15
|
+
include NestedCollectionAttribute
|
|
16
|
+
|
|
15
17
|
# Performance: Frozen empty hash to reduce allocations
|
|
16
18
|
EMPTY_HASH = {}.freeze
|
|
17
19
|
|
|
@@ -361,6 +363,13 @@ module Lutaml
|
|
|
361
363
|
value = normalize_xml_value(value, rule, attr, new_opts,
|
|
362
364
|
effective_register)
|
|
363
365
|
value = rule.transform_value(attr, value, :from, :xml)
|
|
366
|
+
# An over-count is normally left for `.validate` to report. A mapped
|
|
367
|
+
# PORO has no `.validate`, so deferring there would discard the
|
|
368
|
+
# violation entirely — check it while there is still someone to tell.
|
|
369
|
+
if !instance_is_serialize && attr && !attr.collection? &&
|
|
370
|
+
!rule.content_mapping?
|
|
371
|
+
attr.valid_collection!(value, context)
|
|
372
|
+
end
|
|
364
373
|
rule.deserialize(instance, value, attributes, context)
|
|
365
374
|
|
|
366
375
|
instance.value_set_for(rule_to)
|
|
@@ -773,6 +782,10 @@ _effective_register)
|
|
|
773
782
|
rule_has_custom_method = rule.has_custom_method_for_deserialization?
|
|
774
783
|
if rule_has_custom_method || attr_type == ::Lutaml::Model::Type::Hash
|
|
775
784
|
return_child = attr_type == ::Lutaml::Model::Type::Hash || !attr.collection? if attr
|
|
785
|
+
# A custom `from:` receives the over-count in the same shape a custom
|
|
786
|
+
# `to:` already gets for a collection.
|
|
787
|
+
return children if attr && !attr.collection? && children.size > 1
|
|
788
|
+
|
|
776
789
|
return return_child ? children.first : children
|
|
777
790
|
end
|
|
778
791
|
|
|
@@ -797,12 +810,20 @@ _effective_register)
|
|
|
797
810
|
base_cast_options[:lutaml_root] = instance.lutaml_root || instance
|
|
798
811
|
base_cast_options[:resolved_type] = attr_type
|
|
799
812
|
|
|
813
|
+
nested_mapping = nested_collection_attribute_mapping(attr, attr_type, effective_register)
|
|
814
|
+
|
|
800
815
|
children.each do |child|
|
|
801
816
|
if !rule_has_custom_method &&
|
|
802
817
|
(attr_type_is_serializable ||
|
|
803
818
|
(attr&.union? && ::Lutaml::Model::Type::Union.xml_structured?(child)))
|
|
819
|
+
cast_child = if attr_type_is_serializable
|
|
820
|
+
nested_collection_attribute_node(child, nested_mapping)
|
|
821
|
+
else
|
|
822
|
+
child
|
|
823
|
+
end
|
|
824
|
+
|
|
804
825
|
# Performance: Build cast_options efficiently (dup + []= cheaper than merge)
|
|
805
|
-
cast_options = if (child_namespace_uri =
|
|
826
|
+
cast_options = if (child_namespace_uri = cast_child.namespace_uri)
|
|
806
827
|
ns_type = attr.type_with_namespace(effective_register,
|
|
807
828
|
child_namespace_uri)
|
|
808
829
|
cast_options = base_cast_options.dup
|
|
@@ -813,7 +834,7 @@ _effective_register)
|
|
|
813
834
|
base_cast_options
|
|
814
835
|
end
|
|
815
836
|
|
|
816
|
-
cast_result = attr.cast(
|
|
837
|
+
cast_result = attr.cast(cast_child, :xml, effective_register,
|
|
817
838
|
cast_options)
|
|
818
839
|
|
|
819
840
|
# Track original namespace prefix for doubly-defined namespace support.
|
|
@@ -821,10 +842,7 @@ _effective_register)
|
|
|
821
842
|
# we need to preserve which prefix was used for round-trip fidelity.
|
|
822
843
|
# Store on the PARENT model instance keyed by attribute name.
|
|
823
844
|
# Set for ALL attributes (both Serializable and non-Serializable).
|
|
824
|
-
ns_prefix =
|
|
825
|
-
child.namespace_prefix
|
|
826
|
-
child.namespace_prefix
|
|
827
|
-
end
|
|
845
|
+
ns_prefix = explicit_namespace_prefix(child)
|
|
828
846
|
if ns_prefix && instance_is_serialize
|
|
829
847
|
prefixes = instance.xml_ns_prefixes || {}
|
|
830
848
|
prefixes[attr.name] = ns_prefix
|
|
@@ -835,7 +853,7 @@ _effective_register)
|
|
|
835
853
|
# When parsing XML with alias URIs (e.g., "http://.../") against a namespace
|
|
836
854
|
# class with canonical URI (e.g., "http://.../reqif.xsd"), store the original
|
|
837
855
|
# alias URI so it can be serialized back correctly.
|
|
838
|
-
child_uri =
|
|
856
|
+
child_uri = cast_child.namespace_uri
|
|
839
857
|
if child_uri && attr_type_is_serializable
|
|
840
858
|
child_mapping = cast_result.class.mappings_for(:xml)
|
|
841
859
|
child_ns_class = child_mapping&.namespace_class
|
|
@@ -858,8 +876,10 @@ _effective_register)
|
|
|
858
876
|
# - Nil/empty: doubly-defined case -> set @__xml_namespace_prefix on child
|
|
859
877
|
# - Set: mixed content case -> don't set (child has its own namespace)
|
|
860
878
|
# Performance: Use cached parent_ns_prefix instead of re-fetching
|
|
861
|
-
|
|
862
|
-
|
|
879
|
+
cast_child_ns_prefix = nested_collection_attribute_prefix(child, cast_child, ns_prefix)
|
|
880
|
+
if attr_type_is_serializable && cast_child_ns_prefix &&
|
|
881
|
+
(parent_ns_prefix.nil? || parent_ns_prefix.to_s.empty?)
|
|
882
|
+
cast_result.xml_namespace_prefix = cast_child_ns_prefix
|
|
863
883
|
end
|
|
864
884
|
|
|
865
885
|
values << cast_result
|
|
@@ -922,6 +942,10 @@ _effective_register)
|
|
|
922
942
|
return [] if attr&.collection? && [[nil], [""]].include?(values)
|
|
923
943
|
return values if attr&.collection?
|
|
924
944
|
|
|
945
|
+
# Keep the over-count (don't collapse via .first) so it reaches the
|
|
946
|
+
# attribute slot and `.validate` can report the cardinality violation.
|
|
947
|
+
return values if values.is_a?(Array) && values.size > 1
|
|
948
|
+
|
|
925
949
|
values.is_a?(Array) ? values.first : values
|
|
926
950
|
end
|
|
927
951
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module NestedCollectionAttribute
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def nested_collection_attribute_mapping(attr, attr_type, register)
|
|
9
|
+
return unless nested_collection_attribute?(attr, attr_type)
|
|
10
|
+
|
|
11
|
+
collection_mapping = attr_type.mappings_for(:xml, register)
|
|
12
|
+
root_name = collection_mapping&.element_name ||
|
|
13
|
+
collection_mapping&.root_element
|
|
14
|
+
return if root_name.nil?
|
|
15
|
+
|
|
16
|
+
root_name = root_name.to_s
|
|
17
|
+
return if root_name.empty?
|
|
18
|
+
|
|
19
|
+
[root_name, collection_mapping]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def nested_collection_attribute_node(child, nested_collection_mapping)
|
|
23
|
+
return child unless nested_collection_mapping
|
|
24
|
+
|
|
25
|
+
root_name, collection_mapping = nested_collection_mapping
|
|
26
|
+
return child if xml_element_matches_name?(child, root_name,
|
|
27
|
+
collection_mapping)
|
|
28
|
+
|
|
29
|
+
child.element_children.find do |nested_child|
|
|
30
|
+
xml_element_matches_name?(nested_child, root_name,
|
|
31
|
+
collection_mapping)
|
|
32
|
+
end || child
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def nested_collection_attribute_prefix(child, cast_child, ns_prefix)
|
|
36
|
+
return ns_prefix if cast_child.equal?(child)
|
|
37
|
+
|
|
38
|
+
explicit_namespace_prefix(cast_child)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def nested_collection_attribute_element?(rule, value, child_element)
|
|
42
|
+
nested_collection_attribute_value?(rule, value) &&
|
|
43
|
+
rule.serialized_name != child_element.name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def nested_collection_attribute_value?(rule, value)
|
|
47
|
+
value.is_a?(::Lutaml::Model::Collection) &&
|
|
48
|
+
nested_collection_attribute_type?(rule.attribute_type) &&
|
|
49
|
+
!rule.collection?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def nested_collection_wrapper_prefix(rule, options)
|
|
53
|
+
parent_model = options[:current_model]
|
|
54
|
+
return unless parent_model.is_a?(::Lutaml::Model::Serialize)
|
|
55
|
+
|
|
56
|
+
prefix = parent_model.xml_ns_prefixes&.[](rule.attribute_name)
|
|
57
|
+
prefix unless prefix.to_s.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def explicit_namespace_prefix(element)
|
|
61
|
+
return unless element.namespace_prefix_explicit &&
|
|
62
|
+
element.namespace_prefix
|
|
63
|
+
|
|
64
|
+
element.namespace_prefix
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def nested_collection_attribute?(attr, attr_type)
|
|
68
|
+
attr&.singular? && nested_collection_attribute_type?(attr_type)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def nested_collection_attribute_type?(attr_type)
|
|
72
|
+
attr_type.is_a?(Class) &&
|
|
73
|
+
attr_type <= ::Lutaml::Model::Collection
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def xml_element_matches_name?(element, name, mapping)
|
|
77
|
+
element.unprefixed_name == name &&
|
|
78
|
+
xml_element_matches_namespace?(element.namespace_uri, mapping)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def xml_element_matches_namespace?(namespace_uri, mapping)
|
|
82
|
+
namespace_class = mapping&.namespace_class
|
|
83
|
+
mapping_uri = mapping&.namespace_uri
|
|
84
|
+
return namespace_uri.nil? unless namespace_class || mapping_uri
|
|
85
|
+
return true if mapping_uri && namespace_uri == mapping_uri
|
|
86
|
+
|
|
87
|
+
if namespace_class.respond_to?(:all_uris)
|
|
88
|
+
namespace_class.all_uris.include?(namespace_uri)
|
|
89
|
+
else
|
|
90
|
+
namespace_uri == namespace_class&.uri
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|