Module: Canon::TreeDiff::OperationConverterHelpers::PostProcessor

Defined in:
lib/canon/tree_diff/operation_converter_helpers/post_processor.rb

Overview

Post-processing of DiffNodes Handles detection of attribute-order-only differences and other optimizations

Class Method Summary collapse

Class Method Details

.attributes_equal_ignoring_order?(attrs1, attrs2) ⇒ Boolean

Check if two attribute hashes are equal ignoring order

Parameters:

  • attrs1 (Hash)

    First attribute hash

  • attrs2 (Hash)

    Second attribute hash

Returns:

  • (Boolean)

    True if attributes are equal (ignoring order)



67
68
69
70
71
72
73
74
75
76
77
# File 'lib/canon/tree_diff/operation_converter_helpers/post_processor.rb', line 67

def self.attributes_equal_ignoring_order?(attrs1, attrs2)
  return true if attrs1.nil? && attrs2.nil?
  return false if attrs1.nil? || attrs2.nil?

  # Convert to hashes if needed
  attrs1 = attrs1.to_h if attrs1.respond_to?(:to_h)
  attrs2 = attrs2.to_h if attrs2.respond_to?(:to_h)

  # Compare as sets (order-independent)
  attrs1.sort.to_h == attrs2.sort.to_h
end

.detect_attribute_order_diffs(diff_nodes, normative_determiner) ⇒ Array<DiffNode>

Detect INSERT/DELETE pairs that differ only in attribute order and reclassify them to use the attribute_order dimension

Parameters:

  • diff_nodes (Array<DiffNode>)

    Diff nodes to process

  • normative_determiner (#call)

    Proc/object to determine normative status

Returns:

  • (Array<DiffNode>)

    Processed diff nodes



15
16
17
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
# File 'lib/canon/tree_diff/operation_converter_helpers/post_processor.rb', line 15

def self.detect_attribute_order_diffs(diff_nodes, normative_determiner)
  # Group nodes by parent and element type
  deletes = diff_nodes.select { |dn| dn.node1 && !dn.node2 }
  inserts = diff_nodes.select { |dn| !dn.node1 && dn.node2 }

  # For each DELETE, try to find a matching INSERT
  deletes.each do |delete_node|
    node1 = delete_node.node1
    next unless node1.respond_to?(:name) && node1.respond_to?(:attributes)

    # Skip if node has no attributes (can't be attribute order diff)
    next if node1.attributes.nil? || node1.attributes.empty?

    # Find inserts with same element name at same position
    matching_insert = inserts.find do |insert_node|
      node2 = insert_node.node2
      next false unless node2.respond_to?(:name) && node2.respond_to?(:attributes)
      next false unless node1.name == node2.name

      # Must have attributes to differ in order
      next false if node2.attributes.nil? || node2.attributes.empty?

      # Check if they differ only in attribute order
      next false unless attributes_equal_ignoring_order?(
        node1.attributes, node2.attributes
      )

      # Ensure same content (text and children structure)
      nodes_same_except_attr_order?(node1, node2)
    end

    next unless matching_insert

    # Found an attribute-order-only difference
    # Reclassify both nodes to use attribute_order dimension
    delete_node.dimension = :attribute_order
    delete_node.reason = "attribute order changed"
    delete_node.normative = normative_determiner.call(:attribute_order)

    matching_insert.dimension = :attribute_order
    matching_insert.reason = "attribute order changed"
    matching_insert.normative = normative_determiner.call(:attribute_order)
  end

  diff_nodes
end

.nodes_same_except_attr_order?(node1, node2) ⇒ Boolean

Check if two nodes are the same except for attribute order

Parameters:

  • node1 (Nokogiri::XML::Node)

    First node

  • node2 (Nokogiri::XML::Node)

    Second node

Returns:

  • (Boolean)

    True if nodes are same except attribute order



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/canon/tree_diff/operation_converter_helpers/post_processor.rb', line 84

def self.nodes_same_except_attr_order?(node1, node2)
  # Same text content
  return false if node1.text != node2.text

  # Same number of children
  return false if node1.children.length != node2.children.length

  # If has children, they should have same structure
  if node1.children.any?
    node1.children.zip(node2.children).all? do |child1, child2|
      child1.name == child2.name
    end
  else
    true
  end
end