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
@@ -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 = 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(child, :xml, effective_register,
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 = if child.namespace_prefix_explicit &&
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 = child.namespace_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
- if attr_type_is_serializable && ns_prefix && (parent_ns_prefix.nil? || parent_ns_prefix.to_s.empty?)
862
- cast_result.xml_namespace_prefix = ns_prefix
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