Class: Lutaml::Xml::ModelTransform

Inherits:
Model::Transform show all
Includes:
NestedCollectionAttribute
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

Defined Under Namespace

Classes: Record

Constant Summary collapse

RULE_RECORDS =

Per (model class, register): compiled records for the mapping rules — attribute, derived/valid flags, and #765 group-skip — keyed on the mapping's finalize version so late changes rebuild. The loop body used to re-derive these per parsed instance. Concurrent::Map under threaded MRI, plain Hash under Opal (Concurrent is unavailable there); class identity keys either.

if Lutaml::Model.opal?
  {}
else
  Concurrent::Map.new # rubocop:disable Lint/HashCompareByIdentity
end
NAMESPACED_NAME_PARTS =

Namespaced rule name -> [local_name, rule_uri]. Pure string splitting, deterministic per spelling, shared across parses. Concurrent::Map under threaded MRI, plain Hash under Opal.

if Lutaml::Model.opal?
  {}
else
  Lutaml::Model::RuntimeCompatibility.require_native("concurrent")
  Concurrent::Map.new
end
EMPTY_HASH =

Performance: Frozen empty hash to reduce allocations

{}.freeze

Constants inherited from Model::Transform

Model::Transform::MAX_CACHE_SIZE

Instance Attribute Summary

Attributes inherited from Model::Transform

#context, #lutaml_register

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model::Transform

#attributes, cache_size, cached_transform, clear_cache!, data_to_model, #initialize, invalidate_for, #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:

  • The element to collect from

  • The element to collect from

  • (defaults to: [])

    Element path from root (empty for root element)

  • (defaults to: nil)

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

  • (defaults to: nil)

    Set of visited element object_ids to prevent cycles

Returns:

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



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/lutaml/xml/model_transform.rb', line 179

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:

  • The parsed root element

Returns:

  • The plan or nil if no namespaces



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/lutaml/xml/model_transform.rb', line 141

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



227
228
229
# File 'lib/lutaml/xml/model_transform.rb', line 227

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

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



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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/lutaml/xml/model_transform.rb', line 43

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
    # TODO.max-perf/17: the same child element was being re-hydrated
    # per matching rule (593k instantiations for 12k elements on the
    # ISO-13849 profile). Memoize one instance per (element, class)
    # for the whole parse; the memo travels in the propagated
    # deserialization options.
    # Nested identity maps keyed on the element object itself —
    # flat object_id pairs could collide after GC id reuse on
    # long parses (#808 audit).
    memo = options[:__lutaml_child_memo] ||= {}.compare_by_identity
    per_class = (memo[data] ||= {}.compare_by_identity)
    if (existing = per_class[model_class])
      return existing
    end

    instance = model_class.allocate_for_deserialization(child_register)
    per_class[model_class] = instance
  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(ParseSession.new(data, instance, options,
                                     child_register))
end

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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/lutaml/xml/model_transform.rb', line 231

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