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
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -128,6 +128,8 @@ module Lutaml
|
|
|
128
128
|
autoload :TransformationBuilder, "#{__dir__}/transformation_builder"
|
|
129
129
|
autoload :AdapterLoader, "#{__dir__}/adapter_loader"
|
|
130
130
|
autoload :Element, "#{__dir__}/element"
|
|
131
|
+
autoload :NestedCollectionAttribute,
|
|
132
|
+
"#{__dir__}/nested_collection_attribute"
|
|
131
133
|
autoload :ModelTransform, "#{__dir__}/model_transform"
|
|
132
134
|
autoload :ParseSession, "#{__dir__}/parse_session"
|
|
133
135
|
autoload :TypeNamespaceResolver, "#{__dir__}/type_namespace_resolver"
|
|
@@ -239,7 +241,21 @@ Lutaml::Model::Serialize.prepend(
|
|
|
239
241
|
# ModelImportExt override (root?) must land on ClassMethods too. The other
|
|
240
242
|
# two (FormatConversion, InstanceMethods) are already prepended unconditionally
|
|
241
243
|
# above, so Opal's no-double-prepend rule means we don't repeat them here.
|
|
242
|
-
|
|
244
|
+
#
|
|
245
|
+
# TruffleRuby (34.0.1) has the same propagation gap, on both the include
|
|
246
|
+
# side and the extend/singleton side — a module prepended into an
|
|
247
|
+
# already-extended module does not reach prior extenders' singleton chains
|
|
248
|
+
# either (https://github.com/truffleruby/truffleruby/issues/4452). Without
|
|
249
|
+
# the include-side re-prepends below, Mml.parse et al. die with
|
|
250
|
+
# "undefined method 'pending_plan_root_element='". The extend side
|
|
251
|
+
# additionally needs FormatConversion re-prepended onto Serializable's
|
|
252
|
+
# singleton: otherwise `model PlainClass` runs the no-op base
|
|
253
|
+
# add_format_specific_model_methods and plain (non-Serialize) model classes
|
|
254
|
+
# never receive their XML accessors (encoding=, element_order=, ordered=,
|
|
255
|
+
# mixed, doctype, xml_declaration, raw_schema_location), so
|
|
256
|
+
# ModelTransform#apply_xml_mapping dies at `instance.encoding = ...` —
|
|
257
|
+
# e.g. every relaton ItemData parse via Relaton::Cli.parse_xml.
|
|
258
|
+
if Lutaml::Model.opal? || RUBY_ENGINE == "truffleruby"
|
|
243
259
|
Lutaml::Model::Serializable.singleton_class.prepend(
|
|
244
260
|
Lutaml::Xml::Serialization::ModelImportExt,
|
|
245
261
|
)
|
|
@@ -251,6 +267,14 @@ if Lutaml::Model.opal?
|
|
|
251
267
|
Lutaml::Model::Serialize::ClassMethods.prepend(
|
|
252
268
|
Lutaml::Xml::Serialization::ModelImportExt,
|
|
253
269
|
)
|
|
270
|
+
|
|
271
|
+
# TruffleRuby only: Opal's no-double-prepend rule makes the unconditional
|
|
272
|
+
# ClassMethods.prepend(FormatConversion) above sufficient there.
|
|
273
|
+
if RUBY_ENGINE == "truffleruby"
|
|
274
|
+
Lutaml::Model::Serializable.singleton_class.prepend(
|
|
275
|
+
Lutaml::Xml::Serialization::FormatConversion,
|
|
276
|
+
)
|
|
277
|
+
end
|
|
254
278
|
end
|
|
255
279
|
|
|
256
280
|
# Register XML-specific attribute override warning names
|
|
@@ -323,7 +323,13 @@ module Lutaml
|
|
|
323
323
|
strict_match = uri && ns_info[:prefix] &&
|
|
324
324
|
!ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
|
|
325
325
|
if strict_match
|
|
326
|
-
|
|
326
|
+
# Type-level qualification stays lenient on parse: a W3C
|
|
327
|
+
# unprefixed attribute carries no namespace, and 0.8.19 bound
|
|
328
|
+
# it to the type-qualified rule (lutaml-model#786). Only rule
|
|
329
|
+
# and schema-level qualification is strictly prefixed-only.
|
|
330
|
+
names = ["#{uri}:#{name}"]
|
|
331
|
+
names << name.to_s if ns_info[:via_type_namespace]
|
|
332
|
+
names.freeze
|
|
327
333
|
else
|
|
328
334
|
namespaced_names(default_namespace)
|
|
329
335
|
end
|
|
@@ -471,6 +477,7 @@ form_default = :unqualified)
|
|
|
471
477
|
if attr && (type_ns_class = attr.type_namespace_class(register)) &&
|
|
472
478
|
type_ns_class != :blank
|
|
473
479
|
result = build_namespace_result_from_class(type_ns_class)
|
|
480
|
+
result[:via_type_namespace] = true
|
|
474
481
|
# CRITICAL W3C FLAG: Mark when attribute is in same namespace as parent with :unqualified
|
|
475
482
|
# Serialization code will check this to omit prefix per attributeFormDefault
|
|
476
483
|
if type_ns_class.uri == parent_ns_class&.uri && form_default == :unqualified
|
|
@@ -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
|
|
|
@@ -985,9 +1009,41 @@ effective_register = lutaml_register)
|
|
|
985
1009
|
parent_ns_class = nil
|
|
986
1010
|
form_default = :unqualified
|
|
987
1011
|
end
|
|
988
|
-
rule.attribute_match_names(attr, effective_register,
|
|
989
|
-
|
|
990
|
-
|
|
1012
|
+
names = rule.attribute_match_names(attr, effective_register,
|
|
1013
|
+
parent_ns_class, form_default,
|
|
1014
|
+
options[:default_namespace])
|
|
1015
|
+
|
|
1016
|
+
# #786 lenient alias (["uri:name", "name"] from a type-qualified
|
|
1017
|
+
# rule): the unprefixed form must not steal the value when a
|
|
1018
|
+
# sibling namespace-less rule owns the bare local name (#744
|
|
1019
|
+
# disjointness). No sibling — the alias keeps 0.8.19 leniency.
|
|
1020
|
+
if names.size == 2 && !names[1].include?(":") &&
|
|
1021
|
+
instance_is_serialize &&
|
|
1022
|
+
unqualified_sibling_attribute_rule?(instance, rule,
|
|
1023
|
+
effective_register)
|
|
1024
|
+
names = names.first(1)
|
|
1025
|
+
end
|
|
1026
|
+
names
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
# Whether another attribute rule with the same local name resolves
|
|
1030
|
+
# namespace-less (no rule namespace, no value-type namespace) —
|
|
1031
|
+
# the #744 owner of the unprefixed source form.
|
|
1032
|
+
def unqualified_sibling_attribute_rule?(instance, rule,
|
|
1033
|
+
effective_register)
|
|
1034
|
+
mapping = instance.class.mappings_for(:xml, effective_register)
|
|
1035
|
+
return false unless mapping
|
|
1036
|
+
|
|
1037
|
+
attrs = instance.class.attributes(effective_register)
|
|
1038
|
+
mapping.mappings.any? do |sib|
|
|
1039
|
+
next false unless sib.attribute? && !sib.equal?(rule)
|
|
1040
|
+
next false unless sib.name.to_s == rule.name.to_s
|
|
1041
|
+
next false if sib.namespace_set?
|
|
1042
|
+
|
|
1043
|
+
sib_attr = attrs[sib.to]
|
|
1044
|
+
type_ns = sib_attr&.type_namespace_class(effective_register)
|
|
1045
|
+
type_ns.nil? || type_ns == :blank
|
|
1046
|
+
end
|
|
991
1047
|
end
|
|
992
1048
|
|
|
993
1049
|
# [namespace_class, attribute_form_default] for the instance's model,
|
|
@@ -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
|