Module: Lutaml::Xml::TransformationSupport::OrderedApplier

Included in:
Lutaml::Xml::Transformation
Defined in:
lib/lutaml/xml/transformation/ordered_applier.rb

Overview

Module for applying rules in element order for round-trip preservation.

When element_order was captured during parsing (model was deserialized from XML) AND the mapping is marked as ordered, this module ensures the original XML structure is preserved during serialization.

Instance Method Summary collapse

Instance Method Details

#apply_element_rule_single(parent:, rule:, value:, options:) { ... } ⇒ Object

Apply element rule for a single value from a collection

Parameters:

Yields:

  • Block to create element for value



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
# File 'lib/lutaml/xml/transformation/ordered_applier.rb', line 97

def apply_element_rule_single(parent:, rule:, value:, options:)
  # raw: :element — value is a complete XML element string, inject directly
  if rule.raw == :element
    add_raw_element_fragments(parent, value)
    return
  end

  # Extract parent's namespace info for element_form_default inheritance
  parent_ns_class = parent.namespace_class
  # Only pass element_form_default VALUE if it was explicitly set
  # When not set (defaults to :unqualified), pass nil to avoid incorrect blank namespace treatment
  parent_element_form_default = if parent_ns_class&.element_form_default_set?
                                  parent_ns_class.element_form_default
                                end

  # Merge parent context into options
  child_options = options.merge(
    parent_namespace_class: parent_ns_class,
    parent_element_form_default: parent_element_form_default,
    parent_element: parent,
  )

  element = yield(rule, value, child_options)
  parent.add_child(element) if element
end

#apply_rules_in_order(root, model_instance, options, compiled_rules, model_class, register_id) { ... } ⇒ Object

Apply rules in the order specified by element_order

This ensures round-trip serialization preserves the original XML structure. For mixed content, text nodes from element_order are added directly to preserve the original text interleaving.

Parameters:

  • root (::Lutaml::Xml::DataModel::XmlElement)

    Root element

  • model_instance (Object)

    The model instance

  • options (Hash)

    Transformation options

  • compiled_rules (Array<CompiledRule>)

    The compiled rules

  • model_class (Class)

    The model class

  • register_id (Symbol, nil)

    The register ID

Yields:

  • Block to apply individual rules



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
# File 'lib/lutaml/xml/transformation/ordered_applier.rb', line 25

def apply_rules_in_order(root, model_instance, options, compiled_rules,
model_class, register_id)
  element_order, fallback_rules = reconciled_element_order(
    model_instance, compiled_rules, options
  )
  mapping = model_class.mappings_for(:xml, register_id)

  # Track index per compiled rule for collection attributes. Keyed
  # by rule, not by element name: one rule can appear under several
  # names once aliases are in play.
  element_indices = ::Hash.new(0)

  # Track text node index for content-mapped attribute access.
  text_node_index = 0

  # Track whether we processed any text nodes from element_order
  processed_text_nodes = false

  # Find the content rule to get CDATA flag for mixed content text
  content_rule = compiled_rules.find do |r|
    r.option(:mapping_type) == :content
  end
  root.cdata = true if content_rule&.cdata

  # Pre-check: can we use the content attribute for indexed text access?
  # This requires the content array length to match the text node count
  # in element_order. When they match, mutations to the content array
  # are reflected in serialization. When they don't match (e.g., CDATA
  # creates extra whitespace text nodes), we fall back to element_order.
  text_node_count = element_order.count do |o|
    order_entry?(o) && o.type == "Text"
  end
  content_value = content_rule &&
    model_instance&.public_send(content_rule.attribute_name)
  use_content_index = content_rule && content_value.is_a?(Array) &&
    content_value.length == text_node_count

  # Iterate through element_order to preserve original sequence
  element_order.each do |object|
    # element_order is a plain accessor, so entries that are not
    # order entries (no #type) exist in the wild. Nothing can be
    # matched or emitted for them; skip instead of crashing.
    next unless order_entry?(object)

    result = process_element_order_item(
      object, root, model_instance, options,
      compiled_rules, mapping, element_indices,
      content_rule, text_node_index, text_node_count,
      use_content_index
    ) do |action, rule, value, xsi_nil_flag|
      yield(action, rule, value, xsi_nil_flag) if block_given?
    end
    text_node_index += 1 if object.type == "Text"
    processed_text_nodes = true if result == :text_node
  end

  # Apply the rules element_order does not cover: attributes,
  # content and raw.
  apply_remaining_rules(model_instance, options, compiled_rules,
                        mapping, processed_text_nodes,
                        fallback_rules) do |action, rule, value|
    yield(action, rule, value) if block_given?
  end
end

#find_rule_for_element(object, compiled_rules) ⇒ CompiledRule?

Find the mapping rule for an element from element_order

Parameters:

  • object (Xml::Element)

    Element from element_order

  • compiled_rules (Array<CompiledRule>)

    The compiled rules

Returns:

  • (CompiledRule, nil)

    The matching rule or nil



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/lutaml/xml/transformation/ordered_applier.rb', line 128

def find_rule_for_element(object, compiled_rules)
  return nil unless order_entry?(object)
  return nil unless object.type == "Element"

  object_ns_uri = object.namespace_uri # nil if old element_order (backward compat)

  compiled_rules.find do |r|
    r.is_a?(::Lutaml::Model::CompiledRule) &&
      r.option(:mapping_type) == :element &&
      matches_element_rule?(r, object.name, object_ns_uri)
  end
end