lutaml-model 0.8.90 → 0.8.92
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/bench/bench_common.rb +1 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/plan_compiler.rb +3 -3
- data/lib/lutaml/xml/mapping.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +103 -1
- data/lib/lutaml/xml/namespace.rb +18 -8
- data/lib/lutaml/xml/namespace_needs.rb +77 -49
- data/lib/lutaml/xml/parse_session.rb +3 -19
- data/lib/lutaml/xml/transformation/ordered_applier.rb +20 -14
- data/lib/lutaml/xml/transformation.rb +15 -1
- data/lutaml-model.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19d1cc9a53554486e1dda21b67751062851d1990b9878aac1dcd6ccd2542d28e
|
|
4
|
+
data.tar.gz: 459cecd7cbdd127bb65a52cd6058d027e5139f2926e46d408bea5a28b13f2e7f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92637d4ab1efbd63d746983b4e359f2d49c00469e94bb249a050f36f1b80f15b18ae705245fdd8ab563d854e814fea54ce99993f64dbc17beb9c87555f7b3a95
|
|
7
|
+
data.tar.gz: 54412f06f6f1bed27f74a85495b414cfc6da3274ab03214220c27edd344aff8cd58f70874fa317fb61b9e8523a04725d69152dfde490b3aa61a2d75540a161ed
|
data/bench/bench_common.rb
CHANGED
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -26,9 +26,9 @@ module Lutaml
|
|
|
26
26
|
# metadata) completes.
|
|
27
27
|
module PlanCompiler
|
|
28
28
|
# TODO.perf/15: env-gated histogram of why models opt out of the
|
|
29
|
-
# plan path. Set PLAN_COMPILE_STATS=1 and read
|
|
30
|
-
# after a parse. Zero cost when disabled.
|
|
31
|
-
def self.
|
|
29
|
+
# plan path. Set PLAN_COMPILE_STATS=1 and read
|
|
30
|
+
# PlanCompiler.plan_stats after a parse. Zero cost when disabled.
|
|
31
|
+
def self.plan_stats
|
|
32
32
|
@plan_stats ||= Hash.new(0)
|
|
33
33
|
end
|
|
34
34
|
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -1197,6 +1197,31 @@ module Lutaml
|
|
|
1197
1197
|
# transform applies the collection-attribute filter.
|
|
1198
1198
|
#
|
|
1199
1199
|
# @return [Hash{Symbol, String => Array<MappingRule>}]
|
|
1200
|
+
# The when_attribute siblings-vs-plain-names partition of the
|
|
1201
|
+
# element rules. A ParseSession used to rebuild this per parsed
|
|
1202
|
+
# node (sessions are per-element), paying an O(rules) pass on
|
|
1203
|
+
# every node of large documents; it is a pure function of the
|
|
1204
|
+
# rules, so it memoizes here — same lifetime as
|
|
1205
|
+
# plain_element_rules_by_attr.
|
|
1206
|
+
def when_attribute_partition
|
|
1207
|
+
@when_attribute_partition ||= begin
|
|
1208
|
+
siblings = {}
|
|
1209
|
+
plain_names = {}
|
|
1210
|
+
mappings.each do |rule|
|
|
1211
|
+
next if rule.attribute? || rule.content_mapping? ||
|
|
1212
|
+
rule.raw_mapping? || rule.cdata
|
|
1213
|
+
|
|
1214
|
+
pairs = rule.when_attribute
|
|
1215
|
+
if pairs && !pairs.empty?
|
|
1216
|
+
(siblings[rule.name.to_s] ||= []) << rule
|
|
1217
|
+
else
|
|
1218
|
+
plain_names[rule.name.to_s] = true
|
|
1219
|
+
end
|
|
1220
|
+
end
|
|
1221
|
+
[siblings, plain_names]
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1200
1225
|
def plain_element_rules_by_attr(register_id = nil)
|
|
1201
1226
|
reg_key = register_id || :default
|
|
1202
1227
|
@plain_element_rules_by_attr ||= {}
|
|
@@ -344,6 +344,14 @@ module Lutaml
|
|
|
344
344
|
deserialize_attributes = attributes
|
|
345
345
|
mappings.each do |rule|
|
|
346
346
|
record = records&.[](rule)
|
|
347
|
+
|
|
348
|
+
# #903: replay-or-drop unmatched named element rules before
|
|
349
|
+
# any per-rule machinery (see replay_unmatched_rule).
|
|
350
|
+
if replay_unmatched_rule(child_names_set, record, rule,
|
|
351
|
+
instance, defaults_used, xml_mapping)
|
|
352
|
+
next
|
|
353
|
+
end
|
|
354
|
+
|
|
347
355
|
rule_to = rule.to
|
|
348
356
|
|
|
349
357
|
if record
|
|
@@ -847,7 +855,9 @@ _effective_register)
|
|
|
847
855
|
Lutaml::Xml::HtmlEntities.decode(value)
|
|
848
856
|
end
|
|
849
857
|
|
|
850
|
-
Record = Struct.new(:attr, :derived, :group_skip, :valid
|
|
858
|
+
Record = Struct.new(:attr, :derived, :group_skip, :valid,
|
|
859
|
+
:unmatched_mode, :replay_writer,
|
|
860
|
+
:unmatched_names)
|
|
851
861
|
|
|
852
862
|
def compiled_rule_records(model_class, register, grouped_plain_rules)
|
|
853
863
|
return {}.compare_by_identity unless model_class.include?(Lutaml::Model::Serialize)
|
|
@@ -866,12 +876,76 @@ _effective_register)
|
|
|
866
876
|
mapping&.mappings(register)&.each do |rule| # rubocop:disable Style/SafeNavigationChain,Style/SafeNavigation
|
|
867
877
|
attr = attribute_for_rule_static(model_class, rule, register)
|
|
868
878
|
group = grouped_plain_rules[rule.to]
|
|
879
|
+
# #903: for a plain rule, what an unmatched visit writes is
|
|
880
|
+
# known statically. The fast-skip branch hands the attribute's
|
|
881
|
+
# uninitialized sentinel to the pipeline; with no value-map
|
|
882
|
+
# overrides and no transformers every step is identity except
|
|
883
|
+
# apply_value_map, which maps the :omitted key: the default
|
|
884
|
+
# map (:nil) yields nil and the visit writes nil through the
|
|
885
|
+
# setter; an explicit omitted: :omitted map treats it as
|
|
886
|
+
# treat_omitted? == false and the omission gate ends the
|
|
887
|
+
# visit untouched. Both endings replay without the walk:
|
|
888
|
+
# :write_nil (setter write + flag churn) or :drop (nothing).
|
|
889
|
+
# Everything else -- defaults, aliases, custom from:,
|
|
890
|
+
# delegates, when_attribute dispatch, transformers, value
|
|
891
|
+
# maps beyond those two -- keeps its visit.
|
|
892
|
+
unmatched_mode = nil
|
|
893
|
+
replay_writer = nil
|
|
894
|
+
if attr && !attr.derived? &&
|
|
895
|
+
!rule.render_default && !rule.multiple_mappings? &&
|
|
896
|
+
!rule.has_custom_method_for_deserialization? &&
|
|
897
|
+
!rule.delegate &&
|
|
898
|
+
!(rule.when_attribute && !rule.when_attribute.empty?)
|
|
899
|
+
rule_transform = rule.transform
|
|
900
|
+
attr_transform = attr.transform
|
|
901
|
+
no_transforms =
|
|
902
|
+
(rule_transform.nil? ||
|
|
903
|
+
(rule_transform.respond_to?(:empty?) &&
|
|
904
|
+
rule_transform.empty?)) &&
|
|
905
|
+
(attr_transform.nil? ||
|
|
906
|
+
(attr_transform.respond_to?(:empty?) &&
|
|
907
|
+
attr_transform.empty?))
|
|
908
|
+
rule_default = attr.default(register)
|
|
909
|
+
if no_transforms &&
|
|
910
|
+
(rule_default.nil? ||
|
|
911
|
+
rule_default.is_a?(::Lutaml::Model::UninitializedClass)) &&
|
|
912
|
+
rule.class.transform_dispatch(rule, attr) == :assign
|
|
913
|
+
if group && group.size > 1 && attr.collection? &&
|
|
914
|
+
!group.first.equal?(rule)
|
|
915
|
+
# Non-first spelling of a #765 group: the visit is a
|
|
916
|
+
# guarded next (the first member does the merged match).
|
|
917
|
+
unmatched_mode = :drop
|
|
918
|
+
else
|
|
919
|
+
omitted_val = rule.value_map(:from)[:omitted]
|
|
920
|
+
if omitted_val == :nil
|
|
921
|
+
unmatched_mode = :write_nil
|
|
922
|
+
elsif omitted_val == :omitted
|
|
923
|
+
unmatched_mode = :drop
|
|
924
|
+
end
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
# A first group member does the merged match for every
|
|
929
|
+
# spelling, so its name-miss test spans the whole group.
|
|
930
|
+
unmatched_names = if group && group.size > 1
|
|
931
|
+
group.map { |r| r.name.to_s }
|
|
932
|
+
else
|
|
933
|
+
[rule.name.to_s]
|
|
934
|
+
end
|
|
935
|
+
if unmatched_mode == :write_nil
|
|
936
|
+
replay_writer =
|
|
937
|
+
rule.class.parsed_assign_writer(model_class, rule.to)
|
|
938
|
+
unmatched_mode = nil unless replay_writer
|
|
939
|
+
end
|
|
869
940
|
records[rule] = Record.new(
|
|
870
941
|
attr,
|
|
871
942
|
attr&.derived? || false,
|
|
872
943
|
!!(group && group.size > 1 && attr&.collection? &&
|
|
873
944
|
!group.first.equal?(rule)),
|
|
874
945
|
!!(attr || rule.custom_methods[:from]),
|
|
946
|
+
unmatched_mode,
|
|
947
|
+
replay_writer,
|
|
948
|
+
unmatched_names,
|
|
875
949
|
)
|
|
876
950
|
end
|
|
877
951
|
entry = Struct.new(:version, :records).new(version, records)
|
|
@@ -1551,6 +1625,34 @@ effective_register = lutaml_register)
|
|
|
1551
1625
|
# @param child_names_set [Set] Set of child namespaced_name and unprefixed_name
|
|
1552
1626
|
# @param default_namespace [String, nil] the default namespace
|
|
1553
1627
|
# @return [Boolean] true if a matching child likely exists
|
|
1628
|
+
# #903: for an unmatched named element rule whose record classed
|
|
1629
|
+
# it, replay what the visit would write without the walk: :drop
|
|
1630
|
+
# (the visit ended at the omission gate or was a guarded next)
|
|
1631
|
+
# writes nothing; :write_nil replays the nil the default value
|
|
1632
|
+
# map produces, plus the flag churn. Returns true when handled.
|
|
1633
|
+
def replay_unmatched_rule(child_names_set, record, rule, instance,
|
|
1634
|
+
defaults_used, xml_mapping)
|
|
1635
|
+
return false unless child_names_set && record
|
|
1636
|
+
|
|
1637
|
+
mode = record.unmatched_mode
|
|
1638
|
+
return false unless mode
|
|
1639
|
+
return false if rule.attribute? || rule.content_mapping? ||
|
|
1640
|
+
rule.raw_mapping? || !rule.name ||
|
|
1641
|
+
xml_mapping.any_element_rule.equal?(rule)
|
|
1642
|
+
|
|
1643
|
+
matched = record.unmatched_names.any? do |n|
|
|
1644
|
+
child_names_set.include?(n)
|
|
1645
|
+
end
|
|
1646
|
+
return false if matched
|
|
1647
|
+
|
|
1648
|
+
return true if mode == :drop
|
|
1649
|
+
|
|
1650
|
+
defaults_used << rule.to
|
|
1651
|
+
instance.public_send(record.replay_writer, nil)
|
|
1652
|
+
instance.value_set_for(rule.to)
|
|
1653
|
+
true
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1554
1656
|
def child_matches_rule?(rule, child_names_set, default_namespace)
|
|
1555
1657
|
rule_names = rule.namespaced_names(default_namespace)
|
|
1556
1658
|
# Array#intersect? requires an Array argument; child_names_set may
|
data/lib/lutaml/xml/namespace.rb
CHANGED
|
@@ -58,7 +58,10 @@ module Lutaml
|
|
|
58
58
|
# @param value [String, nil] the namespace URI
|
|
59
59
|
# @return [String, nil] the namespace URI
|
|
60
60
|
def uri(value = nil)
|
|
61
|
-
|
|
61
|
+
if value
|
|
62
|
+
@uri_value = value
|
|
63
|
+
@to_key = nil
|
|
64
|
+
end
|
|
62
65
|
@uri_value
|
|
63
66
|
end
|
|
64
67
|
|
|
@@ -130,6 +133,7 @@ module Lutaml
|
|
|
130
133
|
def prefix_default(value = nil)
|
|
131
134
|
if value
|
|
132
135
|
@prefix_default_value = value.to_s
|
|
136
|
+
@to_key = nil
|
|
133
137
|
end
|
|
134
138
|
@prefix_default_value
|
|
135
139
|
end
|
|
@@ -286,13 +290,19 @@ module Lutaml
|
|
|
286
290
|
#
|
|
287
291
|
# @api private
|
|
288
292
|
def to_key
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
# Called per element on the serialize path (90k births per
|
|
294
|
+
# 200-paragraph to_xml); memoized since uri/prefix_default
|
|
295
|
+
# are set at class-definition time. The setters above
|
|
296
|
+
# invalidate on reconfiguration.
|
|
297
|
+
@to_key ||= begin
|
|
298
|
+
prefix = prefix_default
|
|
299
|
+
namespace_uri = uri
|
|
300
|
+
|
|
301
|
+
if prefix && !prefix.empty?
|
|
302
|
+
"#{prefix}:#{namespace_uri}"
|
|
303
|
+
else
|
|
304
|
+
":#{namespace_uri}"
|
|
305
|
+
end
|
|
296
306
|
end
|
|
297
307
|
end
|
|
298
308
|
|
|
@@ -8,20 +8,45 @@ module Lutaml
|
|
|
8
8
|
# This class maintains MECE responsibility: it only stores and organizes
|
|
9
9
|
# namespace needs data. It does NOT make decisions or build XML.
|
|
10
10
|
class NamespaceNeeds
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
# Lazy readers: leaf nodes never touch most containers; the
|
|
12
|
+
# eager 4-Sets + 3-Hashes + 2-Arrays was the serialize path's
|
|
13
|
+
# top allocation site (150k births per 200-paragraph to_xml).
|
|
14
|
+
# Readers get the frozen empty truth; writers build on first
|
|
15
|
+
# touch through the ensure_* helpers below.
|
|
16
|
+
EMPTY_HASH = {}.freeze
|
|
17
|
+
EMPTY_SET = ::Set.new.freeze
|
|
18
|
+
EMPTY_ARRAY = [].freeze
|
|
19
|
+
|
|
20
|
+
def namespaces
|
|
21
|
+
@namespaces || EMPTY_HASH
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def children
|
|
25
|
+
@children || EMPTY_HASH
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def type_namespaces
|
|
29
|
+
@type_namespaces || EMPTY_HASH
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def type_namespace_classes
|
|
33
|
+
@type_namespace_classes || EMPTY_SET
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def type_attribute_namespaces
|
|
37
|
+
@type_attribute_namespaces || EMPTY_SET
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def type_element_namespaces
|
|
41
|
+
@type_element_namespaces || EMPTY_SET
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def type_refs
|
|
45
|
+
@type_refs || EMPTY_ARRAY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def namespace_scope_configs
|
|
49
|
+
@namespace_scope_configs || EMPTY_ARRAY
|
|
25
50
|
end
|
|
26
51
|
|
|
27
52
|
# Add namespace usage for a specific namespace
|
|
@@ -32,29 +57,29 @@ module Lutaml
|
|
|
32
57
|
raise ArgumentError, "Expected NamespaceUsage, got #{usage.class}"
|
|
33
58
|
end
|
|
34
59
|
|
|
35
|
-
@namespaces[key] = usage
|
|
60
|
+
(@namespaces ||= {})[key] = usage
|
|
36
61
|
end
|
|
37
62
|
|
|
38
63
|
# Track a type namespace for a specific attribute
|
|
39
64
|
# @param attr_name [Symbol] Attribute name
|
|
40
65
|
# @param ns_class [Class] XmlNamespace class
|
|
41
66
|
def add_type_namespace(attr_name, ns_class)
|
|
42
|
-
@type_namespaces[attr_name] = ns_class
|
|
43
|
-
@type_namespace_classes << ns_class
|
|
67
|
+
(@type_namespaces ||= {})[attr_name] = ns_class
|
|
68
|
+
(@type_namespace_classes ||= ::Set.new) << ns_class
|
|
44
69
|
end
|
|
45
70
|
|
|
46
71
|
# Add type attribute namespace to tracking set
|
|
47
72
|
# @param ns_class [Class] XmlNamespace class
|
|
48
73
|
def add_type_attribute_namespace(ns_class)
|
|
49
|
-
@type_attribute_namespaces << ns_class
|
|
50
|
-
@type_namespace_classes << ns_class
|
|
74
|
+
(@type_attribute_namespaces ||= ::Set.new) << ns_class
|
|
75
|
+
(@type_namespace_classes ||= ::Set.new) << ns_class
|
|
51
76
|
end
|
|
52
77
|
|
|
53
78
|
# Add type element namespace to tracking set
|
|
54
79
|
# @param ns_class [Class] XmlNamespace class
|
|
55
80
|
def add_type_element_namespace(ns_class)
|
|
56
|
-
@type_element_namespaces << ns_class
|
|
57
|
-
@type_namespace_classes << ns_class
|
|
81
|
+
(@type_element_namespaces ||= ::Set.new) << ns_class
|
|
82
|
+
(@type_namespace_classes ||= ::Set.new) << ns_class
|
|
58
83
|
end
|
|
59
84
|
|
|
60
85
|
# Add a type reference for lazy resolution
|
|
@@ -65,13 +90,13 @@ module Lutaml
|
|
|
65
90
|
"Expected TypeNamespace::Reference, got #{reference.class}"
|
|
66
91
|
end
|
|
67
92
|
|
|
68
|
-
@type_refs << reference
|
|
93
|
+
(@type_refs ||= []) << reference
|
|
69
94
|
end
|
|
70
95
|
|
|
71
96
|
# Clear type references after resolution
|
|
72
97
|
# Used by TypeNamespaceResolver to prevent reprocessing
|
|
73
98
|
def clear_type_refs
|
|
74
|
-
@type_refs
|
|
99
|
+
@type_refs&.clear
|
|
75
100
|
end
|
|
76
101
|
|
|
77
102
|
# Add namespace scope configuration
|
|
@@ -82,7 +107,7 @@ module Lutaml
|
|
|
82
107
|
"Expected NamespaceScopeConfig, got #{config.class}"
|
|
83
108
|
end
|
|
84
109
|
|
|
85
|
-
@namespace_scope_configs << config
|
|
110
|
+
(@namespace_scope_configs ||= []) << config
|
|
86
111
|
end
|
|
87
112
|
|
|
88
113
|
# Add child needs
|
|
@@ -94,7 +119,7 @@ module Lutaml
|
|
|
94
119
|
"Expected NamespaceNeeds, got #{child_needs.class}"
|
|
95
120
|
end
|
|
96
121
|
|
|
97
|
-
@children[name] = child_needs
|
|
122
|
+
(@children ||= {})[name] = child_needs
|
|
98
123
|
end
|
|
99
124
|
|
|
100
125
|
# Merge another NamespaceNeeds into this one
|
|
@@ -105,9 +130,9 @@ module Lutaml
|
|
|
105
130
|
raise ArgumentError, "Expected NamespaceNeeds, got #{other.class}"
|
|
106
131
|
end
|
|
107
132
|
|
|
108
|
-
# Merge namespaces
|
|
133
|
+
# Merge namespaces (build containers on first touch)
|
|
109
134
|
other.namespaces.each do |key, usage|
|
|
110
|
-
if @namespaces.key?(key)
|
|
135
|
+
if (@namespaces ||= {}).key?(key)
|
|
111
136
|
@namespaces[key].merge(usage)
|
|
112
137
|
else
|
|
113
138
|
@namespaces[key] = usage
|
|
@@ -116,7 +141,7 @@ module Lutaml
|
|
|
116
141
|
|
|
117
142
|
# Merge children
|
|
118
143
|
other.children.each do |name, child_needs|
|
|
119
|
-
if @children.key?(name)
|
|
144
|
+
if (@children ||= {}).key?(name)
|
|
120
145
|
@children[name].merge(child_needs)
|
|
121
146
|
else
|
|
122
147
|
@children[name] = child_needs
|
|
@@ -124,17 +149,20 @@ module Lutaml
|
|
|
124
149
|
end
|
|
125
150
|
|
|
126
151
|
# Merge type namespaces
|
|
127
|
-
@type_namespaces.merge!(other.type_namespaces)
|
|
128
|
-
@type_namespace_classes.
|
|
129
|
-
|
|
130
|
-
@
|
|
152
|
+
(@type_namespaces ||= {}).merge!(other.type_namespaces)
|
|
153
|
+
(@type_namespace_classes ||= ::Set.new)
|
|
154
|
+
.merge(other.type_namespace_classes)
|
|
155
|
+
(@type_attribute_namespaces ||= ::Set.new)
|
|
156
|
+
.merge(other.type_attribute_namespaces)
|
|
157
|
+
(@type_element_namespaces ||= ::Set.new)
|
|
158
|
+
.merge(other.type_element_namespaces)
|
|
131
159
|
|
|
132
160
|
# Merge type refs
|
|
133
|
-
@type_refs.concat(other.type_refs)
|
|
161
|
+
(@type_refs ||= []).concat(other.type_refs)
|
|
134
162
|
|
|
135
163
|
# Merge namespace scope configs (avoiding duplicates)
|
|
136
164
|
other.namespace_scope_configs.each do |config|
|
|
137
|
-
unless @namespace_scope_configs.any? do |c|
|
|
165
|
+
unless (@namespace_scope_configs ||= []).any? do |c|
|
|
138
166
|
c.namespace_class == config.namespace_class
|
|
139
167
|
end
|
|
140
168
|
@namespace_scope_configs << config
|
|
@@ -147,32 +175,32 @@ module Lutaml
|
|
|
147
175
|
# Check if needs are empty
|
|
148
176
|
# @return [Boolean]
|
|
149
177
|
def empty?
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
namespaces.empty? &&
|
|
179
|
+
children.empty? &&
|
|
180
|
+
type_refs.empty? &&
|
|
181
|
+
type_namespaces.empty? &&
|
|
182
|
+
namespace_scope_configs.empty?
|
|
155
183
|
end
|
|
156
184
|
|
|
157
185
|
# Get namespace usage by key
|
|
158
186
|
# @param key [String] Namespace key
|
|
159
187
|
# @return [NamespaceUsage, nil]
|
|
160
188
|
def namespace(key)
|
|
161
|
-
|
|
189
|
+
namespaces[key]
|
|
162
190
|
end
|
|
163
191
|
|
|
164
192
|
# Get child needs by name
|
|
165
193
|
# @param name [Symbol] Child attribute name
|
|
166
194
|
# @return [NamespaceNeeds, nil]
|
|
167
195
|
def child(name)
|
|
168
|
-
|
|
196
|
+
children[name]
|
|
169
197
|
end
|
|
170
198
|
|
|
171
199
|
# Check if a namespace is in scope configuration
|
|
172
200
|
# @param ns_class [Class] XmlNamespace class
|
|
173
201
|
# @return [NamespaceScopeConfig, nil]
|
|
174
202
|
def scope_config_for(ns_class)
|
|
175
|
-
|
|
203
|
+
namespace_scope_configs.find do |config|
|
|
176
204
|
config.namespace_class == ns_class
|
|
177
205
|
end
|
|
178
206
|
end
|
|
@@ -180,28 +208,28 @@ module Lutaml
|
|
|
180
208
|
# Get all namespace classes (from usage and type namespaces)
|
|
181
209
|
# @return [Set<Class>]
|
|
182
210
|
def all_namespace_classes
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
classes = Set.new(namespaces.values.map(&:namespace_class))
|
|
212
|
+
classes.merge(type_namespace_classes)
|
|
185
213
|
end
|
|
186
214
|
|
|
187
215
|
# Validate internal consistency
|
|
188
216
|
# @raise [RuntimeError] if inconsistent state detected
|
|
189
217
|
def validate!
|
|
190
218
|
# Type attribute and element namespaces should be mutually exclusive
|
|
191
|
-
overlap =
|
|
219
|
+
overlap = type_attribute_namespaces & type_element_namespaces
|
|
192
220
|
unless overlap.empty?
|
|
193
221
|
raise "Type namespaces appear in both attribute and element contexts: #{overlap.to_a}"
|
|
194
222
|
end
|
|
195
223
|
|
|
196
224
|
# All type namespace classes should be in type_namespace_classes
|
|
197
|
-
|
|
198
|
-
unless
|
|
225
|
+
type_attribute_namespaces.each do |ns_class|
|
|
226
|
+
unless type_namespace_classes.include?(ns_class)
|
|
199
227
|
raise "Type attribute namespace #{ns_class} not in type_namespace_classes"
|
|
200
228
|
end
|
|
201
229
|
end
|
|
202
230
|
|
|
203
|
-
|
|
204
|
-
unless
|
|
231
|
+
type_element_namespaces.each do |ns_class|
|
|
232
|
+
unless type_namespace_classes.include?(ns_class)
|
|
205
233
|
raise "Type element namespace #{ns_class} not in type_namespace_classes"
|
|
206
234
|
end
|
|
207
235
|
end
|
|
@@ -84,25 +84,9 @@ module Lutaml
|
|
|
84
84
|
# Empty unless the mapping uses when_attribute at all; non-Serialize
|
|
85
85
|
# custom models have no mappings to partition.
|
|
86
86
|
def when_attribute_partition
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
plain_names = {}
|
|
91
|
-
mapping.mappings.each do |rule|
|
|
92
|
-
next if rule.attribute? || rule.content_mapping? ||
|
|
93
|
-
rule.raw_mapping? || rule.cdata
|
|
94
|
-
|
|
95
|
-
pairs = rule.when_attribute
|
|
96
|
-
if pairs && !pairs.empty?
|
|
97
|
-
(siblings[rule.name.to_s] ||= []) << rule
|
|
98
|
-
else
|
|
99
|
-
plain_names[rule.name.to_s] = true
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
[siblings, plain_names]
|
|
103
|
-
else
|
|
104
|
-
[{}, {}]
|
|
105
|
-
end
|
|
87
|
+
return [{}, {}] unless instance_is_serialize && (mapping = xml_mapping)
|
|
88
|
+
|
|
89
|
+
mapping.when_attribute_partition
|
|
106
90
|
end
|
|
107
91
|
|
|
108
92
|
def when_attribute_siblings_by_name
|
|
@@ -101,20 +101,26 @@ model_class, register_id)
|
|
|
101
101
|
return
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
# Fast path: the ordered walk pre-sets the parent context keys
|
|
105
|
+
# for every value it emits (one hash per walk, not per value).
|
|
106
|
+
# The three keys are always written together, so a parent
|
|
107
|
+
# identity match implies the namespace context matches too.
|
|
108
|
+
if options[:parent_element] == parent
|
|
109
|
+
child_options = options
|
|
110
|
+
else
|
|
111
|
+
parent_ns_class = parent.namespace_class
|
|
112
|
+
# Only pass element_form_default VALUE if it was explicitly set
|
|
113
|
+
# When not set (defaults to :unqualified), pass nil to avoid incorrect blank namespace treatment
|
|
114
|
+
parent_element_form_default = if parent_ns_class&.element_form_default_set?
|
|
115
|
+
parent_ns_class.element_form_default
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
child_options = options.dup
|
|
119
|
+
child_options[:parent_namespace_class] = parent_ns_class
|
|
120
|
+
child_options[:parent_element_form_default] =
|
|
121
|
+
parent_element_form_default
|
|
122
|
+
child_options[:parent_element] = parent
|
|
123
|
+
end
|
|
118
124
|
|
|
119
125
|
element = yield(rule, value, child_options)
|
|
120
126
|
parent.add_child(element) if element
|
|
@@ -265,6 +265,20 @@ module Lutaml
|
|
|
265
265
|
# Loop-invariant for the whole ordered walk (see
|
|
266
266
|
# apply_standard_rules): one merge per model, not per rule.
|
|
267
267
|
rule_options = options.merge(current_model: model_instance)
|
|
268
|
+
# `root` is the parent of every element value this walk emits,
|
|
269
|
+
# so the parent context keys are walk-invariant too: set them
|
|
270
|
+
# once here and apply_element_rule_single's fast path reuses
|
|
271
|
+
# this hash for every value instead of merging per value.
|
|
272
|
+
# rule_options itself keeps the caller's parent context for
|
|
273
|
+
# the attribute/content/raw rules dispatched via apply_rule.
|
|
274
|
+
element_value_options = rule_options.dup
|
|
275
|
+
root_ns_class = root.namespace_class
|
|
276
|
+
element_value_options[:parent_namespace_class] = root_ns_class
|
|
277
|
+
element_value_options[:parent_element_form_default] =
|
|
278
|
+
if root_ns_class&.element_form_default_set?
|
|
279
|
+
root_ns_class.element_form_default
|
|
280
|
+
end
|
|
281
|
+
element_value_options[:parent_element] = root
|
|
268
282
|
apply_rules_in_order(
|
|
269
283
|
root, model_instance, options,
|
|
270
284
|
compiled_rules, model_class, register_id
|
|
@@ -281,7 +295,7 @@ module Lutaml
|
|
|
281
295
|
parent: root,
|
|
282
296
|
rule: rule,
|
|
283
297
|
value: value,
|
|
284
|
-
options:
|
|
298
|
+
options: element_value_options,
|
|
285
299
|
) do |r, v, child_opts|
|
|
286
300
|
element = create_element_for_value(r, v, child_opts, model_class,
|
|
287
301
|
register_id, register)
|
data/lutaml-model.gemspec
CHANGED
|
@@ -43,7 +43,7 @@ Gem::Specification.new do |spec|
|
|
|
43
43
|
# input-buffer cap (lutaml-model#871).
|
|
44
44
|
spec.add_dependency "moxml", "~> 0.5.84"
|
|
45
45
|
spec.add_dependency "ostruct"
|
|
46
|
-
spec.add_dependency "rubyzip", "~> 3.
|
|
46
|
+
spec.add_dependency "rubyzip", "~> 3.7"
|
|
47
47
|
spec.add_dependency "thor"
|
|
48
48
|
|
|
49
49
|
spec.metadata["rubygems_mfa_required"] = "true"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.92
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -120,14 +120,14 @@ dependencies:
|
|
|
120
120
|
requirements:
|
|
121
121
|
- - "~>"
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '3.
|
|
123
|
+
version: '3.7'
|
|
124
124
|
type: :runtime
|
|
125
125
|
prerelease: false
|
|
126
126
|
version_requirements: !ruby/object:Gem::Requirement
|
|
127
127
|
requirements:
|
|
128
128
|
- - "~>"
|
|
129
129
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '3.
|
|
130
|
+
version: '3.7'
|
|
131
131
|
- !ruby/object:Gem::Dependency
|
|
132
132
|
name: thor
|
|
133
133
|
requirement: !ruby/object:Gem::Requirement
|