Module: Canon::Comparison::RubyObjectComparator

Defined in:
lib/canon/comparison/ruby_object_comparator.rb

Overview

Ruby Object Comparison Utilities

Provides public comparison methods for Ruby objects (Hash, Array, primitives). This module extracts shared comparison logic that was previously accessed via send() from YamlComparator.

Constant Summary collapse

EMPTY_KEYS =
[].freeze

Class Method Summary collapse

Class Method Details

.add_difference(path, obj1, obj2, diff_code, opts, differences) ⇒ Object

Add a Ruby object difference

Parameters:

  • path (String)

    Path to the difference

  • obj1 (Object)

    First object

  • obj2 (Object)

    Second object

  • diff_code (Symbol)

    Difference code

  • opts (Hash)

    Comparison options

  • differences (Array)

    Array to append difference to



201
202
203
204
205
206
207
208
209
210
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 201

def self.add_difference(path, obj1, obj2, diff_code, opts, differences)
  return unless opts[:verbose]

  differences << {
    path: path,
    value1: obj1,
    value2: obj2,
    diff_code: diff_code,
  }
end

.compare_arrays(arr1, arr2, opts, differences, path) ⇒ Symbol

Compare two arrays

Parameters:

  • arr1 (Array)

    First array

  • arr2 (Array)

    Second array

  • opts (Hash)

    Comparison options

  • differences (Array)

    Array to append differences to

  • path (String)

    Current path in the object structure

Returns:

  • (Symbol)

    Comparison result constant



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 142

def self.compare_arrays(arr1, arr2, opts, differences, path)
  unless arr1.length == arr2.length
    add_difference(path, arr1, arr2,
                   Comparison::UNEQUAL_ARRAY_LENGTHS, opts,
                   differences)
    return Comparison::UNEQUAL_ARRAY_LENGTHS
  end

  all_equivalent = true
  arr1.each_with_index do |elem1, index|
    elem2 = arr2[index]
    next if deep_equal?(elem1, elem2)

    elem_path = "#{path}[#{index}]"
    result = compare_objects(elem1, elem2, opts, differences,
                             elem_path)
    all_equivalent = false unless result == Comparison::EQUIVALENT
  end

  all_equivalent ? Comparison::EQUIVALENT : Comparison::UNEQUAL_ARRAY_ELEMENTS
end

.compare_hashes(hash1, hash2, opts, differences, path) ⇒ Symbol

Compare two hashes

Parameters:

  • hash1 (Hash)

    First hash

  • hash2 (Hash)

    Second hash

  • opts (Hash)

    Comparison options

  • differences (Array)

    Array to append differences to

  • path (String)

    Current path in the object structure

Returns:

  • (Symbol)

    Comparison result constant



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
132
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 62

def self.compare_hashes(hash1, hash2, opts, differences, path)
  keys1 = hash1.keys
  keys2 = hash2.keys

  # Sort keys if order should be ignored (based on match options)
  match_opts = opts[:match_opts]
  if match_opts && match_opts[:key_order] != :strict
    # Identical key arrays: one sort replaces two (the sorted
    # forms are trivially equal, and the set algebra below
    # degenerates — missing/extra empty, common = the sorted
    # keys themselves).
    if keys1 == keys2
      keys1 = keys2 = keys1.sort_by(&:to_s)
    else
      keys1 = keys1.sort_by(&:to_s)
      keys2 = keys2.sort_by(&:to_s)
    end
  elsif match_opts && match_opts[:key_order] == :strict
    # Strict mode: key order matters
    # Check if keys are in same order
    # Keys are different or in different order
    # First check if it's just ordering (same keys, different order)
    if (keys1 != keys2) && (keys1.sort_by(&:to_s) == keys2.sort_by(&:to_s))
      # Same keys, different order - this is a key_order difference
      key_path = path.empty? ? "(key order)" : "#{path}.(key order)"
      add_difference(key_path, keys1, keys2,
                     Comparison::UNEQUAL_HASH_KEY_ORDER, opts, differences)
      return Comparison::UNEQUAL_HASH_KEY_ORDER
    end
  end

  # Check for missing keys
  missing_in_second = keys1 == keys2 ? EMPTY_KEYS : keys1 - keys2
  missing_in_first = keys1 == keys2 ? EMPTY_KEYS : keys2 - keys1

  missing_in_second.each do |key|
    key_path = path.empty? ? key.to_s : "#{path}.#{key}"
    add_difference(key_path, hash1[key], nil,
                   Comparison::MISSING_HASH_KEY, opts, differences)
  end

  missing_in_first.each do |key|
    key_path = path.empty? ? key.to_s : "#{path}.#{key}"
    add_difference(key_path, nil, hash2[key],
                   Comparison::MISSING_HASH_KEY, opts, differences)
  end

  has_missing_keys = !missing_in_first.empty? || !missing_in_second.empty?

  # Compare common keys. The path string is built lazily: deep-equal
  # pairs (the overwhelming majority in similar documents) answer
  # equivalent without ever needing it.
  common_keys = keys1 == keys2 ? keys1 : keys1 & keys2
  all_equivalent = true
  common_keys.each do |key|
    value1 = hash1[key]
    value2 = hash2[key]
    next if deep_equal?(value1, value2)

    key_path = path.empty? ? key.to_s : "#{path}.#{key}"
    result = compare_objects(value1, value2, opts, differences,
                             key_path)
    all_equivalent = false unless result == Comparison::EQUIVALENT
  end

  # Return appropriate status
  return Comparison::MISSING_HASH_KEY if has_missing_keys && all_equivalent
  return Comparison::UNEQUAL_HASH_VALUES unless all_equivalent

  has_missing_keys ? Comparison::MISSING_HASH_KEY : Comparison::EQUIVALENT
end

.compare_objects(obj1, obj2, opts, differences, path) ⇒ Symbol

Compare Ruby objects (Hash, Array, primitives) for JSON/YAML

Parameters:

  • obj1 (Object)

    First object

  • obj2 (Object)

    Second object

  • opts (Hash)

    Comparison options

  • differences (Array)

    Array to append differences to

  • path (String)

    Current path in the object structure

Returns:

  • (Symbol)

    Comparison result constant



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
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 19

def self.compare_objects(obj1, obj2, opts, differences, path)
  # Check for type mismatch
  unless obj1.instance_of?(obj2.class)
    add_difference(path, obj1, obj2, Comparison::UNEQUAL_TYPES,
                   opts, differences)
    return Comparison::UNEQUAL_TYPES
  end

  # FAST PATH: deep-equal objects in identical key order — every
  # nested comparison is trivially equivalent and no key-order
  # difference exists (strict key_order compares insertion
  # order, which keys == keys preserves). Most object pairs in
  # similar documents hit this and skip the path-building walk.
  return Comparison::EQUIVALENT if deep_equal?(obj1, obj2)

  case obj1
  when Hash
    compare_hashes(obj1, obj2, opts, differences, path)
  when Array
    compare_arrays(obj1, obj2, opts, differences, path)
  when NilClass, TrueClass, FalseClass, Numeric, String, Symbol
    compare_primitives(obj1, obj2, opts, differences, path)
  else
    # Fallback to equality comparison
    if obj1 == obj2
      Comparison::EQUIVALENT
    else
      add_difference(path, obj1, obj2,
                     Comparison::UNEQUAL_PRIMITIVES, opts,
                     differences)
      Comparison::UNEQUAL_PRIMITIVES
    end
  end
end

.compare_primitives(val1, val2, opts, differences, path) ⇒ Symbol

Compare primitive values

Parameters:

  • val1 (Object)

    First value

  • val2 (Object)

    Second value

  • opts (Hash)

    Comparison options

  • differences (Array)

    Array to append differences to

  • path (String)

    Current path in the object structure

Returns:

  • (Symbol)

    Comparison result constant



172
173
174
175
176
177
178
179
180
181
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 172

def self.compare_primitives(val1, val2, opts, differences, path)
  if val1 == val2
    Comparison::EQUIVALENT
  else
    add_difference(path, val1, val2,
                   Comparison::UNEQUAL_PRIMITIVES, opts,
                   differences)
    Comparison::UNEQUAL_PRIMITIVES
  end
end

.deep_equal?(obj1, obj2) ⇒ Boolean

Deep-equal with identical key order — compare_objects returns EQUIVALENT without consulting the path for such pairs, which is what makes lazy path construction in the hash/array walks behavior-identical.

Returns:

  • (Boolean)


187
188
189
# File 'lib/canon/comparison/ruby_object_comparator.rb', line 187

def self.deep_equal?(obj1, obj2)
  obj1 == obj2 && (!obj1.is_a?(Hash) || obj1.keys == obj2.keys)
end