Class: Lutaml::Xml::ModelTransform
- Inherits:
-
Model::Transform
- Object
- Model::Transform
- Lutaml::Xml::ModelTransform
- 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
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
Class Method Summary collapse
-
.collect_element_namespaces(element, path = [], result = nil, visited = nil) ⇒ Hash
Recursively collect namespace declarations from all elements in the tree.
Instance Method Summary collapse
-
#build_input_declaration_plan(root_element) ⇒ DeclarationPlan?
Build a DeclarationPlan from the parsed element tree's namespace declarations.
-
#collect_element_namespaces ⇒ Object
Instance method delegates to class method.
- #data_to_model(data, _format, options = {}) ⇒ Object
- #model_to_data(model, _format, options = {}) ⇒ Object
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).
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.
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_namespaces ⇒ Object
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, = {}) # 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 = [:__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 !.key?(:lutaml_parent) plan_mode = .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, ) apply_xml_mapping(ParseSession.new(data, instance, , 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, = {}) # 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, ) end end # Fallback to returning model for classes without transformation model end |