lutaml-model 0.8.18 → 0.8.22
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/.github/workflows/dependent-tests.yml +1 -1
- data/.github/workflows/rake.yml +1 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +133 -72
- data/bench/bench_common.rb +8 -5
- data/bench/bench_niso.rb +1 -2
- data/bench/gate_config.rb +4 -1
- data/benchmark/serialization_benchmark.rb +4 -4
- data/docs/_guides/builder-dsl.adoc +127 -0
- data/docs/_guides/value-transformations.adoc +27 -0
- data/lib/compat/opal/generate_boot.rb +3 -1
- data/lib/compat/opal/lutaml_model_boot.rb +10 -7
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/lutaml/hash_format/format.rb +29 -0
- data/lib/lutaml/hash_format.rb +5 -27
- data/lib/lutaml/json/format.rb +61 -0
- data/lib/lutaml/json.rb +4 -59
- data/lib/lutaml/jsonl/format.rb +22 -0
- data/lib/lutaml/jsonl.rb +5 -19
- data/lib/lutaml/key_value/format.rb +26 -0
- data/lib/lutaml/key_value/transform.rb +4 -1
- data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
- data/lib/lutaml/key_value.rb +4 -21
- data/lib/lutaml/model/attribute.rb +38 -4
- data/lib/lutaml/model/collection.rb +2 -2
- data/lib/lutaml/model/compiled_rule.rb +1 -1
- data/lib/lutaml/model/config.rb +2 -2
- data/lib/lutaml/model/consolidation_map.rb +2 -2
- data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
- data/lib/lutaml/model/errors.rb +2 -2
- data/lib/lutaml/model/global_context.rb +4 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
- data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +2 -2
- data/lib/lutaml/model/schema/class_loader.rb +2 -1
- data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
- 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 +4 -3
- 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 +18 -6
- 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/supported_data_types.rb +2 -2
- data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
- data/lib/lutaml/model/sequence.rb +4 -4
- data/lib/lutaml/model/serialize/attribute_definition.rb +10 -9
- data/lib/lutaml/model/serialize/builder.rb +55 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +2 -2
- 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 +4 -3
- data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
- data/lib/lutaml/model/type.rb +9 -0
- data/lib/lutaml/model/type_registry.rb +2 -2
- data/lib/lutaml/model/uninitialized_class.rb +1 -1
- data/lib/lutaml/model/union.rb +5 -2
- data/lib/lutaml/model/utils.rb +2 -2
- data/lib/lutaml/model/value_policy.rb +27 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +17 -16
- data/lib/lutaml/rdf/mapping.rb +2 -2
- data/lib/lutaml/rdf/namespace_set.rb +2 -2
- data/lib/lutaml/rdf/transform.rb +2 -2
- data/lib/lutaml/toml/format.rb +48 -0
- data/lib/lutaml/toml.rb +5 -46
- data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
- data/lib/lutaml/xml/builder/base.rb +2 -2
- data/lib/lutaml/xml/format.rb +331 -0
- data/lib/lutaml/xml/mapping.rb +2 -2
- data/lib/lutaml/xml/mapping_rule.rb +62 -3
- data/lib/lutaml/xml/model_transform.rb +132 -55
- data/lib/lutaml/xml/namespace_collector.rb +3 -3
- data/lib/lutaml/xml/parse_session.rb +59 -0
- data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
- data/lib/lutaml/xml/schema/builder.rb +5 -5
- data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
- data/lib/lutaml/xml/schema/xsd.rb +0 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
- data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +99 -4
- data/lib/lutaml/xml/xml_element.rb +2 -0
- data/lib/lutaml/xml.rb +4 -328
- data/lib/lutaml/yaml/format.rb +30 -0
- data/lib/lutaml/yaml.rb +5 -28
- data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
- data/lib/lutaml/yamls/format.rb +22 -0
- data/lib/lutaml/yamls.rb +5 -19
- data/lib/tasks/benchmark_runner.rb +2 -2
- data/lutaml-model.gemspec +1 -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_bibtex_adapter_spec.rb +2 -2
- data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
- 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/store_spec.rb +3 -1
- 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/model/value_policy_spec.rb +82 -0
- data/spec/lutaml/require_load_order_spec.rb +25 -0
- data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
- data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
- data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -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 +22 -4
|
@@ -86,8 +86,8 @@ module Lutaml
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
root_and_parent_assignment(instance, options)
|
|
89
|
-
apply_xml_mapping(data, instance, options,
|
|
90
|
-
|
|
89
|
+
apply_xml_mapping(ParseSession.new(data, instance, options,
|
|
90
|
+
child_register))
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Build a DeclarationPlan from the parsed element tree's namespace declarations.
|
|
@@ -206,17 +206,12 @@ module Lutaml
|
|
|
206
206
|
|
|
207
207
|
private
|
|
208
208
|
|
|
209
|
-
def apply_xml_mapping(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
effective_register ||= if instance_is_serialize && instance.lutaml_register
|
|
216
|
-
instance.lutaml_register
|
|
217
|
-
else
|
|
218
|
-
lutaml_register
|
|
219
|
-
end
|
|
209
|
+
def apply_xml_mapping(session)
|
|
210
|
+
doc = session.doc
|
|
211
|
+
instance = session.instance
|
|
212
|
+
options = session.options
|
|
213
|
+
effective_register = session.register
|
|
214
|
+
instance_is_serialize = session.instance_is_serialize
|
|
220
215
|
|
|
221
216
|
# Performance: Resolve xml_mapping once — used for both rules and metadata
|
|
222
217
|
xml_mapping = mappings_for(:xml, effective_register)
|
|
@@ -303,8 +298,7 @@ effective_register = nil, instance_is_serialize = nil)
|
|
|
303
298
|
end
|
|
304
299
|
else
|
|
305
300
|
# Performance: Pass cached attr to avoid recomputing attribute_for_rule
|
|
306
|
-
val = value_for_rule(
|
|
307
|
-
effective_register, instance_is_serialize)
|
|
301
|
+
val = value_for_rule(session, rule, new_opts, attr)
|
|
308
302
|
|
|
309
303
|
if (val.nil? || ::Lutaml::Model::Utils.uninitialized?(val)) &&
|
|
310
304
|
(instance.using_default?(rule_to) || rule.render_default)
|
|
@@ -437,13 +431,20 @@ _effective_register)
|
|
|
437
431
|
# - URI format: "http://.../namespace:attr" or "urn:...:attr"
|
|
438
432
|
# - Simple prefix format: "my-ns:attr" (namespace URI is a simple string, not a URI)
|
|
439
433
|
# - Unprefixed: "attr" (no namespace)
|
|
440
|
-
# Performance:
|
|
441
|
-
|
|
434
|
+
# Performance: most rules carry a single name — skip the map and
|
|
435
|
+
# its result array entirely on that path.
|
|
436
|
+
if rule_names.size == 1
|
|
437
|
+
rn = rule_names[0]
|
|
442
438
|
converted = convert_rule_name_to_attribute_name(doc, rn)
|
|
443
|
-
converted || rn
|
|
444
|
-
|
|
439
|
+
value = doc.root.find_attribute_value(converted || rn)
|
|
440
|
+
else
|
|
441
|
+
attribute_names = rule_names.map do |rn|
|
|
442
|
+
converted = convert_rule_name_to_attribute_name(doc, rn)
|
|
443
|
+
converted || rn
|
|
444
|
+
end
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
value = doc.root.find_attribute_value(attribute_names)
|
|
447
|
+
end
|
|
447
448
|
|
|
448
449
|
# Fallback: if value is nil, try to match by local name only.
|
|
449
450
|
# This handles the case where the namespace prefix is not declared in the document
|
|
@@ -468,25 +469,36 @@ _effective_register)
|
|
|
468
469
|
def convert_rule_name_to_attribute_name(doc, rule_name)
|
|
469
470
|
return nil unless rule_name.include?(":")
|
|
470
471
|
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
# Determine if namespace_part is a URI format or a simple prefix
|
|
478
|
-
# URI formats: contains "://" (http/https) or starts with "urn:"
|
|
479
|
-
is_uri_format = namespace_part.include?("://") || namespace_part.start_with?("urn:")
|
|
472
|
+
# URI-vs-prefix detection works on the whole rule name: the
|
|
473
|
+
# namespace part is a prefix of rule_name, so "://" anywhere in
|
|
474
|
+
# the name means a URI-form namespace, and a "urn:" namespace
|
|
475
|
+
# makes the whole name start with "urn:". Slicing the parts out
|
|
476
|
+
# first would allocate two strings per call on this hot path.
|
|
477
|
+
is_uri_format = rule_name.include?("://") || rule_name.start_with?("urn:")
|
|
480
478
|
|
|
479
|
+
last_colon_index = rule_name.rindex(":")
|
|
481
480
|
if is_uri_format
|
|
482
|
-
# URI format:
|
|
483
|
-
|
|
484
|
-
|
|
481
|
+
# URI format: find the attribute whose resolved namespace and
|
|
482
|
+
# local name spell the rule name. Comparison is by namespace
|
|
483
|
+
# prefix + local suffix so prefixed attributes (whose
|
|
484
|
+
# namespaced_name is "prefix:local", never "uri:local") still
|
|
485
|
+
# match, and unprefixed_name is not called per attribute
|
|
486
|
+
# (it splits prefixed names and allocates).
|
|
487
|
+
local_name = rule_name[(last_colon_index + 1)..]
|
|
488
|
+
doc.root.attributes.each_value.find do |attr|
|
|
489
|
+
ns = attr.namespace
|
|
490
|
+
next false if ns.nil? || ns.length > last_colon_index
|
|
491
|
+
next false unless rule_name.start_with?(ns) && rule_name[ns.length] == ":"
|
|
492
|
+
|
|
493
|
+
attr.unprefixed_name == local_name
|
|
485
494
|
end&.name
|
|
486
495
|
else
|
|
487
496
|
# Simple prefix format: look up the actual prefix from document's namespace declarations
|
|
488
497
|
# The namespace_part is the namespace URI declared in the document (e.g., "my-ns").
|
|
489
498
|
# Find the corresponding prefix (e.g., "my") and build "my:val".
|
|
499
|
+
namespace_part = rule_name[0...last_colon_index]
|
|
500
|
+
local_name = rule_name[(last_colon_index + 1)..]
|
|
501
|
+
|
|
490
502
|
ns_data = doc.root.namespaces.values.find do |nd|
|
|
491
503
|
nd.uri == namespace_part
|
|
492
504
|
end
|
|
@@ -507,31 +519,43 @@ _effective_register)
|
|
|
507
519
|
|
|
508
520
|
last_colon_index = rn.rindex(":")
|
|
509
521
|
local_name = rn[(last_colon_index + 1)..]
|
|
522
|
+
rule_uri = rn[0...last_colon_index]
|
|
523
|
+
|
|
524
|
+
matched_attr = doc.root.attributes.each_value.find do |attr|
|
|
525
|
+
# Local-name fallback serves two lenient cases only:
|
|
526
|
+
# - an attribute with an undeclared prefix (raw name keeps the
|
|
527
|
+
# colon, namespace unresolvable), the historical purpose;
|
|
528
|
+
# - an attribute whose resolved namespace IS one of the rule's
|
|
529
|
+
# namespace URIs (prefix rebinding, lutaml-model#744).
|
|
530
|
+
# Everything else — including a definitively namespace-less
|
|
531
|
+
# attribute and an attribute from a foreign namespace — must
|
|
532
|
+
# not satisfy the fallback, or a plain rule steals a
|
|
533
|
+
# namespaced sibling attribute (lutaml-model#758). An
|
|
534
|
+
# XmlAttribute with a resolved namespace has namespaced_name
|
|
535
|
+
# "URI:local", so this comparison is allocation-free.
|
|
536
|
+
if attr.namespace.nil?
|
|
537
|
+
next false if attr.namespace_prefix.nil? &&
|
|
538
|
+
!attr.name.include?(":")
|
|
510
539
|
|
|
511
|
-
matched_attr = doc.root.attributes.values.find do |attr|
|
|
512
|
-
# Match by the attribute's namespaced_name, unprefixed_name, or by extracting
|
|
513
|
-
# local name from the attribute's key (handles unresolved prefixes).
|
|
514
|
-
attr.namespaced_name == local_name ||
|
|
515
540
|
attr.unprefixed_name == local_name ||
|
|
516
|
-
|
|
541
|
+
((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name
|
|
542
|
+
else
|
|
543
|
+
# Prefixed attributes carry namespaced_name "prefix:local",
|
|
544
|
+
# which a URI-form rule name can never equal — match by the
|
|
545
|
+
# attribute's resolved namespace instead.
|
|
546
|
+
attr.namespace == rule_uri && attr.unprefixed_name == local_name
|
|
547
|
+
end
|
|
517
548
|
end
|
|
518
549
|
return matched_attr&.value if matched_attr
|
|
519
550
|
end
|
|
520
551
|
nil
|
|
521
552
|
end
|
|
522
553
|
|
|
523
|
-
def value_for_rule(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
# Use threaded register from apply_xml_mapping if provided, otherwise derive
|
|
530
|
-
effective_register ||= if instance_is_serialize && instance.lutaml_register
|
|
531
|
-
instance.lutaml_register
|
|
532
|
-
else
|
|
533
|
-
lutaml_register
|
|
534
|
-
end
|
|
554
|
+
def value_for_rule(session, rule, options, cached_attr = nil)
|
|
555
|
+
doc = session.doc
|
|
556
|
+
instance = session.instance
|
|
557
|
+
instance_is_serialize = session.instance_is_serialize
|
|
558
|
+
effective_register = session.register
|
|
535
559
|
|
|
536
560
|
# Performance: Use cached attr from caller if available
|
|
537
561
|
attr = cached_attr || attribute_for_rule(rule)
|
|
@@ -544,9 +568,14 @@ _effective_register)
|
|
|
544
568
|
rule_namespace = rule_namespace_set ? rule.namespace : nil
|
|
545
569
|
options[:default_namespace]
|
|
546
570
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
571
|
+
rule_names = if rule.attribute?
|
|
572
|
+
resolve_attribute_rule_names(rule, attr, options,
|
|
573
|
+
effective_register,
|
|
574
|
+
instance, instance_is_serialize)
|
|
575
|
+
else
|
|
576
|
+
resolve_rule_names_with_type(rule, attr, options,
|
|
577
|
+
effective_register, attr_type)
|
|
578
|
+
end
|
|
550
579
|
|
|
551
580
|
return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
|
|
552
581
|
|
|
@@ -565,15 +594,25 @@ _effective_register)
|
|
|
565
594
|
end
|
|
566
595
|
type_ns_prefix_str = type_ns_class&.prefix_default&.to_s
|
|
567
596
|
type_ns_all_uris = type_ns_class&.all_uris
|
|
597
|
+
# Adopt the parsed document's namespace (memoized on the session):
|
|
598
|
+
# when the whole document is out-of-namespace, treat it as an
|
|
599
|
+
# implicit alias so lenient parsing is lossless (#754).
|
|
600
|
+
adopted_uri = session.adopted_namespace_uri
|
|
601
|
+
if adopted_uri && !type_ns_all_uris&.include?(adopted_uri)
|
|
602
|
+
type_ns_all_uris = (type_ns_all_uris || []) + [adopted_uri]
|
|
603
|
+
end
|
|
568
604
|
end
|
|
569
605
|
|
|
570
606
|
# Pre-compute model's namespace class for simple-type alias matching
|
|
571
|
-
# (Third branch in select loop —
|
|
607
|
+
# (Third branch in select loop — memoized on the session)
|
|
572
608
|
model_ns_class = if instance_is_serialize &&
|
|
573
609
|
!rule_namespace_set && !attr_type_is_serializable
|
|
574
|
-
|
|
610
|
+
session.xml_mapping&.namespace_class
|
|
575
611
|
end
|
|
576
|
-
|
|
612
|
+
# Includes the adopted document namespace (#754): a prefixed child
|
|
613
|
+
# in the document's (foreign) namespace must still bind by local
|
|
614
|
+
# name, or its content is silently dropped.
|
|
615
|
+
model_ns_all_uris = model_ns_class ? session.model_namespace_uris : nil
|
|
577
616
|
|
|
578
617
|
# Performance: Use cached element children from XmlElement
|
|
579
618
|
# This avoids O(children * rules) scans by pre-filtering once per element
|
|
@@ -879,6 +918,44 @@ effective_register = lutaml_register)
|
|
|
879
918
|
end
|
|
880
919
|
end
|
|
881
920
|
|
|
921
|
+
# Match names for an attribute rule, mirroring how serialization
|
|
922
|
+
# qualifies the same attribute (MappingRule#attribute_match_names):
|
|
923
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
924
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
925
|
+
# attributes.
|
|
926
|
+
def resolve_attribute_rule_names(rule, attr, options, effective_register,
|
|
927
|
+
instance, instance_is_serialize)
|
|
928
|
+
return rule.namespaced_names(options[:default_namespace]) if attr.nil? ||
|
|
929
|
+
rule.multiple_mappings?
|
|
930
|
+
|
|
931
|
+
if instance_is_serialize
|
|
932
|
+
parent_ns_class, form_default = parent_attribute_context(instance)
|
|
933
|
+
else
|
|
934
|
+
parent_ns_class = nil
|
|
935
|
+
form_default = :unqualified
|
|
936
|
+
end
|
|
937
|
+
rule.attribute_match_names(attr, effective_register, parent_ns_class,
|
|
938
|
+
form_default,
|
|
939
|
+
options[:default_namespace])
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
# [namespace_class, attribute_form_default] for the instance's model,
|
|
943
|
+
# keyed on the model class. Attribute rules of one element share the
|
|
944
|
+
# model, so a 1-entry cache avoids a mappings_for lookup per rule.
|
|
945
|
+
def parent_attribute_context(instance)
|
|
946
|
+
klass = instance.class
|
|
947
|
+
if @parent_ns_klass == klass && @parent_ns_value
|
|
948
|
+
return @parent_ns_value
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
ns_class = klass.mappings_for(:xml)&.namespace_class
|
|
952
|
+
value = [ns_class,
|
|
953
|
+
ns_class&.attribute_form_default || :unqualified]
|
|
954
|
+
@parent_ns_klass = klass
|
|
955
|
+
@parent_ns_value = value
|
|
956
|
+
value
|
|
957
|
+
end
|
|
958
|
+
|
|
882
959
|
# Resolve rule names with type namespace support
|
|
883
960
|
#
|
|
884
961
|
# @param rule [MappingRule] the mapping rule
|
|
@@ -37,18 +37,18 @@ module Lutaml
|
|
|
37
37
|
# @param mapping [Xml::Mapping] the XML mapping for this element
|
|
38
38
|
# @param options [Hash] additional options including mapper_class for recursive calls
|
|
39
39
|
# @return [NamespaceNeeds] namespace needs structure
|
|
40
|
-
def collect(element, mapping, visited: nil, **)
|
|
40
|
+
def collect(element, mapping, visited: nil, **options)
|
|
41
41
|
# If this is a top-level call (no visited yet), ensure cleanup
|
|
42
42
|
if visited.nil?
|
|
43
43
|
Thread.current[:namespace_collector_call_stack] = []
|
|
44
44
|
begin
|
|
45
|
-
collect_internal(element, mapping, visited: nil, **)
|
|
45
|
+
collect_internal(element, mapping, visited: nil, **options)
|
|
46
46
|
ensure
|
|
47
47
|
Thread.current[:namespace_collector_call_stack] = nil
|
|
48
48
|
end
|
|
49
49
|
else
|
|
50
50
|
# Nested call - don't clean up yet
|
|
51
|
-
collect_internal(element, mapping, visited: visited, **)
|
|
51
|
+
collect_internal(element, mapping, visited: visited, **options)
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Per-parse context threaded through ModelTransform's mapping passes.
|
|
6
|
+
#
|
|
7
|
+
# Bundles everything one data_to_model call needs — the parsed doc, the
|
|
8
|
+
# instance under construction, the effective register, and the flags
|
|
9
|
+
# derived from them — so apply_xml_mapping and value_for_rule take one
|
|
10
|
+
# object instead of a growing positional parameter list. Memoizes the
|
|
11
|
+
# per-parse lookups (mapping, namespace class, adopted document
|
|
12
|
+
# namespace) that every rule would otherwise recompute.
|
|
13
|
+
class ParseSession
|
|
14
|
+
attr_reader :doc, :instance, :options, :register
|
|
15
|
+
|
|
16
|
+
def initialize(doc, instance, options, register)
|
|
17
|
+
@doc = doc
|
|
18
|
+
@instance = instance
|
|
19
|
+
@options = options
|
|
20
|
+
@register = register
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def instance_is_serialize
|
|
24
|
+
@instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def model_class
|
|
28
|
+
@model_class ||= instance.class
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def xml_mapping
|
|
32
|
+
@xml_mapping ||= model_class.mappings_for(:xml, register)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The document's own namespace when it differs from the model's
|
|
36
|
+
# bound namespace (lenient, out-of-namespace documents). Adopted as
|
|
37
|
+
# an implicit alias for element matching (lutaml-model#754).
|
|
38
|
+
def adopted_namespace_uri
|
|
39
|
+
@adopted_namespace_uri ||=
|
|
40
|
+
instance_is_serialize ? instance.original_namespace_uri : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Namespace URIs accepted for children of this model: the model's
|
|
44
|
+
# namespace URIs plus the adopted document namespace when present.
|
|
45
|
+
def model_namespace_uris
|
|
46
|
+
@model_namespace_uris ||=
|
|
47
|
+
begin
|
|
48
|
+
ns_class = instance_is_serialize ? xml_mapping&.namespace_class : nil
|
|
49
|
+
uris = ns_class&.all_uris
|
|
50
|
+
adopted = adopted_namespace_uri
|
|
51
|
+
if ns_class && adopted && !uris&.include?(adopted)
|
|
52
|
+
uris = (uris || []) + [adopted]
|
|
53
|
+
end
|
|
54
|
+
uris
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -20,7 +20,7 @@ module Lutaml
|
|
|
20
20
|
# Supported schema builders (separate from XML parsing adapters)
|
|
21
21
|
SUPPORTED_BUILDERS = %i[nokogiri oga].freeze
|
|
22
22
|
|
|
23
|
-
def initialize(adapter_type: nil, options: {}, &)
|
|
23
|
+
def initialize(adapter_type: nil, options: {}, &block)
|
|
24
24
|
# Use specified adapter, or configured XML adapter, or default to Nokogiri
|
|
25
25
|
requested_adapter = adapter_type || Lutaml::Model::Config.xml_adapter_type || :nokogiri
|
|
26
26
|
|
|
@@ -32,7 +32,7 @@ module Lutaml
|
|
|
32
32
|
:nokogiri
|
|
33
33
|
end
|
|
34
34
|
@options = options
|
|
35
|
-
@builder = create_builder(&)
|
|
35
|
+
@builder = create_builder(&block)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Generate the XSD schema XML string
|
|
@@ -45,12 +45,12 @@ module Lutaml
|
|
|
45
45
|
|
|
46
46
|
private
|
|
47
47
|
|
|
48
|
-
def create_builder(&)
|
|
48
|
+
def create_builder(&block)
|
|
49
49
|
case @adapter_type
|
|
50
50
|
when :nokogiri
|
|
51
|
-
Nokogiri.new(@options, &)
|
|
51
|
+
Nokogiri.new(@options, &block)
|
|
52
52
|
when :oga
|
|
53
|
-
Oga.new(@options, &)
|
|
53
|
+
Oga.new(@options, &block)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -28,7 +28,7 @@ module Lutaml
|
|
|
28
28
|
# @param to [String] The target path
|
|
29
29
|
# @param pattern [Boolean, nil] Explicit pattern flag (optional)
|
|
30
30
|
def initialize(attributes = nil, from: nil, to: nil, pattern: nil,
|
|
31
|
-
**)
|
|
31
|
+
**options)
|
|
32
32
|
# Handle hash argument from Lutaml::Model deserialization
|
|
33
33
|
if attributes.is_a?(Hash)
|
|
34
34
|
from = attributes[:from] || attributes["from"]
|
|
@@ -45,7 +45,7 @@ module Lutaml
|
|
|
45
45
|
# Use explicit pattern flag if provided, otherwise use detected value
|
|
46
46
|
final_pattern = pattern.nil? ? detected_pattern : pattern
|
|
47
47
|
|
|
48
|
-
super(from: from_str, to: to, pattern: final_pattern, **)
|
|
48
|
+
super(from: from_str, to: to, pattern: final_pattern, **options)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Convert hash-based mapping to SchemaLocationMapping instance
|
|
@@ -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
|
|
@@ -89,7 +89,7 @@ module Lutaml
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# Override choice to set format: :xml so Choice knows which format it belongs to.
|
|
92
|
-
def choice(min: 1, max: 1, format: :xml, &)
|
|
92
|
+
def choice(min: 1, max: 1, format: :xml, &block)
|
|
93
93
|
super
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -101,9 +101,9 @@ module Lutaml
|
|
|
101
101
|
# @param format [Symbol] The format
|
|
102
102
|
# @param args [Array] Additional arguments (mapping class for XML)
|
|
103
103
|
# @param block [Proc] The DSL block
|
|
104
|
-
def process_mapping(format, *args, &)
|
|
104
|
+
def process_mapping(format, *args, &block)
|
|
105
105
|
if format == :xml && args.any? && args.first.is_a?(Class) && args.first < Lutaml::Xml::Mapping
|
|
106
|
-
process_xml_mapping_class_inheritance(args.first, &)
|
|
106
|
+
process_xml_mapping_class_inheritance(args.first, &block)
|
|
107
107
|
else
|
|
108
108
|
super
|
|
109
109
|
end
|
|
@@ -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
|
#
|