lutaml-model 0.8.31 → 0.8.33
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 +76 -14
- 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)
|
|
@@ -460,7 +469,31 @@ _effective_register)
|
|
|
460
469
|
end
|
|
461
470
|
end
|
|
462
471
|
|
|
463
|
-
|
|
472
|
+
# Whether no OTHER attribute rule in the mapping claims the same
|
|
473
|
+
# local name. A sole claimant is namespace-flexible on the local-
|
|
474
|
+
# name fallback: real-world documents qualify such attributes in
|
|
475
|
+
# the element's namespace, a foreign extension namespace, or not
|
|
476
|
+
# at all (OOXML w:val on w15: elements — lutaml-model#790). When
|
|
477
|
+
# a sibling claims the name, matching stays strict so each rule
|
|
478
|
+
# keeps its own namespace (#744, #758).
|
|
479
|
+
def attribute_rule_sole_local_claimant?(instance, rule,
|
|
480
|
+
effective_register)
|
|
481
|
+
klass = instance.class
|
|
482
|
+
return true unless klass.include?(Lutaml::Model::Serialize)
|
|
483
|
+
|
|
484
|
+
mapping = klass.mappings_for(:xml, effective_register)
|
|
485
|
+
return true unless mapping
|
|
486
|
+
|
|
487
|
+
expected = Array(rule.name).map(&:to_s)
|
|
488
|
+
mapping.mappings.none? do |sib|
|
|
489
|
+
next false unless sib.attribute? && !sib.equal?(rule)
|
|
490
|
+
|
|
491
|
+
Array(sib.name).map(&:to_s) == expected
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def value_for_xml_attribute(doc, rule, rule_names,
|
|
496
|
+
flexible_local: false)
|
|
464
497
|
# For attributes, rule_names may contain namespaced names, but find_attribute_value
|
|
465
498
|
# expects prefix:name or just name, so we need to convert namespaced names to prefix format.
|
|
466
499
|
#
|
|
@@ -487,7 +520,8 @@ _effective_register)
|
|
|
487
520
|
# This handles the case where the namespace prefix is not declared in the document
|
|
488
521
|
# (e.g., v:ext without xmlns:v="...").
|
|
489
522
|
if value.nil? && rule_names.any? { |rn| rn.include?(":") }
|
|
490
|
-
value = find_attribute_by_local_name(doc, rule_names
|
|
523
|
+
value = find_attribute_by_local_name(doc, rule_names,
|
|
524
|
+
flexible_local: flexible_local)
|
|
491
525
|
end
|
|
492
526
|
|
|
493
527
|
value = value&.split(rule.delimiter) if rule.delimiter
|
|
@@ -550,7 +584,8 @@ _effective_register)
|
|
|
550
584
|
# @param doc [XmlElement] the parsed XML document root
|
|
551
585
|
# @param rule_names [Array<String>] the rule names to match
|
|
552
586
|
# @return [String, nil] the attribute value or nil
|
|
553
|
-
def find_attribute_by_local_name(doc, rule_names
|
|
587
|
+
def find_attribute_by_local_name(doc, rule_names,
|
|
588
|
+
flexible_local: false)
|
|
554
589
|
rule_names.each do |rn|
|
|
555
590
|
next unless rn.include?(":")
|
|
556
591
|
|
|
@@ -570,7 +605,12 @@ _effective_register)
|
|
|
570
605
|
# namespaced sibling attribute (lutaml-model#758). An
|
|
571
606
|
# XmlAttribute with a resolved namespace has namespaced_name
|
|
572
607
|
# "URI:local", so this comparison is allocation-free.
|
|
573
|
-
if
|
|
608
|
+
if flexible_local
|
|
609
|
+
# Sole claimant: any qualification of the local name binds
|
|
610
|
+
# (element namespace, foreign extension namespace, none).
|
|
611
|
+
attr.unprefixed_name == local_name ||
|
|
612
|
+
((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name
|
|
613
|
+
elsif attr.namespace.nil?
|
|
574
614
|
next false if attr.namespace_prefix.nil? &&
|
|
575
615
|
!attr.name.include?(":")
|
|
576
616
|
|
|
@@ -621,7 +661,14 @@ _effective_register)
|
|
|
621
661
|
# return the same children again.
|
|
622
662
|
rule_names = rule_names.concat(extra_rule_names).uniq if extra_rule_names
|
|
623
663
|
|
|
624
|
-
|
|
664
|
+
if rule.attribute?
|
|
665
|
+
return value_for_xml_attribute(
|
|
666
|
+
doc, rule, rule_names,
|
|
667
|
+
flexible_local: attribute_rule_sole_local_claimant?(
|
|
668
|
+
instance, rule, effective_register
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
end
|
|
625
672
|
|
|
626
673
|
# Performance: Pre-compute type-related values used in the hot loop
|
|
627
674
|
attr_type_is_serializable = attr_type && attr_type <= ::Lutaml::Model::Serialize
|
|
@@ -773,6 +820,10 @@ _effective_register)
|
|
|
773
820
|
rule_has_custom_method = rule.has_custom_method_for_deserialization?
|
|
774
821
|
if rule_has_custom_method || attr_type == ::Lutaml::Model::Type::Hash
|
|
775
822
|
return_child = attr_type == ::Lutaml::Model::Type::Hash || !attr.collection? if attr
|
|
823
|
+
# A custom `from:` receives the over-count in the same shape a custom
|
|
824
|
+
# `to:` already gets for a collection.
|
|
825
|
+
return children if attr && !attr.collection? && children.size > 1
|
|
826
|
+
|
|
776
827
|
return return_child ? children.first : children
|
|
777
828
|
end
|
|
778
829
|
|
|
@@ -797,12 +848,20 @@ _effective_register)
|
|
|
797
848
|
base_cast_options[:lutaml_root] = instance.lutaml_root || instance
|
|
798
849
|
base_cast_options[:resolved_type] = attr_type
|
|
799
850
|
|
|
851
|
+
nested_mapping = nested_collection_attribute_mapping(attr, attr_type, effective_register)
|
|
852
|
+
|
|
800
853
|
children.each do |child|
|
|
801
854
|
if !rule_has_custom_method &&
|
|
802
855
|
(attr_type_is_serializable ||
|
|
803
856
|
(attr&.union? && ::Lutaml::Model::Type::Union.xml_structured?(child)))
|
|
857
|
+
cast_child = if attr_type_is_serializable
|
|
858
|
+
nested_collection_attribute_node(child, nested_mapping)
|
|
859
|
+
else
|
|
860
|
+
child
|
|
861
|
+
end
|
|
862
|
+
|
|
804
863
|
# Performance: Build cast_options efficiently (dup + []= cheaper than merge)
|
|
805
|
-
cast_options = if (child_namespace_uri =
|
|
864
|
+
cast_options = if (child_namespace_uri = cast_child.namespace_uri)
|
|
806
865
|
ns_type = attr.type_with_namespace(effective_register,
|
|
807
866
|
child_namespace_uri)
|
|
808
867
|
cast_options = base_cast_options.dup
|
|
@@ -813,7 +872,7 @@ _effective_register)
|
|
|
813
872
|
base_cast_options
|
|
814
873
|
end
|
|
815
874
|
|
|
816
|
-
cast_result = attr.cast(
|
|
875
|
+
cast_result = attr.cast(cast_child, :xml, effective_register,
|
|
817
876
|
cast_options)
|
|
818
877
|
|
|
819
878
|
# Track original namespace prefix for doubly-defined namespace support.
|
|
@@ -821,10 +880,7 @@ _effective_register)
|
|
|
821
880
|
# we need to preserve which prefix was used for round-trip fidelity.
|
|
822
881
|
# Store on the PARENT model instance keyed by attribute name.
|
|
823
882
|
# Set for ALL attributes (both Serializable and non-Serializable).
|
|
824
|
-
ns_prefix =
|
|
825
|
-
child.namespace_prefix
|
|
826
|
-
child.namespace_prefix
|
|
827
|
-
end
|
|
883
|
+
ns_prefix = explicit_namespace_prefix(child)
|
|
828
884
|
if ns_prefix && instance_is_serialize
|
|
829
885
|
prefixes = instance.xml_ns_prefixes || {}
|
|
830
886
|
prefixes[attr.name] = ns_prefix
|
|
@@ -835,7 +891,7 @@ _effective_register)
|
|
|
835
891
|
# When parsing XML with alias URIs (e.g., "http://.../") against a namespace
|
|
836
892
|
# class with canonical URI (e.g., "http://.../reqif.xsd"), store the original
|
|
837
893
|
# alias URI so it can be serialized back correctly.
|
|
838
|
-
child_uri =
|
|
894
|
+
child_uri = cast_child.namespace_uri
|
|
839
895
|
if child_uri && attr_type_is_serializable
|
|
840
896
|
child_mapping = cast_result.class.mappings_for(:xml)
|
|
841
897
|
child_ns_class = child_mapping&.namespace_class
|
|
@@ -858,8 +914,10 @@ _effective_register)
|
|
|
858
914
|
# - Nil/empty: doubly-defined case -> set @__xml_namespace_prefix on child
|
|
859
915
|
# - Set: mixed content case -> don't set (child has its own namespace)
|
|
860
916
|
# Performance: Use cached parent_ns_prefix instead of re-fetching
|
|
861
|
-
|
|
862
|
-
|
|
917
|
+
cast_child_ns_prefix = nested_collection_attribute_prefix(child, cast_child, ns_prefix)
|
|
918
|
+
if attr_type_is_serializable && cast_child_ns_prefix &&
|
|
919
|
+
(parent_ns_prefix.nil? || parent_ns_prefix.to_s.empty?)
|
|
920
|
+
cast_result.xml_namespace_prefix = cast_child_ns_prefix
|
|
863
921
|
end
|
|
864
922
|
|
|
865
923
|
values << cast_result
|
|
@@ -922,6 +980,10 @@ _effective_register)
|
|
|
922
980
|
return [] if attr&.collection? && [[nil], [""]].include?(values)
|
|
923
981
|
return values if attr&.collection?
|
|
924
982
|
|
|
983
|
+
# Keep the over-count (don't collapse via .first) so it reaches the
|
|
984
|
+
# attribute slot and `.validate` can report the cardinality violation.
|
|
985
|
+
return values if values.is_a?(Array) && values.size > 1
|
|
986
|
+
|
|
925
987
|
values.is_a?(Array) ? values.first : values
|
|
926
988
|
end
|
|
927
989
|
|
|
@@ -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
|