# = dev-utils/debug/diff.rb # # _Planned_ functionality to support comparing complex objects and discovering object # topologies. Nothing here for now. # =begin # Implement Debug.diff here. Do not load this file directly. module DevUtils::Debug # # Helps identify the difference between two objects when it's not immediately obvious to # the eye. # # +o1+ and +o2+ are simply objects to be compared. If one or the other is a built-in type, # like String, then it's a simple yes/no answer and this method doesn't buy you much. If # they are both complex objects (aggregating other data, like a Struct or data object), # then the comparison takes place with each of their composed objects, recursively. # # The typical usage: diff(x, y) returns an array representing the first difference # found, like ['age: 31', 'age: 84']. This is designed to be output with +puts+ # in +irb+, so they appear one string per line. # # The +flags+ modify the result. Only one flag is currently observed. If it's a number, # say 3, then the third difference, in alphabetical order, is returned. If it's # :all, then all differences are returned in an array of arrays. If it's # :n, then the number of differences is returned. # # == Examples # # ... # def diff(o1, o2, *flags) case (flag = flags.first || 1) when :n op = :count n = -1 when :all op = :find_all n = -1 when Numeric op = :find_one n = flag.to_i n = 1 if n < 1 else raise ArgumentError, flag.inspect end diff = catch(:found) do diffs = _diff(o1, o2, '', n) case op when :find_one return nil # If we get this far, no difference was found. when :find_all return diffs when :count return diffs.size end end return diff # This was thrown from _diff; it's a single result. end private # # +o1+ and +o2+ are the objects being compared. +base+ is the base name for the fields so # far (so we can report the full path of the field name, like 'person.name.first'). +n+ is # the number of differences we should skip before immediately returning the one we're # after. # # Each time we find a difference, we add it to +diffs+ and decrement n. If n is zero, we # have found the difference we were looking for so we throw :found and the difference (a # 2-tuple). # # We return an array of all the differences we have found. # def _diff(o1, o2, base, n) if _immediate?(o1) or _immediate?(o2) return "Immediate value(s)" #return [o1,o2].map { |o| "#{field_path}: #{o.inspect}" } else # Both objects are complex, and we're happy. h1 = _hash_representation(o1) h2 = _hash_representation(o2) fields = (h1.keys + h2.keys).uniq.sort diffs = [] fields.each do |f| v1 = h1[f] v2 = h2[f] goirb binding if $X if v1 == v2 next else # We've found two unequal values. If they're immediate values, # we count it as a difference. If they're complex, then we # recurse into them. field_path = [base, f].reject { |s| s.empty? }.join('.') if _immediate?(v1) or _immediate?(v2) diff = [v1,v2].map { |v| "#{field_path}: #{v.inspect}" } throw :found, diff if (n -= 1).zero? diffs << diff else diffs.concat _diff(v1, v2, field_path, n) end end end return diffs end end # # Returns true iff the given object is an "immediate value". That means a fundamenal Ruby # data type. It's an arbitrary definition, and the choices are hardcoded. Hopefully I can # think of a better heuristic. # def _immediate?(object) [Numeric, String, Range, Array, Hash, NilClass, TrueClass, FalseClass].any? { |klass| klass === object } end # # Returns a hash representation of an object, mapping field names to values. # def _hash_representation(object) _methods = object.public_methods(false) if Struct === object # In a Struct, if X is an attribute, then there are methods X and X=. fields = _methods.grep(/=/).map { |meth| meth.to_s.tr('=', '') } fields = fields.select { |meth| _methods.include? meth } else # In a general complex oject, if X in an attribute, then there is a method X and an # instance variable @X. _variables = object.instance_variables.map { |v| v.tr('@', '') } fields = _variables.select { |v| _methods.include? v } end # Now we've got the fields, which in all cases are public methods. So we call them to # get the values and construct our hash. result = {} fields.each do |field| result[field] = object.send(field) end result end # # Implementation notes for diff. # # Approach: # * we don't dig into arrays or hashes, etc.; those are "immediate" values along # with Numeric, String, Range, etc. # * if object under inspection is immediate, we compare with 'equal?' and that's it # * OK, so we've got two complex objects # * get hash representations, one level deep: field => value # * there is special logic for Structs # * if field sets are different, do we care? # * we can just report that field F is X in o1 but is nil in o2 # * examine fields in alphabetical order # * compare values (with #equal?) # * if equal, go to next field # * if not equal # * if value(s) are immediate, compare and report (or skip, or count, # according to flags) # * otherwise, dig into those values (recurse) # # Alternative notes for diff. # # Approach: # * have a separate method 'structure(obj)', which returns the structure of the # given object; e.g. structure(person) -> ['name', 'age', 'address.number', # 'address.road', 'address.city'] # * that 'structure' method would be useful for some other things # * you can yield each bit as it's done # * use 'structure' and 'eval' to get the value of things # * be smart about nil values; e.g. person.address could be nil, which # would represent a different, but compatible, structure # # This seems simpler than mixing it all up as in the implementation above. end # module Kernel class Object def topology end end =end