Class: Canon::Xml::ElementMatcher

Inherits:
Object
  • Object
show all
Defined in:
lib/canon/xml/element_matcher.rb

Overview

Matches XML elements semantically across two DOM trees

This class implements intelligent element matching for XML diffs. Instead of naive line-by-line comparison, it semantically matches elements across documents using identity attributes and structural position.

Matching Strategy

Elements are matched in two passes:

  1. Identity attribute matching: Elements with same identity attribute values are matched (e.g., id="foo" matches id="foo")
  2. Position-based matching: Remaining elements matched by name and document position

This allows detecting when elements:

  • Move to different positions (matched by ID)
  • Have content changes (matched, diff shows changes)
  • Are added/deleted (no match found)

Identity Attributes

By default, these attributes identify elements:

  • id
  • ref
  • name
  • key

Custom identity attributes can be provided to the constructor.

Usage

matcher = ElementMatcher.new
root1 = Canon::Xml::DataModel.from_xml(xml1)
root2 = Canon::Xml::DataModel.from_xml(xml2)
matches = matcher.match_trees(root1, root2)

matches.each do |match|
case match.status
when :matched
  # Elements found in both trees
when :deleted
  # Element only in first tree
when :inserted
  # Element only in second tree
end
end

Defined Under Namespace

Classes: MatchResult

Constant Summary collapse

DEFAULT_IDENTITY_ATTRS =

Default attributes used to identify elements

%w[id ref name key class].freeze
EMPTY_PATH =
[].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(identity_attrs: DEFAULT_IDENTITY_ATTRS) ⇒ ElementMatcher

Returns a new instance of ElementMatcher.



118
119
120
121
# File 'lib/canon/xml/element_matcher.rb', line 118

def initialize(identity_attrs: DEFAULT_IDENTITY_ATTRS)
  @identity_attrs = identity_attrs
  @matches = []
end

Class Method Details

.positionally_paired?(elems1, elems2) ⇒ Boolean

Public: true when both lists pair positionally (equal length, equal name/ns/identity at every index). ChildComparison uses this to skip MatchResult allocation on the common same-structure path.

Returns:

  • (Boolean)


153
154
155
156
157
# File 'lib/canon/xml/element_matcher.rb', line 153

def self.positionally_paired?(elems1, elems2)
  return false if elems1.length != elems2.length || elems1.empty?

  new.paired_positionally?(elems1, elems2)
end

Instance Method Details

#match_children_only(children1, children2) ⇒ Array<MatchResult>

Match one level of children only — no descent into matched pairs. The comparator walks descendants itself (compare_nodes → ChildComparison per level), so the nested matches match_trees would produce below this level are discarded by its direct- children filter; matching them is wasted work.

Parameters:

Returns:

  • (Array<MatchResult>)

    Match results for these children only



143
144
145
146
147
# File 'lib/canon/xml/element_matcher.rb', line 143

def match_children_only(children1, children2)
  @matches = []
  match_level(children1, children2, [], recursive: false)
  @matches
end

#match_trees(root1, root2) ⇒ Array<MatchResult>

Match elements between two DOM trees

Parameters:

Returns:



128
129
130
131
132
# File 'lib/canon/xml/element_matcher.rb', line 128

def match_trees(root1, root2)
  @matches = []
  match_children(root1.children, root2.children, [])
  @matches
end

#paired_positionally?(elems1, elems2) ⇒ Boolean

Instance form of .positionally_paired?

Returns:

  • (Boolean)


160
161
162
# File 'lib/canon/xml/element_matcher.rb', line 160

def paired_positionally?(elems1, elems2)
  pairwise_corresponding?(elems1, elems2)
end