Class: Lutaml::Xml::ModelTransform

Inherits:
Model::Transform show all
Defined in:
lib/lutaml/xml/model_transform.rb

Overview

ModelTransform is the XML transform handler for the model layer. It inherits from Lutaml::Model::Transform and implements XML-specific data_to_model and model_to_data methods.

This class bridges the model layer and the XML module:

  • Inherits from Lutaml::Model::Transform (model’s format-agnostic base)

  • Delegates to Lutaml::Xml::Transformation for actual XML serialization

  • Used by model’s serialization pipeline via Transform.for(:xml)

Direct Known Subclasses

Transform

Constant Summary collapse

EMPTY_HASH =

Performance: Frozen empty hash to reduce allocations

{}.freeze

Instance Attribute Summary

Attributes inherited from Model::Transform

#attributes, #context, #lutaml_register

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model::Transform

data_to_model, #initialize, #model_class, model_to_data

Constructor Details

This class inherits a constructor from Lutaml::Model::Transform

Class Method Details

.collect_element_namespaces(element, path = [], result = nil, visited = nil) ⇒ Hash

Recursively collect namespace declarations from all elements in the tree.

Each element may declare namespaces via xmlns attributes. The path array tracks the element names from root to current element. Root has path [], its children have path [“childName”], etc. This matches DeclarationPlan.from_input_with_locations expectations where root path is [] and child paths are built by appending.

Recursively collect namespace declarations from all elements in the tree.

Each element may declare namespaces via xmlns attributes. The path array tracks the element names from root to current element. Root has path [], its children have path [“childName”], etc. This matches DeclarationPlan.from_input_with_locations expectations where root path is [] and child paths are built by appending.

Available as both class method (for lazy plan building from instance context) and instance method (for use within ModelTransform).

Parameters:

  • element (XmlElement)

    The element to collect from

  • element (XmlElement)

    The element to collect from

  • path (Array<String>) (defaults to: [])

    Element path from root (empty for root element)

  • result (Hash) (defaults to: nil)

    Accumulated result { path_array => { key => { uri:, prefix:, format: } } }

  • visited (Set) (defaults to: nil)

    Set of visited element object_ids to prevent cycles

Returns:

  • (Hash)

    { [path_array] => { key => { uri:, prefix:, format: } } }



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/lutaml/xml/model_transform.rb', line 139

def self.collect_element_namespaces(element, path = [], result = nil,
                                    visited = nil)
  result ||= {}
  visited ||= Set.new
  return result unless element
  return result if visited.include?(element.object_id)

  visited.add(element.object_id)

  # Collect this element's own namespace declarations
  own_ns = element.own_namespaces
  if own_ns&.any?
    input_namespaces = {}
    own_ns.each do |prefix, ns_data|
      key = prefix.nil? ? :default : prefix
      input_namespaces[key] = {
        uri: ns_data.uri,
        prefix: prefix,
        format: prefix.nil? ? :default : :prefix,
      }
    end

    result[path] = input_namespaces unless input_namespaces.empty?
  end

  # Recurse into children with path extended by child local name
  # Use local name (without prefix) since serialization lookup uses
  # xml_element.name which is the local name without namespace prefix
  element.children.each do |child|
    next if child.is_a?(String) # Skip text nodes

    # Strip namespace prefix if present (e.g., "c:childName" -> "childName")
    # to match the key format used during serialization lookup
    full_name = child.name.to_s
    child_local_name = if full_name.include?(":")
                         full_name.split(":",
                                         2).last
                       else
                         full_name
                       end
    child_path = path + [child_local_name]
    collect_element_namespaces(child, child_path, result, visited)
  end

  result
end

Instance Method Details

#build_input_declaration_plan(root_element) ⇒ DeclarationPlan?

Build a DeclarationPlan from the parsed element tree’s namespace declarations.

Walks the entire element tree to capture ALL xmlns declarations from input XML, including unused ones (like xmlns:xi for XInclude) and declarations on child elements. The plan preserves WHERE each namespace was declared and its original format/URI.

Parameters:

  • root_element (XmlElement)

    The parsed root element

Returns:



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/lutaml/xml/model_transform.rb', line 101

def build_input_declaration_plan(root_element)
  return nil unless root_element

  # Walk the element tree collecting namespaces with their declaration locations
  namespaces_with_locations = collect_element_namespaces(root_element)
  return nil if namespaces_with_locations.nil? || namespaces_with_locations.empty?

  # Get the mapping for namespace resolution
  xml_mapping = mappings_for(:xml)

  # Create location-aware DeclarationPlan
  DeclarationPlan.from_input_with_locations(namespaces_with_locations,
                                            xml_mapping)
end

#collect_element_namespacesObject

Instance method delegates to class method



187
188
189
# File 'lib/lutaml/xml/model_transform.rb', line 187

def collect_element_namespaces(...)
  self.class.collect_element_namespaces(...)
end

#data_to_model(data, _format, options = {}) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/lutaml/xml/model_transform.rb', line 18

def data_to_model(data, _format, options = {})
  # Use child's own default register if it has one
  # This ensures versioned schemas (e.g., MML v2 with lutaml_default_register = :mml_v2)
  # are instantiated with their native context
  child_register = Lutaml::Model::Register.resolve_for_child(
    model_class, lutaml_register
  )

  instance_is_serialize = model_class.include?(::Lutaml::Model::Serialize)
  if instance_is_serialize
    instance = model_class.allocate_for_deserialization(child_register)
  else
    instance = model_class.new
    register_accessor_methods_for(instance, child_register)
  end
  # Set @__xml_namespace_prefix on root model for doubly-defined namespace support.
  # This is read during serialization to determine if the root element should use
  # an explicit prefix from the input XML.
  # Check the XmlElement's namespace_prefix (not the model's):
  # - Nil/empty: root uses default format (doubly-defined case)
  # - Set: root has explicit prefix (mixed content case)
  root_element = if data.is_a?(::Lutaml::Xml::XmlElement)
                   data
                 else
                   data.root
                 end
  if root_element && instance_is_serialize
    root_ns_prefix = if root_element.namespace_prefix_explicit && root_element.namespace_prefix
                       root_element.namespace_prefix
                     else
                       root_element.xml_namespace_prefix
                     end
    if root_ns_prefix && !root_ns_prefix.empty?
      instance.xml_namespace_prefix = root_ns_prefix
    end
    # Track original namespace URI for namespace alias support.
    #
    # When root element's namespace URI differs from the model's canonical URI,
    # it's an alias that should be preserved during serialization.
    root_ns_uri = root_element.namespace_uri
    if root_ns_uri
      model_ns_class = instance.class.mappings_for(:xml)&.namespace_class
      if model_ns_class && model_ns_class.uri != root_ns_uri
        # root_ns_uri differs from canonical - preserve it (alias or other)
        instance.original_namespace_uri = root_ns_uri
      end
    end

    # Namespace declaration plan for round-trip fidelity.
    # Only needed for root elements (no lutaml_parent in options).
    # Three modes: :lazy (default), :eager, :skip.
    if !options.key?(:lutaml_parent)
      plan_mode = options.fetch(:import_declaration_plan, :lazy)
      case plan_mode
      when :skip
        # Skip plan building entirely (fastest)
      when :eager
        input_declaration_plan = build_input_declaration_plan(root_element)
        instance.import_declaration_plan = input_declaration_plan if input_declaration_plan
      when :lazy
        # Store element reference for lazy plan building on first to_xml.
        # No recursive walk during deserialization — plan is built on demand.
        # Element reference is released after first serialization.
        instance.pending_plan_root_element = root_element
      else
        raise ArgumentError,
              "import_declaration_plan must be :eager, :lazy, or :skip, got #{plan_mode.inspect}"
      end
    end
  end
  root_and_parent_assignment(instance, options)
  apply_xml_mapping(data, instance, options, child_register,
                    instance_is_serialize)
end

#model_to_data(model, _format, options = {}) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/lutaml/xml/model_transform.rb', line 191

def model_to_data(model, _format, options = {})
  # Check if model class has a pre-compiled transformation
  model_class = model.class
  if model_class.is_a?(Class) && model_class.include?(::Lutaml::Model::Serialize)
    transformation = model_class.transformation_for(:xml, lutaml_register)

    # If transformation exists and is an Xml::Transformation, use it
    if transformation.is_a?(::Lutaml::Xml::Transformation)
      return transformation.transform(model, options)
    end
  end

  # Fallback to returning model for classes without transformation
  model
end