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
-
.add_difference(path, obj1, obj2, diff_code, opts, differences) ⇒ Object
Add a Ruby object difference.
-
.compare_arrays(arr1, arr2, opts, differences, path) ⇒ Symbol
Compare two arrays.
-
.compare_hashes(hash1, hash2, opts, differences, path) ⇒ Symbol
Compare two hashes.
-
.compare_objects(obj1, obj2, opts, differences, path) ⇒ Symbol
Compare Ruby objects (Hash, Array, primitives) for JSON/YAML.
-
.compare_primitives(val1, val2, opts, differences, path) ⇒ Symbol
Compare primitive values.
-
.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.
Class Method Details
.add_difference(path, obj1, obj2, diff_code, opts, differences) ⇒ Object
Add a Ruby object difference
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
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
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
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
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.
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 |