lutaml-model 0.8.18 → 0.9.1
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.
Potentially problematic release.
This version of lutaml-model might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +1 -1
- data/.github/workflows/rake.yml +1 -0
- data/.rubocop_todo.yml +133 -72
- data/bench/bench_common.rb +6 -3
- data/bench/bench_niso.rb +1 -2
- data/docs/_guides/builder-dsl.adoc +127 -0
- data/lib/compat/opal/generate_boot.rb +3 -1
- data/lib/lutaml/key_value/transform.rb +4 -1
- data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
- data/lib/lutaml/model/attribute.rb +23 -2
- data/lib/lutaml/model/global_context.rb +4 -2
- data/lib/lutaml/model/schema/class_loader.rb +2 -1
- data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
- data/lib/lutaml/model/schema/file_writer.rb +5 -2
- data/lib/lutaml/model/schema/renderers/base.rb +2 -1
- data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
- data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
- data/lib/lutaml/model/schema/renderers/model.rb +16 -4
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
- data/lib/lutaml/model/schema/renderers.rb +2 -1
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
- data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
- data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
- data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
- data/lib/lutaml/model/serialize/attribute_definition.rb +10 -9
- data/lib/lutaml/model/serialize/builder.rb +55 -0
- data/lib/lutaml/model/serialize/initialization.rb +12 -3
- data/lib/lutaml/model/serialize/model_import.rb +1 -0
- data/lib/lutaml/model/serialize.rb +5 -4
- data/lib/lutaml/model/union.rb +5 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping_rule.rb +55 -1
- data/lib/lutaml/xml/model_transform.rb +53 -5
- data/lib/lutaml/xml/schema/xsd.rb +0 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
- data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +99 -4
- data/lutaml-model.gemspec +4 -1
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
- data/spec/lutaml/model/attribute_spec.rb +8 -3
- data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
- data/spec/lutaml/model/defaults_spec.rb +45 -0
- data/spec/lutaml/model/json_spec.rb +1 -2
- data/spec/lutaml/model/ordered_content_spec.rb +18 -7
- data/spec/lutaml/model/serialize/builder_spec.rb +361 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
- data/spec/lutaml/model/union_attribute_spec.rb +4 -2
- data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
- data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
- metadata +7 -3
|
@@ -108,24 +108,20 @@ module Lutaml
|
|
|
108
108
|
current = instance_variable_get(:"@#{name}") || []
|
|
109
109
|
new_value = current.is_a?(Array) ? current + [value] : value
|
|
110
110
|
instance_variable_set(:"@#{name}", new_value)
|
|
111
|
-
|
|
112
|
-
track_order(name, value, nil) if @__order_tracking__
|
|
111
|
+
record_mutation(name, value)
|
|
113
112
|
value
|
|
114
113
|
end
|
|
115
114
|
end
|
|
116
115
|
else
|
|
117
116
|
# For non-collection attributes, getter accepts optional argument
|
|
118
|
-
# for builder-style syntax: g.description(value) sets the value
|
|
117
|
+
# for builder-style syntax: g.description(value) sets the value.
|
|
118
|
+
# Tracking happens inside the setter, so no duplicate call here.
|
|
119
119
|
define_method(name) do |*args|
|
|
120
120
|
if args.empty?
|
|
121
121
|
instance_variable_get(:"@#{name}")
|
|
122
122
|
else
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
public_send(:"#{name}=", value)
|
|
126
|
-
# Track order for mixed_content serialization
|
|
127
|
-
track_order(name, value, nil) if @__order_tracking__
|
|
128
|
-
value
|
|
123
|
+
public_send(:"#{name}=", args.first)
|
|
124
|
+
args.first
|
|
129
125
|
end
|
|
130
126
|
end
|
|
131
127
|
end
|
|
@@ -156,12 +152,16 @@ module Lutaml
|
|
|
156
152
|
else
|
|
157
153
|
instance_variable_set(:"@#{name}", value)
|
|
158
154
|
end
|
|
155
|
+
# Track one entry per item so element_order reflects the
|
|
156
|
+
# number of <name> elements that will be emitted.
|
|
157
|
+
record_mutation_collection(name, value)
|
|
159
158
|
end
|
|
160
159
|
else
|
|
161
160
|
define_method(:"#{name}=") do |value|
|
|
162
161
|
value_set_for(name)
|
|
163
162
|
value = attr.cast_value(value, lutaml_register)
|
|
164
163
|
instance_variable_set(:"@#{name}", value)
|
|
164
|
+
record_mutation(name, value)
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -188,6 +188,7 @@ module Lutaml
|
|
|
188
188
|
|
|
189
189
|
attr = Attribute.new(name, type, options)
|
|
190
190
|
@attributes[name] = attr
|
|
191
|
+
@merged_attributes_cache = nil
|
|
191
192
|
define_attribute_methods(attr)
|
|
192
193
|
|
|
193
194
|
attr
|
|
@@ -75,6 +75,61 @@ module Lutaml
|
|
|
75
75
|
mapping&.ordered? || false
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Whether this instance was constructed via a builder block and
|
|
79
|
+
# therefore records mutations into element_order. Parsed models
|
|
80
|
+
# and instances constructed without a block do not track; their
|
|
81
|
+
# element_order (if any) comes from the parser and is treated as
|
|
82
|
+
# the complete source of truth by the serializer.
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def order_tracking_enabled?
|
|
85
|
+
@__order_tracking__ ? true : false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Record a singular attribute mutation in element_order.
|
|
89
|
+
#
|
|
90
|
+
# No-op unless order tracking is enabled (i.e. the instance was
|
|
91
|
+
# constructed via a builder block on an ordered/mixed_content
|
|
92
|
+
# model). This is the single entry point for singular mutations
|
|
93
|
+
# and is called from generated setters and getter-with-arg paths
|
|
94
|
+
# so that direct setters (`x.foo = v`) and appender calls
|
|
95
|
+
# (`x.foo(v)`) behave identically.
|
|
96
|
+
#
|
|
97
|
+
# Returns `value` so generated setters preserve Ruby's setter
|
|
98
|
+
# contract: `obj.foo = v` evaluates to `v`. Callers like
|
|
99
|
+
# `obj.foo || (obj.foo = [])` depend on this.
|
|
100
|
+
#
|
|
101
|
+
# @param attribute_name [Symbol] The attribute being mutated
|
|
102
|
+
# @param value [Object, nil] The value being set; stored as text
|
|
103
|
+
# content for content-mapped attributes, ignored otherwise
|
|
104
|
+
# @return [Object] the passed value
|
|
105
|
+
def record_mutation(attribute_name, value = nil)
|
|
106
|
+
return value unless @__order_tracking__
|
|
107
|
+
|
|
108
|
+
track_order(attribute_name, value, nil)
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Record a collection-attribute mutation in element_order.
|
|
113
|
+
#
|
|
114
|
+
# Emits one entry per item so that element_order length matches
|
|
115
|
+
# the number of serialized child elements. No-op when tracking
|
|
116
|
+
# is disabled, when the value is nil/empty, or when the
|
|
117
|
+
# collection's frozen sentinel is preserved (no real data).
|
|
118
|
+
#
|
|
119
|
+
# Returns `value` so generated setters preserve Ruby's setter
|
|
120
|
+
# contract (see {record_mutation}).
|
|
121
|
+
#
|
|
122
|
+
# @param attribute_name [Symbol] The collection attribute
|
|
123
|
+
# @param value [Object, nil] The value assigned to the collection
|
|
124
|
+
# @return [Object] the passed value
|
|
125
|
+
def record_mutation_collection(attribute_name, value)
|
|
126
|
+
return value unless @__order_tracking__
|
|
127
|
+
return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
|
|
128
|
+
|
|
129
|
+
Array(value).each { |item| track_order(attribute_name, item, nil) }
|
|
130
|
+
value
|
|
131
|
+
end
|
|
132
|
+
|
|
78
133
|
private
|
|
79
134
|
|
|
80
135
|
# Intercept method calls to track order for mixed_content
|
|
@@ -115,13 +115,18 @@ module Lutaml
|
|
|
115
115
|
# Get all attributes for this model
|
|
116
116
|
#
|
|
117
117
|
# Merges class-level attributes with register-specific attributes.
|
|
118
|
+
# Memoized per register: this is on the per-element deserialization
|
|
119
|
+
# hot path, and the merged hash is stable until an attribute, import
|
|
120
|
+
# or register record mutates (those clear @merged_attributes_cache).
|
|
118
121
|
#
|
|
119
122
|
# @param register [Symbol, nil] The register context
|
|
120
123
|
# @return [Hash] The attributes hash
|
|
121
124
|
def attributes(register = nil)
|
|
122
125
|
ensure_imports!(register) if finalized?
|
|
123
126
|
if @register_records&.any?
|
|
124
|
-
|
|
127
|
+
register_id = extract_register_id(register)
|
|
128
|
+
(@merged_attributes_cache ||= {})[register_id] ||=
|
|
129
|
+
@attributes.merge(@register_records[register_id][:attributes])
|
|
125
130
|
else
|
|
126
131
|
@attributes
|
|
127
132
|
end
|
|
@@ -183,6 +188,9 @@ module Lutaml
|
|
|
183
188
|
GlobalContext.resolver.clear_cache(register_id)
|
|
184
189
|
end
|
|
185
190
|
|
|
191
|
+
# Clear memoized attribute merge (see .attributes)
|
|
192
|
+
@merged_attributes_cache = nil
|
|
193
|
+
|
|
186
194
|
# Clear per-Attribute type caches (stale entries from GC'd TypeContext objects)
|
|
187
195
|
class_attributes.each_value(&:clear_type_cache)
|
|
188
196
|
@register_records&.each_value do |record|
|
|
@@ -370,7 +378,6 @@ module Lutaml
|
|
|
370
378
|
instance_variable_get(:"@#{name}")
|
|
371
379
|
else
|
|
372
380
|
public_send(:"#{name}=", args.first)
|
|
373
|
-
track_order(name, args.first, nil) if @__order_tracking__
|
|
374
381
|
args.first
|
|
375
382
|
end
|
|
376
383
|
end
|
|
@@ -380,6 +387,7 @@ module Lutaml
|
|
|
380
387
|
reg_attr = resolve_register_attr(name)
|
|
381
388
|
value = reg_attr.cast_value(value, lutaml_register)
|
|
382
389
|
instance_variable_set(:"@#{name}", value)
|
|
390
|
+
record_mutation(name, value)
|
|
383
391
|
end
|
|
384
392
|
end
|
|
385
393
|
|
|
@@ -395,7 +403,7 @@ module Lutaml
|
|
|
395
403
|
current = [] if current.equal?(LAZY_EMPTY_COLLECTION)
|
|
396
404
|
new_value = current.is_a?(Array) ? current + [value] : value
|
|
397
405
|
instance_variable_set(:"@#{name}", new_value)
|
|
398
|
-
|
|
406
|
+
record_mutation(name, value)
|
|
399
407
|
value
|
|
400
408
|
end
|
|
401
409
|
end
|
|
@@ -411,6 +419,7 @@ module Lutaml
|
|
|
411
419
|
else
|
|
412
420
|
instance_variable_set(:"@#{name}", value)
|
|
413
421
|
end
|
|
422
|
+
record_mutation_collection(name, value)
|
|
414
423
|
end
|
|
415
424
|
end
|
|
416
425
|
|
|
@@ -175,18 +175,19 @@ module Lutaml
|
|
|
175
175
|
@using_default[attribute_name]
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
def method_missing(method_name, *args)
|
|
178
|
+
def method_missing(method_name, *, &)
|
|
180
179
|
if method_name.to_s.end_with?("=") && attribute_exist?(method_name)
|
|
181
180
|
define_singleton_method(method_name) do |value|
|
|
182
181
|
instance_variable_set(:"@#{method_name.to_s.chomp('=')}", value)
|
|
183
182
|
end
|
|
184
|
-
send(method_name, *
|
|
183
|
+
send(method_name, *)
|
|
184
|
+
elsif ::Lutaml::Model::Attribute.default_evaluation? &&
|
|
185
|
+
self.class.respond_to?(method_name)
|
|
186
|
+
self.class.public_send(method_name, *, &)
|
|
185
187
|
else
|
|
186
188
|
super
|
|
187
189
|
end
|
|
188
190
|
end
|
|
189
|
-
# rubocop:enable Style/ArgumentsForwarding
|
|
190
191
|
|
|
191
192
|
def respond_to_missing?(method_name, include_private = false)
|
|
192
193
|
(method_name.to_s.end_with?("=") && attribute_exist?(method_name)) ||
|
data/lib/lutaml/model/union.rb
CHANGED
|
@@ -131,7 +131,8 @@ module Lutaml
|
|
|
131
131
|
|
|
132
132
|
def validate_members!(members)
|
|
133
133
|
unless members.is_a?(::Array) && !members.empty?
|
|
134
|
-
raise ArgumentError,
|
|
134
|
+
raise ArgumentError,
|
|
135
|
+
"union type must be a non-empty array of types"
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
resolved = members.map { |member| resolve_member!(member) }
|
|
@@ -166,7 +167,9 @@ module Lutaml
|
|
|
166
167
|
return :no_match if keys.nil? || keys.empty?
|
|
167
168
|
|
|
168
169
|
field_names = member_field_names(member, format, register)
|
|
169
|
-
return :no_match unless keys.all?
|
|
170
|
+
return :no_match unless keys.all? do |key|
|
|
171
|
+
field_names.include?(key)
|
|
172
|
+
end
|
|
170
173
|
|
|
171
174
|
# Plain-Ruby assignment (format nil) builds straight from the
|
|
172
175
|
# attribute hash, mirroring how non-union model attributes accept
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -183,6 +183,34 @@ module Lutaml
|
|
|
183
183
|
result
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# Match names an attribute rule consumes during parsing, mirroring how
|
|
187
|
+
# serialization qualifies the same attribute (resolve_namespace):
|
|
188
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
189
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
190
|
+
# attributes.
|
|
191
|
+
#
|
|
192
|
+
# Performance: memoized on the rule with zero-allocation cache hits.
|
|
193
|
+
# The inputs are constant for a given rule, register and parent model,
|
|
194
|
+
# so every element of a collection reuses the first resolution.
|
|
195
|
+
def attribute_match_names(attr, register, parent_ns_class, form_default,
|
|
196
|
+
default_namespace)
|
|
197
|
+
if defined?(@_amn_names) && @_amn_register == register &&
|
|
198
|
+
@_amn_parent_ns == parent_ns_class &&
|
|
199
|
+
@_amn_form_default == form_default &&
|
|
200
|
+
@_amn_default_ns == default_namespace
|
|
201
|
+
return @_amn_names
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result = compute_attribute_match_names(attr, register, parent_ns_class,
|
|
205
|
+
form_default, default_namespace)
|
|
206
|
+
@_amn_register = register
|
|
207
|
+
@_amn_parent_ns = parent_ns_class
|
|
208
|
+
@_amn_form_default = form_default
|
|
209
|
+
@_amn_default_ns = default_namespace
|
|
210
|
+
@_amn_names = result
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
186
214
|
def namespaced_name(parent_namespace = nil, name = self.name)
|
|
187
215
|
if name.to_s == "lang"
|
|
188
216
|
::Lutaml::Model::Utils.blank?(prefix) ? name.to_s : "#{prefix}:#{name}"
|
|
@@ -274,6 +302,30 @@ module Lutaml
|
|
|
274
302
|
|
|
275
303
|
private
|
|
276
304
|
|
|
305
|
+
# Extracted body of attribute_match_names for caching
|
|
306
|
+
def compute_attribute_match_names(attr, register, parent_ns_class,
|
|
307
|
+
form_default, default_namespace)
|
|
308
|
+
ns_info = resolve_namespace(
|
|
309
|
+
attr: attr,
|
|
310
|
+
register: register,
|
|
311
|
+
parent_ns_class: parent_ns_class,
|
|
312
|
+
form_default: form_default,
|
|
313
|
+
)
|
|
314
|
+
uri = ns_info[:uri]
|
|
315
|
+
# Schema-level attribute_form_default keeps the historical plain-name
|
|
316
|
+
# parse: dependents (e.g. ogc-gml) declare :qualified but their
|
|
317
|
+
# documents and expectations rely on unprefixed attributes parsing.
|
|
318
|
+
# Explicit namespaces, type namespaces and explicit form: rules
|
|
319
|
+
# mirror serialization strictly.
|
|
320
|
+
strict_match = uri && ns_info[:prefix] &&
|
|
321
|
+
!ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
|
|
322
|
+
if strict_match
|
|
323
|
+
["#{uri}:#{name}"].freeze
|
|
324
|
+
else
|
|
325
|
+
namespaced_names(default_namespace)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
277
329
|
# Extracted body of namespaced_names for caching
|
|
278
330
|
def compute_namespaced_names(parent_namespace)
|
|
279
331
|
if multiple_mappings?
|
|
@@ -427,7 +479,9 @@ form_default = :unqualified)
|
|
|
427
479
|
# When schema specifies attributeFormDefault="qualified", attributes
|
|
428
480
|
# must be qualified (prefixed) with the parent element's namespace
|
|
429
481
|
if form_default == :qualified && parent_ns_class
|
|
430
|
-
|
|
482
|
+
result = build_namespace_result_from_class(parent_ns_class)
|
|
483
|
+
result[:via_schema_default] = true
|
|
484
|
+
return result
|
|
431
485
|
end
|
|
432
486
|
|
|
433
487
|
# 4. No namespace (W3C default for unprefixed attributes)
|
|
@@ -509,8 +509,13 @@ _effective_register)
|
|
|
509
509
|
local_name = rn[(last_colon_index + 1)..]
|
|
510
510
|
|
|
511
511
|
matched_attr = doc.root.attributes.values.find do |attr|
|
|
512
|
-
#
|
|
513
|
-
#
|
|
512
|
+
# A namespace-less, prefix-less attribute cannot belong to the
|
|
513
|
+
# rule's namespace, so it must not satisfy the fallback
|
|
514
|
+
# (lutaml-model#744). Attributes with undeclared prefixes keep
|
|
515
|
+
# their prefix in the raw name and stay eligible below.
|
|
516
|
+
next false if attr.namespace.nil? &&
|
|
517
|
+
attr.namespace_prefix.nil? && !attr.name.include?(":")
|
|
518
|
+
|
|
514
519
|
attr.namespaced_name == local_name ||
|
|
515
520
|
attr.unprefixed_name == local_name ||
|
|
516
521
|
(attr.namespace.nil? && ((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name)
|
|
@@ -544,9 +549,14 @@ _effective_register)
|
|
|
544
549
|
rule_namespace = rule_namespace_set ? rule.namespace : nil
|
|
545
550
|
options[:default_namespace]
|
|
546
551
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
rule_names = if rule.attribute?
|
|
553
|
+
resolve_attribute_rule_names(rule, attr, options,
|
|
554
|
+
effective_register,
|
|
555
|
+
instance, instance_is_serialize)
|
|
556
|
+
else
|
|
557
|
+
resolve_rule_names_with_type(rule, attr, options,
|
|
558
|
+
effective_register, attr_type)
|
|
559
|
+
end
|
|
550
560
|
|
|
551
561
|
return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
|
|
552
562
|
|
|
@@ -879,6 +889,44 @@ effective_register = lutaml_register)
|
|
|
879
889
|
end
|
|
880
890
|
end
|
|
881
891
|
|
|
892
|
+
# Match names for an attribute rule, mirroring how serialization
|
|
893
|
+
# qualifies the same attribute (MappingRule#attribute_match_names):
|
|
894
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
895
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
896
|
+
# attributes.
|
|
897
|
+
def resolve_attribute_rule_names(rule, attr, options, effective_register,
|
|
898
|
+
instance, instance_is_serialize)
|
|
899
|
+
return rule.namespaced_names(options[:default_namespace]) if attr.nil? ||
|
|
900
|
+
rule.multiple_mappings?
|
|
901
|
+
|
|
902
|
+
if instance_is_serialize
|
|
903
|
+
parent_ns_class, form_default = parent_attribute_context(instance)
|
|
904
|
+
else
|
|
905
|
+
parent_ns_class = nil
|
|
906
|
+
form_default = :unqualified
|
|
907
|
+
end
|
|
908
|
+
rule.attribute_match_names(attr, effective_register, parent_ns_class,
|
|
909
|
+
form_default,
|
|
910
|
+
options[:default_namespace])
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# [namespace_class, attribute_form_default] for the instance's model,
|
|
914
|
+
# keyed on the model class. Attribute rules of one element share the
|
|
915
|
+
# model, so a 1-entry cache avoids a mappings_for lookup per rule.
|
|
916
|
+
def parent_attribute_context(instance)
|
|
917
|
+
klass = instance.class
|
|
918
|
+
if @parent_ns_klass == klass && @parent_ns_value
|
|
919
|
+
return @parent_ns_value
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
ns_class = klass.mappings_for(:xml)&.namespace_class
|
|
923
|
+
value = [ns_class,
|
|
924
|
+
ns_class&.attribute_form_default || :unqualified]
|
|
925
|
+
@parent_ns_klass = klass
|
|
926
|
+
@parent_ns_value = value
|
|
927
|
+
value
|
|
928
|
+
end
|
|
929
|
+
|
|
882
930
|
# Resolve rule names with type namespace support
|
|
883
931
|
#
|
|
884
932
|
# @param rule [MappingRule] the mapping rule
|
|
@@ -141,9 +141,7 @@ module Lutaml
|
|
|
141
141
|
# Trigger autoloads for all XSD model classes to ensure they are
|
|
142
142
|
# registered before parsing begins. This is necessary because
|
|
143
143
|
# type resolution during parsing looks up classes in the register.
|
|
144
|
-
# rubocop:disable Style/RedundantFetchOverride
|
|
145
144
|
XSD_AUTOLOAD_CLASSES.each { |c| const_get(c) unless c == :VERSION }
|
|
146
|
-
# rubocop:enable Style/RedundantFetchOverride
|
|
147
145
|
|
|
148
146
|
# Validate XSD schema structure before parsing (unless disabled)
|
|
149
147
|
if validate_schema && !nested_schema
|
|
@@ -410,7 +410,8 @@ attr_name)
|
|
|
410
410
|
def self.get_attribute_xsd_type(attr, attr_type, register,
|
|
411
411
|
_mapping_rule = nil)
|
|
412
412
|
if attr.union?
|
|
413
|
-
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
413
|
+
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
414
|
+
"XSD")
|
|
414
415
|
end
|
|
415
416
|
|
|
416
417
|
# 1. Check for deprecated attribute-level xsd_type override
|
|
@@ -143,7 +143,8 @@ parent_element_form_default)
|
|
|
143
143
|
# Get transformation for the actual class. Unions resolve the member
|
|
144
144
|
# from the value's own class, like polymorphism.
|
|
145
145
|
child_transformation = if union
|
|
146
|
-
value.class.transformation_for(:xml,
|
|
146
|
+
value.class.transformation_for(:xml,
|
|
147
|
+
register)
|
|
147
148
|
elsif is_polymorphic || is_polymorphic_subtype
|
|
148
149
|
actual_class.transformation_for(:xml,
|
|
149
150
|
register)
|
|
@@ -297,7 +297,16 @@ _options)
|
|
|
297
297
|
end
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
-
# Apply remaining rules (attributes
|
|
300
|
+
# Apply remaining rules (attributes, content/raw, and any element
|
|
301
|
+
# rules not represented in element_order).
|
|
302
|
+
#
|
|
303
|
+
# The element-type skip that used to live here caused silent data
|
|
304
|
+
# loss whenever an element-typed attribute was missing from
|
|
305
|
+
# element_order (e.g. when a direct setter forgot to call
|
|
306
|
+
# track_order). After the builder-side fix all mutation paths
|
|
307
|
+
# record into element_order, so this branch is a defense-in-depth
|
|
308
|
+
# safety net: emit any element-typed rule whose value would
|
|
309
|
+
# otherwise vanish.
|
|
301
310
|
#
|
|
302
311
|
# @param root [XmlElement] Root element
|
|
303
312
|
# @param model_instance [Object] The model instance
|
|
@@ -317,21 +326,107 @@ compiled_rules, mapping, processed_text_nodes)
|
|
|
317
326
|
compiled_rules
|
|
318
327
|
end
|
|
319
328
|
|
|
329
|
+
emitted_counts = element_order_coverage(model_instance,
|
|
330
|
+
compiled_rules)
|
|
331
|
+
|
|
320
332
|
rules_to_apply.each do |rule|
|
|
321
|
-
|
|
333
|
+
mapping_type = rule.option(:mapping_type)
|
|
322
334
|
|
|
323
|
-
# Skip content
|
|
324
|
-
if %i[content raw].include?(
|
|
335
|
+
# Skip content/raw if mixed or text nodes were processed
|
|
336
|
+
if %i[content raw].include?(mapping_type) &&
|
|
325
337
|
(mapping&.mixed_content? || processed_text_nodes)
|
|
326
338
|
next
|
|
327
339
|
end
|
|
328
340
|
|
|
341
|
+
if mapping_type == :element &&
|
|
342
|
+
element_rule_already_emitted?(rule, model_instance,
|
|
343
|
+
emitted_counts)
|
|
344
|
+
next
|
|
345
|
+
end
|
|
346
|
+
|
|
329
347
|
next unless valid_mapping?(rule, options)
|
|
330
348
|
|
|
331
349
|
yield(:apply_rule, rule, nil) if block_given?
|
|
332
350
|
end
|
|
333
351
|
end
|
|
334
352
|
|
|
353
|
+
# Count, per element-typed rule, how many entries in element_order
|
|
354
|
+
# already cover it. Returns a Hash keyed by CompiledRule identity.
|
|
355
|
+
#
|
|
356
|
+
# @param model_instance [Object] The model instance
|
|
357
|
+
# @param compiled_rules [Array<CompiledRule>] The compiled rules
|
|
358
|
+
# @return [Hash<CompiledRule, Integer>] Coverage counts
|
|
359
|
+
def element_order_coverage(model_instance, compiled_rules)
|
|
360
|
+
counts = ::Hash.new(0)
|
|
361
|
+
return counts unless model_instance.respond_to?(:element_order)
|
|
362
|
+
return counts unless (order = model_instance.element_order)
|
|
363
|
+
|
|
364
|
+
element_rules = compiled_rules.select do |r|
|
|
365
|
+
r.is_a?(::Lutaml::Model::CompiledRule) &&
|
|
366
|
+
r.option(:mapping_type) == :element
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
order.each do |object|
|
|
370
|
+
next unless object.type == "Element"
|
|
371
|
+
|
|
372
|
+
object_ns_uri = object.namespace_uri
|
|
373
|
+
matched = element_rules.find do |r|
|
|
374
|
+
matches_element_rule?(r, object.name, object_ns_uri)
|
|
375
|
+
end
|
|
376
|
+
counts[matched] += 1 if matched
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
counts
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Whether an element-typed rule has already been fully emitted
|
|
383
|
+
# via element_order (or should not be emitted at all by the safety
|
|
384
|
+
# net). Returns true when:
|
|
385
|
+
# - the model was not constructed via builder block
|
|
386
|
+
# (`@__order_tracking__` is nil/false): parsed models trust
|
|
387
|
+
# element_order as the complete source of truth, so the safety
|
|
388
|
+
# net must not second-guess it by emitting defaults/uninitialized
|
|
389
|
+
# values that the standard path would otherwise have skipped.
|
|
390
|
+
# - the standard skip logic says the value should be skipped
|
|
391
|
+
# (handles defaults, render_nil/render_empty, value_map, etc.)
|
|
392
|
+
# - or the rule was fully covered by element_order entries
|
|
393
|
+
#
|
|
394
|
+
# The safety net targets the bug class where a builder-block
|
|
395
|
+
# construction bypassed element_order tracking (e.g. via a
|
|
396
|
+
# mutation path that forgot to call record_mutation). After
|
|
397
|
+
# Option A, all setter/getter paths record into element_order,
|
|
398
|
+
# so this branch is defense-in-depth rather than the common
|
|
399
|
+
# path.
|
|
400
|
+
#
|
|
401
|
+
# @param rule [CompiledRule] The element rule
|
|
402
|
+
# @param model_instance [Object] The model instance
|
|
403
|
+
# @param emitted_counts [Hash<CompiledRule, Integer>] Coverage map
|
|
404
|
+
# @return [Boolean]
|
|
405
|
+
def element_rule_already_emitted?(rule, model_instance,
|
|
406
|
+
emitted_counts)
|
|
407
|
+
return true unless model_order_tracking_enabled?(model_instance)
|
|
408
|
+
|
|
409
|
+
value = extract_ordered_rule_value(rule, model_instance)
|
|
410
|
+
return true if should_skip_value?(value, rule, model_instance)
|
|
411
|
+
|
|
412
|
+
emitted = emitted_counts[rule]
|
|
413
|
+
if rule.collection?
|
|
414
|
+
value_length = value.respond_to?(:length) ? value.length : 0
|
|
415
|
+
emitted >= value_length
|
|
416
|
+
else
|
|
417
|
+
emitted.positive?
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Whether the model was constructed via a builder block and thus
|
|
422
|
+
# has order tracking enabled. Only such models are candidates for
|
|
423
|
+
# the safety net; parsed models trust element_order as-is.
|
|
424
|
+
def model_order_tracking_enabled?(model_instance)
|
|
425
|
+
return false unless model_instance.respond_to?(:order_tracking_enabled?)
|
|
426
|
+
|
|
427
|
+
model_instance.order_tracking_enabled?
|
|
428
|
+
end
|
|
429
|
+
|
|
335
430
|
# Sort compiled rules so attribute rules follow the captured attribute_order.
|
|
336
431
|
# Non-attribute rules (content, raw) maintain their original position.
|
|
337
432
|
#
|
data/lutaml-model.gemspec
CHANGED
|
@@ -18,7 +18,10 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.bindir = "exe"
|
|
20
20
|
spec.require_paths = ["lib"]
|
|
21
|
-
|
|
21
|
+
# lib/ uses anonymous argument forwarding in call position, which parses
|
|
22
|
+
# only on Ruby 3.2+ (verified: 6 files fail to parse on 3.1). Without this
|
|
23
|
+
# floor, bundler resolves 0.8.x on 3.1 and it dies at require time.
|
|
24
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
22
25
|
|
|
23
26
|
# Specify which files should be added to the gem when it is released.
|
|
24
27
|
# The `git ls-files -z` loads the files in the
|