class Lutaml::Xml::DeclarationPlanner
CRITICAL: This planner ONLY builds trees. NO flat mode.
The tree is isomorphic to XmlDataModel for index-based parallel traversal.
Builds ElementNode tree with W3C-compliant attribute prefix decisions.
Phase 2: Declaration Planning
def build_child_hoisted_declarations(attr_def, needs, _options,
-
(Hash- Hoisted declarations {prefix => uri})
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
attr_def(Attribute) -- The attribute definition for the child
def build_child_hoisted_declarations(attr_def, needs, _options, _class: nil) hoisted = {} # Get the child's own namespace needs child_needs = needs.child(attr_def.name) return hoisted unless child_needs # Add type attribute namespaces for the child element # CRITICAL: Type attribute namespaces MUST use prefix format (W3C rule) # NOTE: We only add type ATTRIBUTE namespaces, not type ELEMENT namespaces. # Type element namespaces are used by child elements and should be declared # on the parent element (or root, depending on namespace_scope). child_needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end # Get child's element namespace if available child_type = attr_def.type(mapper_class ? register_for(mapper_class) : @register) if child_type.is_a?(Class) && child_type < Lutaml::Model::Serialize child_reg = register_for(child_type) child_mapping = child_type.mappings_for(:xml, child_reg) if child_mapping&.namespace_class element_namespace = child_mapping.namespace_class # Only add if not already present unless hoisted.value?(element_namespace.uri) # For child elements, prefer default format (cleaner) hoisted[nil] = element_namespace.uri end end end hoisted end
def build_children_plans(xml_element, mapping, needs, options)
-
(Hash- Children plans by attribute name)
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping -
xml_element(XmlDataModel::XmlElement) -- Parent element
def build_children_plans(xml_element, mapping, needs, options) children_plans = {} # Get mapper_class to find child attributes mapper_class = options[:mapper_class] return children_plans unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) # Build namespace_classes hash for child plans namespace_classes = {} needs.all_namespace_classes.each do |ns_class| namespace_classes[ns_class.uri] = ns_class end # Iterate through XmlElement children xml_element.children.each do |xml_child| next unless xml_child.is_a?(Lutaml::Xml::DataModel::XmlElement) # Find the matching mapping rule for this child child_name = xml_child.name matching_rule = mapping.elements.find do |rule| rule.name.to_s == child_name end next unless matching_rule # Get the attribute definition for this child attr_def = mapper_class.attributes[matching_rule.to] next unless attr_def # Build child's hoisted declarations # CRITICAL: Child elements with type namespace attributes need those # namespaces declared on themselves. Get type attribute namespaces from # child's own needs (stored in needs.children) child_hoisted = build_child_hoisted_declarations(attr_def, needs, options, mapper_class: mapper_class) # Create child DeclarationPlan with the child's namespace info # Children inherit parent's namespace_classes child_plan = DeclarationPlan.new( root_node: DeclarationPlan::ElementNode.new( qualified_name: xml_child.name, use_prefix: nil, hoisted_declarations: child_hoisted, ), global_prefix_registry: build_prefix_registry(needs), input_formats: {}, namespace_classes: namespace_classes, ) # Store by attribute name children_plans[attr_def.name] = child_plan end children_plans end
def build_children_plans_from_metadata(mapping, needs, options)
-
(Hash- Children plans by attribute name)
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping
def build_children_plans_from_metadata(mapping, needs, options) children_plans = {} # Get mapper_class to find child attributes mapper_class = options[:mapper_class] return children_plans unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) # Build namespace_classes hash for child plans namespace_classes = {} needs.all_namespace_classes.each do |ns_class| namespace_classes[ns_class.uri] = ns_class end # Get parent's hoisted declarations for child inheritance parent_hoisted = build_parent_hoisted_for_children(mapping, needs, options) # Iterate through mapper_class attributes mapper_class.attributes.each_value do |attr_def| # Check if attribute has a Serializable type attr_type = attr_def.type(register_for(mapper_class)) next unless attr_type next unless attr_type.is_a?(Class) next unless attr_type < Lutaml::Model::Serialize # Create child DeclarationPlan with parent's namespace_classes # Child inherits parent's hoisted declarations child_plan = DeclarationPlan.new( root_node: DeclarationPlan::ElementNode.new( qualified_name: "", use_prefix: nil, hoisted_declarations: parent_hoisted, ), global_prefix_registry: build_prefix_registry(needs), input_formats: {}, namespace_classes: namespace_classes, ) # Store by attribute name children_plans[attr_def.name] = child_plan end children_plans end
def build_collection_item_plans(collection, _mapping, _needs)
-
(Hash- Children plans by item index)
Parameters:
-
needs(NamespaceNeeds) -- namespace needs from collector -
mapping(Xml::Mapping) -- the XML mapping -
collection(Collection) -- the collection object
def build_collection_item_plans(collection, _mapping, _needs) children_plans = {} return children_plans unless collection.is_a?(Array) # Get the item type from the collection class item_type = begin collection.class.instance_type rescue StandardError nil end collection.each_with_index do |item, index| next unless item # Get the item's mapper class item_mapper_class = if item.is_a?(Lutaml::Model::Serializable) item.class elsif item_type.is_a?(Class) item_type else next end # Get the item's XML mapping item_reg = register_for(item_mapper_class) item_mapping = item_mapper_class.mappings_for(:xml, item_reg) next unless item_mapping # Collect namespace needs for this item collector = NamespaceCollector.new(item_reg) item_needs = collector.collect(item, item_mapping) # Build a plan for this item item_plan = plan(item, item_mapping, item_needs) children_plans[index] = item_plan if item_plan end children_plans end
def build_element_node(xml_element, mapping, needs, options,
-
(ElementNode)- Element node with all decisions
Parameters:
-
parent_hoisted(Hash) -- Namespaces hoisted on parent {prefix => uri} -
parent_namespace_prefix(String, nil) -- Parent's actual namespace prefix -
parent_namespace_class(Class, nil) -- Parent's namespace class -
parent_format(Symbol, nil) -- Parent's namespace format (:default or :prefix) -
is_root(Boolean) -- Whether this is the root element -
parent_node(ElementNode, nil) -- Parent element node -
options(Hash) -- Serialization options (may contain :input_formats) -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping -
xml_element(XmlDataModel::XmlElement) -- Element to plan
def build_element_node(xml_element, mapping, needs, options, nt_node: nil, is_root: false, parent_format: nil, parent_namespace_class: nil, parent_namespace_prefix: nil, parent_hoisted: {}, element_path: []) # Determine element's prefix (checks input_formats, parent context for preservation) # Priority: # 1. Lutaml::Xml::XmlElement (from parsed XML): has namespace_prefix_explicit # 2. DataModel::XmlElement (from serialization): has @__xml_namespace_prefix element_prefix_explicit = xml_element.is_a?(Lutaml::Xml::XmlElement) && xml_element.namespace_prefix_explicit # For ROOT elements: don't use @__xml_namespace_prefix from XmlElement. # The root's prefix should be determined by the DecisionEngine (model's prefix_default). # This ensures mixed content roots use their namespace's default prefix, not the input prefix. # For NESTED elements: use @__xml_namespace_prefix if set (supports doubly-defined namespaces). element_used_prefix = if is_root nil elsif xml_element.is_a?(Lutaml::Xml::XmlElement) xml_element.namespace_prefix else xml_element.xml_namespace_prefix end element_prefix = determine_element_prefix( xml_element, mapping, needs, options, is_root: is_root, parent_format: parent_format, parent_namespace_class: parent_namespace_class, parent_namespace_prefix: parent_namespace_prefix, parent_hoisted: parent_hoisted, element_prefix_explicit: element_prefix_explicit, element_used_prefix: element_used_prefix ) # Determine hoisted xmlns declarations at this element # CRITICAL: Pass element_prefix to avoid calling determine_element_prefix twice # which could return different results due to context differences hoisted = determine_hoisted_declarations(xml_element, mapping, needs, options, is_root: is_root, parent_hoisted: parent_hoisted, element_prefix: element_prefix, element_path: element_path, parent_namespace_class: parent_namespace_class, parent_namespace_prefix: parent_namespace_prefix) # For ROOT element with a stored input plan, merge additional declarations # from the stored plan that the normal computation didn't include. # This preserves namespace prefix declarations from input XML that # the recomputation would not produce. Specifically: # - Doubly-defined: xmlns="..." AND xmlns:b="..." on the same element # - Non-default prefix: xmlns:xyzabc="..." when model uses prefix_default "a:" # # Only merge when the root element itself did NOT use a prefix in the input. # When the root's element name had a prefix (e.g., <examplecom:schema>), # the deserialization sets @__xml_namespace_prefix on the root XmlElement, # and element_builder clears child prefixes — so the stored plan's # prefix declarations are NOT needed by any child element. if is_root && !options.key?(:use_prefix) stored_plan = options[:stored_xml_declaration_plan] if stored_plan.is_a?(Lutaml::Xml::DeclarationPlan) && stored_plan.root_node.is_a?(Lutaml::Xml::DeclarationPlan::ElementNode) root_has_input_prefix = xml_element.xml_namespace_prefix.to_s != "" unless root_has_input_prefix stored_hoisted = stored_plan.root_node.hoisted_declarations stored_original_uris = stored_plan.original_namespace_uris || {} stored_hoisted.each do |prefix, uri| effective_uri = stored_original_uris[uri] || uri already_present = hoisted.any? do |_k, v| v == effective_uri || v == uri end if already_present && !hoisted.key?(prefix) hoisted[prefix] = uri end end end end end # Determine if child needs xmlns="" (W3C compliance) # W3C XML Namespaces 1.0 §6.2: When parent has default namespace and child # has NO namespace (namespace_class is nil), child MUST explicitly opt out # with xmlns="" to prevent inheriting parent's default namespace. # # This applies when: # 1. Transformation marked the element as needing xmlns="" (explicit :blank) # 2. OR element has no namespace AND parent uses default format AND the default # namespace does NOT have element_form_default :qualified # # Note: The transformation sets @needs_xmlns_blank on the XmlElement when the child # model has explicit `namespace :blank` declaration. element_marked_blank = xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement) && xml_element.needs_xmlns_blank # Add xmlns="" when parent's effective namespace form is :qualified. # This implements the W3C XML Schema default behavior: # - element_form_default :qualified (explicit) → children with no namespace get xmlns="" # to opt out of parent's default namespace # - element_form_default :unqualified (explicit) → children inherit parent's ns, no xmlns="" # - not set (nil) → W3C default is :qualified, BUT children opt out when parent's # namespace doesn't have an explicit form set (nested conflict case) # # The parent's effective namespace form (default_ns_form) is: # - nil when no form was explicitly set on the parent's namespace # - :qualified when explicitly set to qualified # - :unqualified when explicitly set to unqualified default_ns_form = options[:default_ns_element_form_default] parent_ns_class = parent_namespace_class # Skip xmlns="" when parent explicitly set :unqualified. # Override original default_ns_form.nil? with parent_explicitly_unqualified check. parent_explicitly_unqualified = parent_ns_class&.element_form_default_set? && parent_ns_class.element_form_default == :unqualified # Original: default_ns_form.nil? (W3C default is :qualified, opt out with xmlns="") # Override: if parent explicitly set :unqualified, don't opt out (inherit parent's ns) implicit_blank_needs_xmlns = xml_element.namespace_class.nil? && parent_hoisted&.key?(nil) && default_ns_form.nil? && !parent_explicitly_unqualified child_needs_xmlns_blank = element_marked_blank || implicit_blank_needs_xmlns # Build schema_location_attr if root element and any namespace has schema_location schema_location_attr = nil if is_root schema_location_attr = build_schema_location_attr_for_needs(needs) end # Create ElementNode element_node = DeclarationPlan::ElementNode.new( qualified_name: build_qualified_element_name(xml_element, element_prefix), use_prefix: element_prefix, hoisted_declarations: hoisted, needs_xmlns_blank: child_needs_xmlns_blank, schema_location_attr: schema_location_attr, ) # Calculate this element's format for passing to children this_format = element_prefix.nil? ? :default : :prefix this_namespace = xml_element.namespace_class # Get this element's element_form_default for passing to children # This determines whether child elements inherit this element's namespace this_element_form_default = this_namespace&.element_form_default # Track the effective default namespace's element_form_default # This is used to determine if children should inherit the default namespace # If this element declares a default namespace (hoisted[nil]), use its element_form_default # ONLY if it was explicitly set. Otherwise use the parent's effective value. effective_default_ns_form = if hoisted.key?(nil) && this_namespace&.element_form_default_set? this_namespace.element_form_default else options[:default_ns_element_form_default] end # Plan ALL attributes (PRESERVES ORDER) xml_element.attributes.each do |xml_attr| attr_node = plan_attribute(xml_attr, xml_element, mapping, options) element_node.add_attribute_node(attr_node) end # Get mapper_class from options to match children to mapping rules mapper_class = options[:mapper_class] attributes = if mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) mapper_class.attributes else {} end # Recursively plan ALL children (PRESERVES ORDER, mark as NOT root, pass parent context) xml_element.children.each do |xml_child| next unless xml_child.is_a?(Lutaml::Xml::DataModel::XmlElement) # Match child XmlElement to its mapping rule to get correct child mapping child_name = xml_child.name matching_rule = mapping.elements.find do |rule| rule.name.to_s == child_name end child_mapping = mapping # Default to parent mapping child_options = options if matching_rule && attributes.any? # Get child's mapper_class and mapping attr_def = attributes[matching_rule.to] if attr_def child_reg = mapper_class ? register_for(mapper_class) : @register child_type = attr_def.type(child_reg) if child_type.is_a?(Class) && child_type < Lutaml::Model::Serialize child_type_reg = register_for(child_type) child_mapping_obj = child_type.mappings_for(:xml, child_type_reg) if child_mapping_obj child_mapping = child_mapping_obj # CRITICAL: Pass parent's mapping so child can find its attribute name # NOTE: do NOT propagate use_prefix to child elements # use_prefix only applies to root element, not to children # Children should use their own namespace's default presentation use_prefix_value = options[:use_prefix] base_child_options = { mapper_class: child_type, parent_mapping: mapping, parent_element_form_default: this_element_form_default, default_ns_element_form_default: effective_default_ns_form, } if use_prefix_value == true # Do NOT propagate use_prefix: true to children # Children should use their own namespace's default presentation elsif use_prefix_value.is_a?(String) # Custom string prefix is specific to root's namespace - don't propagate else # use_prefix: false or nil - don't propagate end child_options = options.except(:use_prefix).merge(base_child_options) end end end end child_node = build_element_node( xml_child, child_mapping, needs, child_options, parent_node: element_node, is_root: false, parent_format: this_format, parent_namespace_class: this_namespace, parent_namespace_prefix: element_prefix, parent_hoisted: parent_hoisted.merge(hoisted), element_path: element_path + [xml_child.name] ) element_node.add_element_node(child_node) end element_node end
def build_parent_hoisted_for_children(mapping, needs, options)
-
(Hash- Hoisted declarations {prefix => uri})
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping
def build_parent_hoisted_for_children(mapping, needs, options) hoisted = {} # Get element's own namespace (from mapping's namespace_class) element_namespace = mapping&.namespace_class # Process namespace_scope configurations needs.namespace_scope_configs.each do |scope_config| ns_class = scope_config.namespace_class next if ns_class == element_namespace ns_usage = needs.namespace(ns_class.to_key) should_declare = scope_config.always_mode? || (scope_config.auto_mode? && ns_usage&.used_in&.any?) if should_declare prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end end # Add element's own namespace (if not already added) if element_namespace && !hoisted.value?(element_namespace.uri) use_prefix_option = options[:use_prefix] case use_prefix_option when true prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = element_namespace.uri when String hoisted[use_prefix_option] = element_namespace.uri when false hoisted[nil] = element_namespace.uri else # Default: prefer default format (cleaner) hoisted[nil] = element_namespace.uri end end # Add type namespaces # Type attribute namespaces needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end # Type element namespaces needs.type_element_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end # Add remaining namespaces needs.all_namespace_classes.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) scope_config = needs.scope_config_for(ns_class) next if scope_config hoisted[nil] = ns_class.uri end hoisted end
def build_prefix_registry(needs)
-
(Hash- URI => prefix mapping)
Parameters:
-
needs(NamespaceNeeds) -- Namespace needs
def build_prefix_registry(needs) registry = {} needs.namespaces.each_value do |ns_usage| ns_class = ns_usage.namespace_class if ns_class.prefix_default registry[ns_class.uri] = ns_class.prefix_default end end registry end
def build_qualified_element_name(xml_element, prefix)
-
(String)- Qualified name: "prefix:name" or "name"
Parameters:
-
prefix(String, nil) -- Prefix (nil = no prefix) -
xml_element(XmlDataModel::XmlElement) -- Element
def build_qualified_element_name(xml_element, prefix) if prefix "#{prefix}:#{xml_element.name}" else xml_element.name end end
def build_schema_location_attr_for_needs(needs)
-
(Hash, nil)- { "xsi:schemaLocation" => "uri1 loc1 uri2 loc2" } or nil
Parameters:
-
needs(NamespaceNeeds) -- Namespace needs
def build_schema_location_attr_for_needs(needs) return nil unless needs # Collect all namespace classes that have schema_location namespaces_with_schema = needs.all_namespace_classes.select do |ns_class| ns_class.is_a?(Class) && ns_class < Lutaml::Xml::Namespace && ns_class.schema_location end return nil if namespaces_with_schema.empty? # Build xsi:schemaLocation value value = namespaces_with_schema.map do |ns_class| "#{ns_class.uri} #{ns_class.schema_location}" end.join(" ") # Get XSI prefix from hoisted declarations or use default xsi_prefix = W3c::XsiNamespace.prefix_default || "xsi" { "#{xsi_prefix}:schemaLocation" => value } end
def determine_element_prefix(xml_element, mapping, needs, options,
def determine_element_prefix(xml_element, mapping, needs, options, is_root: false, parent_format: nil, parent_namespace_class: nil, parent_namespace_prefix: nil, parent_hoisted: {}, element_prefix_explicit: false, element_used_prefix: nil) # CRITICAL: Check for Type namespace FIRST # Type namespaces are declared on Type::Value subclasses and used as # prefixes on child elements. They take precedence over element namespace. type_ns_class = find_type_namespace_for_element(xml_element, mapping, needs, options) if type_ns_class # If parent uses default format AND Type namespace matches parent's namespace, # inherit parent's namespace (return nil for prefix) if parent_format == :default && parent_namespace_class&.uri == type_ns_class.uri return nil end # CRITICAL: Check if parent hoisted this type namespace with a custom prefix # When user specifies prefix: "custom", the parent hoists the namespace # with that custom prefix, and child elements must use the same prefix parent_prefix = parent_hoisted.find do |_prefix, uri| uri == type_ns_class.uri end&.first if parent_prefix return parent_prefix end # CRITICAL: Check if user specified an explicit custom prefix option # This handles the case where parent hasn't hoisted yet AND the type namespace # matches the parent's namespace (i.e., they share the same namespace) use_prefix_option = options[:use_prefix] if use_prefix_option && parent_namespace_class && parent_namespace_class.uri == type_ns_class.uri # Parent has the same namespace as the type namespace # Use the prefix option to maintain consistency case use_prefix_option when String return use_prefix_option when true return parent_namespace_class.prefix_default when false return nil end end # Default: use the Type namespace's prefix_default return type_ns_class.prefix_default end # If no Type namespace, check element's own namespace return nil unless xml_element.namespace_class # NEW: If element used explicit prefix during deserialization, use that prefix. # This handles doubly-defined namespaces where <a:item> and <b:item> both # map to same URI but need different prefixes. # Check: (a) from XmlElement (root), (b) from NamespaceUsage (children) used_prefix = element_used_prefix unless used_prefix # Look up from NamespaceUsage in needs (set during collection from model instance). # CRITICAL: Only use NamespaceUsage.used_prefix when the XmlElement is a # Lutaml::Xml::XmlElement wrapper (from original parsed XML, has namespace_prefix_explicit). # Do NOT use it when the XmlElement is a DataModel::XmlElement (from transformation) # because NamespaceUsage.used_prefix may have been set from a parent/r sibling element. # For DataModel::XmlElement, the prefix must come from @__xml_namespace_prefix on the # XmlElement itself (which is set during transformation for doubly-defined case). ns_key = xml_element.namespace_class.to_key ns_usage = needs.namespaces[ns_key] ns_from_wrapper = xml_element.is_a?(Lutaml::Xml::XmlElement) && xml_element.namespace_prefix_explicit if ns_from_wrapper used_prefix = ns_usage&.used_prefix end end # For the ROOT element: always use DecisionEngine (model's prefix_default). # The root's format should be determined by the namespace's default, not the input. # This prevents mixed content roots from using input prefixes like "examplecom:". # # For CHILD elements: use input prefix when it differs from model default. # This handles doubly-defined namespaces where input uses "xyzabc:" but # the input uses a different prefix than the model defines (e.g., input has # "xyzabc:" but model has prefix_default "a:"). # # When used_prefix == model_default_prefix, fall through to DecisionEngine # to preserve the original format decision (default vs prefix). if used_prefix && !is_root ns_class = xml_element.namespace_class model_default_prefix = ns_class.prefix_default # Only use used_prefix when it differs from model default # This preserves arbitrary input prefixes (xyzabc:) while allowing # the DecisionEngine to decide format when input matches model default. if model_default_prefix.nil? || used_prefix != model_default_prefix return used_prefix end end # Use the OOP decision resolver @prefix_resolver ||= Decisions::ElementPrefixResolver.new decision = @prefix_resolver.resolve_with_decision( xml_element, mapping, needs, options, is_root: is_root, parent_format: parent_format, parent_namespace_class: parent_namespace_class, parent_namespace_prefix: parent_namespace_prefix, parent_hoisted: parent_hoisted, element_used_prefix: element_used_prefix ) decision.prefix end
def determine_hoisted_declarations(xml_element, mapping, needs,
-
(Hash- xmlns attributes: {prefix_or_nil => uri})
Parameters:
-
parent_namespace_prefix(String, nil) -- Parent's actual namespace prefix -
parent_namespace_class(Class, nil) -- Parent's namespace class -
element_path(Array) -- Element path in the tree for namespace_locations lookup -
parent_hoisted(Hash) -- Namespaces hoisted on parent {prefix => uri} -
is_root(Boolean) -- Whether this is the root element -
options(Hash) -- Serialization options (may contain :input_formats) -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping -
xml_element(XmlDataModel::XmlElement) -- Element
def determine_hoisted_declarations(xml_element, mapping, needs, ons, is_root: false, parent_hoisted: {}, element_prefix: nil, element_path: [], parent_namespace_class: nil, parent_namespace_prefix: nil) hoisted = {} # Compute element_used_prefix the same way it's done in build_element_node. # This is needed because determine_element_prefix is called again inside this method, # and it needs element_used_prefix to correctly determine the prefix. element_used_prefix = if is_root nil elsif xml_element.is_a?(Lutaml::Xml::XmlElement) xml_element.namespace_prefix else xml_element.xml_namespace_prefix end # CRITICAL: Get the current element's namespace_scope_configs. # For child elements, use their own namespace_scope, not the parent's. current_scope_configs = get_element_namespace_scope_configs( xml_element, mapping, needs, options ) # Check if element's namespace is hoisted to root via namespace_scope element_ns_hoisted_to_root = false if xml_element.namespace_class scope_config = find_scope_config_for(xml_element.namespace_class, current_scope_configs) if scope_config ns_usage = needs.namespaces[xml_element.namespace_class.to_key] element_ns_hoisted_to_root = scope_config.always_mode? || (scope_config.auto_mode? && ns_usage&.used_in&.any?) end end # Check if element's namespace was already hoisted on parent (locally) if xml_element.namespace_class ns_uri = xml_element.namespace_class.uri parent_hoisted.value?(ns_uri) end # FIRST: Add element's OWN namespace if it has one # This ensures the namespace is declared with the correct format (prefix or default) # even when it's already hoisted by parent, to maintain consistency. if xml_element.namespace_class ns_class = xml_element.namespace_class ns_uri = ns_class.uri # Check if namespace_locations has an original URI that should be used instead. # namespace_locations is keyed by element path (e.g., "child/grandchild") # matching the key format from collect_element_namespaces in model_transform.rb. # Use element_path for lookup, not xml_element.name (which is just the local name). # # For round-trip fidelity, ALWAYS use the alias URI from stored plan when available. # The use_prefix option only affects element format (prefix vs default), not the URI. # Users expect round-trip to preserve the exact namespace URI from input. stored_plan = options[:stored_xml_declaration_plan] if stored_plan # For child elements, look up by element path # For root elements (empty path), use namespaces_at_path([]) to get root declarations loc_key = element_path.join("/") element_ns_loc = if is_root stored_plan.namespaces_at_path([]) else stored_plan.namespace_locations&.dig(loc_key) end # Check if any URI in the stored location is an alias of the canonical URI element_ns_loc&.each_value do |stored_uri| if ns_class.is_alias?(stored_uri) # Use the original alias URI instead of canonical ns_uri = stored_uri break end end end # Check if namespace is hoisted on parent # When namespace has uri_aliases, the parent may have declared a different # URI (alias or canonical) than what this element wants to use. # Rules: # 1. Exact match → already hoisted, don't re-declare # 2. Parent declared alias, child wants canonical → already hoisted (alias covers canonical) # 3. Parent declared canonical/alias-A, child wants alias-B → NOT hoisted (child must declare its alias) # This ensures round-trip fidelity: if input used alias URI, output preserves it. ns_hoisted_by_parent = parent_hoisted.value?(ns_uri) if !ns_hoisted_by_parent && ns_class.is_a?(Class) && ns_class < Lutaml::Xml::Namespace && ns_uri == ns_class.uri # child is using canonical URI # Parent declared an alias of this namespace — child doesn't need to re-declare # because the alias already establishes the namespace binding ns_hoisted_by_parent = ns_class.uri_aliases.any? do |alias_uri| parent_hoisted.value?(alias_uri) end end # Check if namespace is hoisted to root via namespace_scope (for non-root elements) element_ns_hoisted_to_root && !is_root # Determine the prefix to use for this namespace if !ns_hoisted_by_parent # Namespace is NOT already hoisted by parent - need to determine prefix element_prefix = determine_element_prefix(xml_element, mapping, needs, options, is_root: is_root, parent_hoisted: parent_hoisted, element_used_prefix: element_used_prefix, parent_namespace_class: parent_namespace_class, parent_namespace_prefix: parent_namespace_prefix) # W3C elementFormDefault="unqualified": prefer prefix format so children # can be in blank namespace (no xmlns attribute). Only for non-root elements # since elementFormDefault only applies to local elements. # CRITICAL: Only applies when explicitly set, not when defaulted to :unqualified. if element_prefix.nil? && ns_class.element_form_default_set? && ns_class.element_form_default == :unqualified && !is_root element_prefix = ns_class.prefix_default || "ns" end hoisted[element_prefix] = ns_uri elsif element_prefix # Namespace is already hoisted by parent, and we have an explicit prefix # Check if parent has the SAME prefix declaration (accounting for URI aliases) parent_uri_for_prefix = parent_hoisted[element_prefix] all_uris = ns_class.is_a?(Class) && ns_class < Lutaml::Xml::Namespace ? ns_class.all_uris : [ns_uri] parent_has_same_prefix = parent_uri_for_prefix && all_uris.include?(parent_uri_for_prefix) if parent_has_same_prefix # Parent already declared this namespace with the same prefix - don't re-declare # Just keep track that we're using the parent's prefix (no need to add to hoisted) else # Parent has different prefix or no prefix - add our declaration hoisted[element_prefix] = ns_uri end else # Namespace is hoisted by parent, and we don't have an explicit prefix # W3C XML Namespaces 1.0 §6.2: Child elements inherit namespace from parent # DO NOT re-declare the namespace - the child will inherit it # This prevents redundant xmlns declarations on nested elements with same namespace end # CRITICAL FIX: If root element uses default format (nil prefix) and child elements # need prefix format for the same namespace (due to form: :qualified), also declare # the namespace with prefix format on the root element. # This allows child elements to use the prefix without declaring it locally. # IMPORTANT: This only applies to the element's OWN namespace, not type namespaces. # Type namespaces should be declared locally on the element that uses the type. # Check if any child elements have form: :qualified for this namespace # by checking the children's form attribute in the XmlElement tree # # ADDITIONAL CHECK: Skip if namespace was NOT declared at root in input XML. # Use namespaces_at_path([]) to verify this namespace was actually at root level. # Note: namespace_locations only has child paths, not root. Root namespaces # are stored in root_node.hoisted_declarations, which namespaces_at_path returns. if is_root && hoisted.key?(nil) && hoisted[nil] == ns_uri && xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement) stored_plan = options[:stored_xml_declaration_plan] root_ns_at_input = stored_plan&.namespaces_at_path([]) namespace_was_at_root = if root_ns_at_input root_ns_at_input.value?(ns_uri) else true # No location data, assume it was at root end if namespace_was_at_root child_needs_prefix = xml_element.children.any? do |child| next unless child.is_a?(Lutaml::Xml::DataModel::XmlElement) # Check if child has form: :qualified and same namespace child.form == :qualified && child.namespace_class&.uri == ns_uri end if child_needs_prefix prefix = ns_class.prefix_default hoisted[prefix] = ns_uri end end end end # SECOND: Add namespace_scope namespaces (ONLY at root!) if is_root stored_plan = options[:stored_xml_declaration_plan] current_scope_configs.each do |scope_config| ns_class = scope_config.namespace_class ns_uri = ns_class.uri # Skip if already added (element's own namespace) next if ns_class == xml_element.namespace_class # CRITICAL: Skip if namespace URI is already in hoisted hash # An element can declare the same URI with both default and prefix formats next if hoisted.value?(ns_uri) || hoisted.key?(ns_class.prefix_default) # NEW: Skip if namespace was NOT declared at root in input XML. # Use namespace_locations to verify this namespace was actually at root level. # If namespace_locations shows it was only at child paths, do NOT hoist to root. root_ns_at_input = stored_plan&.namespaces_at_path([]) if root_ns_at_input && !root_ns_at_input.value?(ns_uri) # Namespace was not declared at root in input - skip hoisting next end # Check :always mode or :auto mode with usage ns_usage = needs.namespace(ns_class.to_key) should_declare_here = scope_config.always_mode? || (scope_config.auto_mode? && ns_usage&.used_in&.any?) if should_declare_here prefix = ns_class.prefix_default hoisted[prefix] = ns_uri end end end # THIRD: Add type namespaces # Type namespaces are declared on PARENT elements and used by child elements. # W3C rule: Namespaces in attributes MUST use prefix format. # Type namespaces for elements MUST also be declared at root with prefix. # Example: ContactInfo declares xmlns:name for personName's name:prefix attribute. # Example: Document declares xmlns:dc for title's dc:title element. # # CRITICAL: Type namespaces respect namespace_scope directive. # When namespace_scope is configured, hoist type namespaces to root. # When namespace_scope is NOT configured, type namespaces are declared # locally on the elements that use them. if is_root # Check if namespace_scope is configured has_namespace_scope = !current_scope_configs.empty? # Only hoist type namespaces to root when namespace_scope is configured if has_namespace_scope # Type attribute namespaces needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # If namespace_scope is configured, only hoist if in scope scope_config = find_scope_config_for(ns_class, current_scope_configs) next unless scope_config # Type attribute namespaces MUST use prefix format (W3C rule) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end # Type element namespaces needs.type_element_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # CRITICAL: Type namespaces are different from child element namespaces. # Type namespaces are declared on Type::Value subclasses (via xml_namespace # directive) and MUST ALWAYS be hoisted to root with prefix format (W3C # constraint: only one default namespace per element). # # The restriction below applies ONLY to child element namespaces (from nested # models), NOT to Type namespaces. Type namespaces are about TYPE identity, # not about element structure. # # Examples of child element namespaces (should NOT be hoisted if different): # - Root has XMI namespace, child model has XMI_NEW namespace → child declares # - Root has NO namespace, child model has XMI namespace → child declares # # Examples of Type namespaces (should ALWAYS be hoisted): # - Root has any namespace, attribute type has XMI namespace → hoist XMI # - Root has NO namespace, attribute type has XMI namespace → hoist XMI # If namespace_scope is configured, only hoist if in scope scope_config = find_scope_config_for(ns_class, current_scope_configs) next unless scope_config # Type element namespaces MUST use prefix format prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end else # Root element without namespace_scope: hoist type namespaces # Type namespaces MUST use prefix format (W3C rule) # # CRITICAL: Don't hoist type namespaces that are also child element # namespaces. When a type namespace is also a child's element namespace, it # should be declared on that child element (not hoisted to root). # Type element namespaces (for Type::Value classes used by root's attributes) needs.type_element_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # Check if this namespace is used by any child element as its element namespace # If so, don't hoist - let the child declare it locally # UNLESS the parent also uses this namespace for its attributes child_uses_namespace = needs.children&.any? do |_attr_name, child_needs| child_needs.namespaces.any? do |_key, ns_usage| ns_usage.used_in_elements? && ns_usage.namespace_class.uri == ns_uri end end # Only skip hoisting if child uses namespace AND parent doesn't use it for attributes # Check if parent element instance has attributes with this namespace parent_has_attr_with_ns = if xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement) xml_element.attributes.any? do |xml_attr| next false unless xml_attr.namespace_class xml_attr.namespace_class.uri == ns_uri end else # For nil/Class root_element, check if mapping has attributes with this namespace # This handles the case where parent doesn't have the attribute set # Only hoist if parent's MAPPING declares attributes with this namespace mapping.attributes.any? do |attr_rule| next false unless attr_rule.attribute? # Get the mapper_class to find attribute definition mapper_class = options[:mapper_class] || mapping.owner next false unless mapper_class next false unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) attr_def = mapper_class.attributes[attr_rule.to] next false unless attr_def # Check if this attribute's type has the namespace type_ns_class = attr_def.type_namespace_class(register_for(mapper_class)) type_ns_class&.uri == ns_uri end end next if child_uses_namespace && !parent_has_attr_with_ns prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end # Type attribute namespaces (for Type::Value classes used in child element attributes) # CRITICAL: Don't hoist if namespace is also child element's namespace needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # Check if this namespace is used by any child element as its element namespace # If so, don't hoist - let the child declare it locally child_uses_namespace = needs.children&.any? do |_attr_name, child_needs| child_needs.namespaces.any? do |_key, ns_usage| ns_usage.used_in_elements? && ns_usage.namespace_class.uri == ns_uri end end # Only skip hoisting if child uses namespace AND parent doesn't use it for attributes parent_has_attr_with_ns = if xml_element.is_a?(Lutaml::Xml::DataModel::XmlElement) xml_element.attributes.any? do |xml_attr| next false unless xml_attr.namespace_class xml_attr.namespace_class.uri == ns_uri end else # For nil/Class root_element, check if mapping has attributes with this namespace mapping.attributes.any? do |attr_rule| next false unless attr_rule.attribute? mapper_class = options[:mapper_class] || mapping.owner next false unless mapper_class next false unless mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) attr_def = mapper_class.attributes[attr_rule.to] next false unless attr_def type_ns_class = attr_def.type_namespace_class(register_for(mapper_class)) type_ns_class&.uri == ns_uri end end next if child_uses_namespace && !parent_has_attr_with_ns prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end end else # For non-root (child) elements, add their own type namespaces # CRITICAL: Child elements with type namespace attributes need those # namespaces declared on themselves (W3C compliance) # # Get the child's own namespace needs by matching the current element # to its corresponding attribute name in the parent's mapping child_attr_name = find_child_attribute_name(xml_element, mapping, options) if child_attr_name child_needs = needs.child(child_attr_name) if child_needs # Add child's type element namespaces (for Type::Value attributes) # These need to be hoisted to the child element child_needs.type_element_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if parent_hoisted.value?(ns_uri) # Skip if parent already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # Type namespaces MUST use prefix format (W3C rule) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end # Add child's type attribute namespaces # CRITICAL: Skip if namespace is child's own element namespace # The element's namespace declaration already covers it child_needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already declared next if parent_hoisted.value?(ns_uri) # Skip if parent already declared next if ns_class == xml_element.namespace_class # Skip element's own namespace # Type attribute namespaces MUST use prefix format (W3C rule) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_uri end end end end # FOURTH: Add namespaces NOT in namespace_scope (LOCAL hoisting) # W3C minimal-subtree principle: declare namespace at first element using it if !is_root needs.namespaces.each_value do |ns_usage| ns_class = ns_usage.namespace_class # Skip if already added (element's own namespace) next if ns_class == xml_element.namespace_class # Skip if already hoisted on parent (don't redeclare!) next if parent_hoisted.value?(ns_class.uri) # CRITICAL: Skip if this namespace is ONLY used by child elements (not by # the current element). Child elements should declare their own namespace # locally using default format. But if the current element uses this namespace # for its attributes, it should be hoisted. # Check: if NOT used in attributes AND used in elements → skip hoisting # NOTE: Also check type_attribute_namespaces (attribute type namespaces) used_in_attributes = ns_usage.used_in_attributes? || needs.type_attribute_namespaces.include?(ns_class) next if !used_in_attributes && ns_usage.used_in_elements? scope_config = find_scope_config_for(ns_class, current_scope_configs) # Only hoist if NOT in namespace_scope (local hoisting) # Check if any child/grandchild uses this namespace if !scope_config && element_needs_namespace?(xml_element, ns_class) prefix = ns_class.prefix_default hoisted[prefix] = ns_class.uri end end end # PRESERVATION: For root element, add stored input namespace declarations # that were declared AT ROOT in the original XML. # This includes namespaces like xmlns:xi that were in input at root level # but are not "needed" by model. These namespaces may be required for # downstream processing (XInclude, XSchema, etc.) # # For doubly-defined namespaces (same URI with different prefixes), we allow # each prefix variant to be preserved if used by children. # # CRITICAL: Only preserve namespaces that were ORIGINALLY declared at root. # Namespaces declared on child elements in the input should remain on children. # NOTE: PRESERVATION should always run when we have a stored plan, because # unused namespaces (like xmlns:xsi for schemaLocation) should be preserved # for round-trip fidelity regardless of prefix formatting options. stored_plan = options[:stored_xml_declaration_plan] if is_root && stored_plan # Get root-level namespaces from location tracking. # This properly distinguishes namespaces declared at root vs hoisted there. root_level_namespaces = stored_plan&.namespaces_at_path([]) # Track namespaces that are in child paths (not root) in namespace_locations # These should NOT be on root in the input XML child_only_namespaces = {} stored_plan&.namespace_locations&.each do |path, ns_hash| next if path.empty? # Skip root path ns_hash.each do |prefix, uri| child_only_namespaces[[prefix, uri]] = true end end # Get namespace_locations to verify which namespaces were actually declared at root # in the input XML. This prevents hoisting namespaces that were declared on child # elements but got incorrectly added to root's hoisted during FIRST phase. input_root_ns = {} if stored_plan&.namespace_locations # Aggregate all namespaces from child paths child_ns = {} stored_plan.namespace_locations.each_value do |ns_hash| ns_hash.each do |prefix, uri| child_ns[[prefix, uri]] = true end end # Namespaces in root's hoisted but NOT in any child path were actually at root root_level_namespaces&.each do |prefix, uri| if prefix != :not_root && !child_ns.key?([ prefix, uri ]) input_root_ns[prefix] = uri end end end if input_root_ns.any? root_level_namespaces = input_root_ns end if root_level_namespaces root_level_namespaces.each do |prefix, uri| # Skip if same prefix already used for different URI next if hoisted.key?(prefix) && hoisted[prefix] != uri # Skip if this namespace is a child element's OWN element namespace # (not a type namespace). Children should declare their own namespace # locally rather than having it hoisted to root. child_own_element_ns = needs.children&.any? do |_attr_name, child_needs| child_needs.namespaces.values.any? do |ns_usage| ns_usage.namespace_class.uri == uri && ns_usage.used_in_elements? && needs.type_namespace_classes.none? { |tc| tc.uri == uri } end end next if child_own_element_ns # Check if children use this specific prefix variant # This enables doubly-defined namespace preservation child_uses_this_prefix = needs.children&.any? do |_attr_name, child_needs| child_needs.namespaces.values.any? do |ns_usage| ns_usage.used_prefix == prefix && ns_usage.namespace_class.uri == uri end end # Hoist if: # - URI not yet declared (no prefix+URI combo), OR # - This specific prefix variant is used by children (doubly-defined ns) uri_already_hoisted = hoisted.value?(uri) if !uri_already_hoisted || child_uses_this_prefix hoisted[prefix] = uri end end elsif stored_plan&.root_node&.hoisted_declarations # Fallback: Legacy behavior for plans without location data # Only add namespaces that are NOT needed by any child # # CRITICAL: If namespace_locations has child entries but root path is empty, # it means the root's own namespaces were NOT captured during parsing. # In this case, we should NOT add anything from fallback to avoid # incorrectly adding child namespaces to root. root_path_in_locations = stored_plan.namespace_locations&.key?("") if root_path_in_locations && stored_plan.namespace_locations[""].to_h.empty? # Root path exists but is empty - skip fallback to avoid adding child namespaces to root else stored_hoisted = stored_plan.root_node.hoisted_declarations stored_hoisted.each do |prefix, uri| # Skip if this namespace is in child_only_namespaces # (declared on a child element in the input) next if child_only_namespaces.key?([prefix, uri]) # Skip if same prefix already used for different URI next if hoisted.key?(prefix) && hoisted[prefix] != uri # Check if any child element needs this namespace child_needs_ns = needs.children&.any? do |_attr_name, child_needs| child_needs.namespaces.any? do |_key, ns_usage| ns_usage.namespace_class&.uri == uri end end next if child_needs_ns # Safe to preserve at root hoisted[prefix] = uri end end end end # FIFTH: Add XSI namespace if any Namespace in scope has schema_location # Schema location is handled by DeclarationPlan.build_schema_location_attr # which builds xsi:schemaLocation from all Namespace.schema_location values if namespaces_have_schema_location?(needs, options) xsi_uri = W3c::XsiNamespace.uri xsi_prefix = W3c::XsiNamespace.prefix_default || "xsi" hoisted[xsi_prefix] = xsi_uri unless hoisted.value?(xsi_uri) end hoisted end
def element_needs_namespace?(xml_element, ns_class)
-
(Boolean)- true if namespace is used in subtree
Parameters:
-
ns_class(Class) -- Namespace class to search for -
xml_element(XmlDataModel::XmlElement) -- Element to check
def element_needs_namespace?(xml_element, ns_class) # Check direct children xml_element.children.each do |child| next unless child.is_a?(Lutaml::Xml::DataModel::XmlElement) # Does child use this namespace? return true if child.namespace_class == ns_class # Does child have attributes using this namespace? child.attributes.each do |attr| return true if attr.namespace_class == ns_class end # Recurse to grandchildren return true if element_needs_namespace?(child, ns_class) end false end
def find_child_attribute_name(xml_element, mapping, options = {})
-
(Symbol, nil)- The attribute name, or nil if not found
Parameters:
-
options(Hash) -- Serialization options (may contain :parent_mapping) -
mapping(Xml::Mapping) -- The current element's mapping (parent's for child elements) -
xml_element(XmlDataModel::XmlElement) -- The child element
def find_child_attribute_name(xml_element, mapping, options = {}) # Use parent's mapping if available (for child elements) search_mapping = options[:parent_mapping] || mapping element_name = xml_element.name.to_s matching_rule = search_mapping.elements.find do |rule| rule.name.to_s == element_name end matching_rule&.to end
def find_scope_config_for(ns_class, scope_configs)
-
(NamespaceScopeConfig, nil)- The matching scope config, or nil
Parameters:
-
scope_configs(Array) -- The scope configs to search -
ns_class(Class) -- The namespace class
def find_scope_config_for(ns_class, scope_configs) scope_configs.find { |config| config.namespace_class == ns_class } end
def find_type_namespace_for_element(xml_element, mapping, needs,
-
(String, nil)- The prefix to use, or nil for default format
Parameters:
-
parent_hoisted(Hash) -- Namespaces hoisted on parent {prefix => uri} -
parent_namespace_class(Class, nil) -- Parent's namespace class -
parent_format(Symbol, nil) -- Parent's format (:prefix or :default) -
is_root(Boolean) -- Whether this is the root element -
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- XML mapping -
xml_element(XmlDataModel::XmlElement) -- Element
def find_type_namespace_for_element(xml_element, mapping, needs, ons) # Get mapper_class from options mapper_class = options[:mapper_class] return nil unless mapper_class # Get attributes from mapper_class attributes = if mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) mapper_class.attributes else {} end return nil unless attributes.any? # Find matching element rule element_name = xml_element.name.to_s matching_rule = mapping.elements.find do |rule| rule.name.to_s == element_name end return nil unless matching_rule # Get attribute definition attr_def = attributes[matching_rule.to] return nil unless attr_def # Look up type namespace from needs.type_namespaces needs.type_namespaces[attr_def&.name] end
def get_element_namespace_scope_configs(_xml_element, _mapping, needs,
-
(Array- The element's namespace_scope_configs)
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs -
mapping(Xml::Mapping) -- The element's mapping -
xml_element(XmlDataModel::XmlElement) -- The element
def get_element_namespace_scope_configs(_xml_element, _mapping, needs, ions) # Use the parent's namespace_scope_configs needs.namespace_scope_configs end
def initialize(register = nil)
-
register(Symbol) -- the register ID for type resolution
def initialize(register = nil) @register = register || Lutaml::Model::Config.default_register end
def namespaces_have_schema_location?(needs, _options)
-
(Boolean)- True if any namespace has schema_location
Parameters:
-
options(Hash) -- Serialization options -
needs(NamespaceNeeds) -- Namespace needs
def namespaces_have_schema_location?(needs, _options) return false unless needs # Check all namespace classes in needs needs.all_namespace_classes.any? do |ns_class| ns_class.is_a?(Class) && ns_class < Lutaml::Xml::Namespace && ns_class.schema_location end end
def native_type_element?(xml_element)
-
(Boolean)- true if element is a native type element
Parameters:
-
xml_element(XmlDataModel::XmlElement) -- Element to check
def native_type_element?(xml_element) # Native type elements have: # - text_content (not nil) # - No element children (all children are Strings, not XmlElement) return false unless xml_element.text_content xml_element.children.all?(String) end
def normalize_root_element(root_element, _mapping, options)
-
(XmlDataModel::XmlElement, nil, Class)- Normalized root element
Parameters:
-
options(Hash) -- Serialization options -
mapping(Xml::Mapping) -- XML mapping -
root_element(XmlDataModel::XmlElement, Model, nil, Class) -- Root element
def normalize_root_element(root_element, _mapping, options) return root_element if root_element.nil? || root_element.is_a?(Class) return root_element if root_element.is_a?(Lutaml::Xml::DataModel::XmlElement) # Check if root_element is a Model instance (has xml mapping) if root_element.is_a?(Lutaml::Model::Serialize) # Get mapper_class from options or infer from root_element mapper_class = options[:mapper_class] || root_element.class # Get mapping for the model class root_reg = register_for(mapper_class) mapping_dsl = mapper_class.mappings_for(:xml, root_reg) # Use Xml::Transformation to convert model to XmlElement # Pass register ID directly (Transformation handles Symbol) transformation = Xml::Transformation.new(mapper_class, mapping_dsl, :xml, root_reg) transformed = transformation.transform(root_element, options) # Return transformed XmlElement return transformed if transformed.is_a?(Lutaml::Xml::DataModel::XmlElement) end # Return as-is if no transformation needed root_element end
def plan(root_element, mapping, needs, parent_plan: nil, options: {},
-
(DeclarationPlan)- declaration plan with tree structure
Parameters:
-
options(Hash) -- serialization options (may contain :stored_xml_declaration_plan with input_formats) -
needs(NamespaceNeeds) -- namespace needs from collector -
mapping(Xml::Mapping) -- the XML mapping -
root_element(XmlDataModel::XmlElement, Model, nil, Class) -- root element, model instance, or nil/Class for unit testing
def plan(root_element, mapping, needs, parent_plan: nil, options: {}, ted_types: Set.new) # Normalize root_element: transform Model instances to XmlElement root_element = normalize_root_element(root_element, mapping, options) # Allow nil and Class for unit testing (type analysis without element instance) if root_element && !root_element.is_a?(Lutaml::Xml::DataModel::XmlElement) && !root_element.is_a?(Class) raise ArgumentError, "DeclarationPlanner ONLY works with XmlElement trees. Got: #{root_element.class}" end # Handle nil or Class root_element for unit testing if root_element.nil? || root_element.is_a?(Class) # CRITICAL: Resolve type namespace refs BEFORE using type_attribute_namespaces TypeNamespaceResolver.new(@register).resolve(needs) # Build namespace_classes hash from needs for unit testing namespace_classes = {} needs.all_namespace_classes.each do |ns_class| namespace_classes[ns_class.uri] = ns_class end # CRITICAL: Add namespace_scope namespaces to namespace_classes # (even if not used, :always mode requires them to be declared) needs.namespace_scope_configs.each do |scope_config| ns_class = scope_config.namespace_class namespace_classes[ns_class.uri] ||= ns_class end # Get element's own namespace (from mapping) element_namespace = mapping&.namespace_class # Create minimal root node with namespace hoisting info # W3C rule: Namespaces used in attributes MUST use prefix format hoisted = {} # FIRST: Process namespace_scope configurations (root-only) needs.namespace_scope_configs.each do |scope_config| ns_class = scope_config.namespace_class next if ns_class == element_namespace # Don't add root's own namespace here # Check :always mode or :auto mode with usage ns_usage = needs.namespace(ns_class.to_key) should_declare = scope_config.always_mode? || (scope_config.auto_mode? && ns_usage&.used_in&.any?) if should_declare prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end end # SECOND: Add element's own namespace (if not already added) if element_namespace && !hoisted.value?(element_namespace.uri) # Check if element's namespace is used in type attributes # If so, use prefix format (W3C rule: namespaces in attributes MUST use prefix) element_ns_in_attributes = needs.type_attribute_namespaces.any? do |ns| ns.uri == element_namespace.uri end # Check use_prefix option (Tier 1 priority) use_prefix_option = options[:use_prefix] if element_ns_in_attributes # Namespace used in attributes - MUST use prefix format prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = element_namespace.uri elsif use_prefix_option == true # Force prefix format when use_prefix: true prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = element_namespace.uri elsif use_prefix_option.is_a?(String) # Use custom prefix string hoisted[use_prefix_option] = element_namespace.uri elsif use_prefix_option == false # Force default format when use_prefix: false hoisted[nil] = element_namespace.uri elsif element_namespace.element_form_default_set? && element_namespace.element_form_default == :unqualified # W3C elementFormDefault="unqualified": local elements should be unqualified. # When parent uses prefix format, children can simply omit xmlns (blank namespace). # When parent uses default format, children need xmlns="" to opt out. # Prefer prefix format so children can be truly blank (no xmlns attribute). # CRITICAL: Only applies when explicitly set, not when defaulted to :unqualified. prefix = element_namespace.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = element_namespace.uri else # Default: prefer default format (cleaner) hoisted[nil] = element_namespace.uri end end # THIRD: Add type namespaces (W3C: type namespaces MUST use prefix) # CRITICAL: Type namespaces respect namespace_scope directive. # When namespace_scope is configured, only hoist type namespaces in scope. # # Check if namespace_scope is configured has_namespace_scope = !needs.namespace_scope_configs.empty? # Type attribute namespaces needs.type_attribute_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already added # If namespace_scope is configured, only hoist if in scope if has_namespace_scope scope_config = needs.scope_config_for(ns_class) next unless scope_config end # Namespaces in attributes MUST use prefix format (W3C rule) prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end # Type element namespaces needs.type_element_namespaces.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already added next if ns_class == element_namespace # Skip element's own namespace # NOTE: Type namespaces are different from child element namespaces. # Type namespaces are declared on Type::Value subclasses and used as # prefixes on child elements. They MUST be hoisted to root with prefix # format (W3C constraint: only one default namespace per element). # # The condition below only applies to child element namespaces, NOT Type # namespaces. Type namespaces should ALWAYS be hoisted. # # Examples of child element namespaces (should NOT be hoisted if different): # - Root has XMI namespace, child has XMI_NEW namespace → child declares # - Root has NO namespace, child has XMI namespace → child declares # # Examples of Type namespaces (should ALWAYS be hoisted): # - Root has any namespace, attribute type has XMI namespace → hoist XMI # - Root has NO namespace, attribute type has XMI namespace → hoist XMI # Type namespaces are NOT about element structure, they're about type identity. # If namespace_scope is configured, only hoist if in scope if has_namespace_scope scope_config = needs.scope_config_for(ns_class) next unless scope_config end # Type element namespaces MUST use prefix format prefix = ns_class.prefix_default || "ns#{hoisted.keys.length}" hoisted[prefix] = ns_class.uri end # FOURTH: Add remaining namespaces not in namespace_scope needs.all_namespace_classes.each do |ns_class| ns_uri = ns_class.uri next if hoisted.value?(ns_uri) # Skip if already added # Check if this namespace is in namespace_scope (skip if yes) scope_config = needs.scope_config_for(ns_class) next if scope_config # Add remaining namespace (default format preferred) hoisted[nil] = ns_class.uri end root_node = DeclarationPlan::ElementNode.new( qualified_name: "", use_prefix: nil, hoisted_declarations: hoisted, ) # Build children_plans for attributes with Serializable types children_plans = build_children_plans_from_metadata(mapping, needs, options) return DeclarationPlan.new( root_node: root_node, global_prefix_registry: build_prefix_registry(needs), input_formats: {}, namespace_classes: namespace_classes, children_plans: children_plans, original_namespace_uris: options[:__original_namespace_uris] || {}, ) end # TREE PATH: XmlElement tree path # CRITICAL: Resolve type namespace refs BEFORE using type_attribute_namespaces TypeNamespaceResolver.new(@register).resolve(needs) # Extract input_formats from stored plan if present (format preservation) input_formats = options[:stored_xml_declaration_plan]&.input_formats || {} build_options = options.merge(input_formats: input_formats) # Build namespace_classes hash for unit testing compatibility namespace_classes = {} needs.all_namespace_classes.each do |ns_class| namespace_classes[ns_class.uri] = ns_class end # Build the element node tree recursively (mark root, no parent context) root_node = build_element_node( root_element, mapping, needs, build_options, is_root: true, parent_format: nil, parent_namespace_class: nil, parent_namespace_prefix: nil, parent_hoisted: {} ) # Build children_plans for each child element (for child_plan() method) children_plans = build_children_plans(root_element, mapping, needs, build_options) # Create DeclarationPlan with tree and input_formats DeclarationPlan.new( root_node: root_node, global_prefix_registry: build_prefix_registry(needs), input_formats: input_formats, namespace_classes: namespace_classes, children_plans: children_plans, original_namespace_uris: options[:__original_namespace_uris] || {}, ) end
def plan_attribute(xml_attr, xml_element, mapping, options)
-
(AttributeNode)- Attribute decision node
Parameters:
-
options(Hash) -- Serialization options -
mapping(Xml::Mapping) -- XML mapping -
xml_element(XmlDataModel::XmlElement) -- Parent element -
xml_attr(XmlDataModel::XmlAttribute) -- Attribute to plan
def plan_attribute(xml_attr, xml_element, mapping, options) attr_ns_class = xml_attr.namespace_class element_ns_class = xml_element.namespace_class # Get W3C attributeFormDefault setting attribute_form_default = element_ns_class&.attribute_form_default || :unqualified # Compare by URI since namespace classes can be different instances # with the same URI (e.g., dynamically created in tests with Class.new) same_namespace = attr_ns_class && element_ns_class && attr_ns_class.uri == element_ns_class.uri # Get the attribute's mapping rule to check form option mapper_class = options[:mapper_class] attr_mapping_rule = nil if mapper_class.is_a?(Class) && mapper_class.include?(Lutaml::Model::Serialize) attrs = mapper_class.attributes attr_def = attrs[xml_attr.name.to_sym] || attrs[xml_attr.name.to_s] if attr_def attr_mapping_rule = mapping.attributes.find do |r| r.to == attr_def.name end end end # W3C Attribute Prefix Decision (MECE) # Priority: form option on mapping > same_namespace + attribute_form_default > type namespace use_prefix = if attr_mapping_rule&.form == :qualified # Priority 1: Explicit form: :qualified → YES prefix attr_ns_class&.prefix_default || element_ns_class&.prefix_default elsif same_namespace && attribute_form_default == :unqualified # Priority 2: Same namespace + unqualified → NO prefix nil elsif attr_ns_class && element_ns_class # Priority 3: Different namespace OR qualified → YES prefix attr_ns_class.prefix_default elsif attr_ns_class # Priority 4: Only attribute has namespace → YES prefix attr_ns_class.prefix_default elsif attribute_form_default == :qualified # Priority 5: No namespace but qualified → inherit element prefix # CRITICAL: xmlns declarations (xmlns, xmlns:*) must NEVER have a prefix. # The "xmlns" part is part of the declaration syntax, not a namespace prefix. # These declarations are handled separately by the XML processor. # Also, xsi:* attributes (xsi:schemaLocation, xsi:type, xsi:nil) must # ALWAYS use the "xsi" prefix. They should never inherit element prefix. # They conventionally belong to the XSI namespace. attr_name = xml_attr.name.to_s if attr_name.start_with?("xmlns", "xsi:", "xml:") nil else element_ns_class&.prefix_default end else # Priority 6: No namespace, unqualified → NO prefix (W3C default) nil end DeclarationPlan::AttributeNode.new( local_name: xml_attr.name, use_prefix: use_prefix, namespace_uri: attr_ns_class&.uri, ) end
def plan_collection(collection, mapping, needs)
-
(DeclarationPlan)- declaration plan with children_plans
Parameters:
-
needs(NamespaceNeeds) -- namespace needs from collector -
mapping(Xml::Mapping) -- the XML mapping -
collection(Collection) -- the collection object
def plan_collection(collection, mapping, needs) # For collections, create a plan with children_plans for each item root_node = DeclarationPlan::ElementNode.new( qualified_name: mapping.root_element || "", use_prefix: nil, hoisted_declarations: {}, ) # Build namespace_classes from needs namespace_classes = {} needs.all_namespace_classes.each do |ns_class| namespace_classes[ns_class.uri] = ns_class end # Build individual child plans for collection items children_plans = build_collection_item_plans(collection, mapping, needs) DeclarationPlan.new( root_node: root_node, global_prefix_registry: build_prefix_registry(needs), input_formats: {}, namespace_classes: namespace_classes, children_plans: children_plans, ) end
def register_for(mapper_class)
-
(Symbol)- The resolved register ID
Parameters:
-
mapper_class(Class) -- The child's mapper class
def register_for(mapper_class) Lutaml::Model::Register.resolve_for_child(mapper_class, @register) end