module Diffable::InstanceMethods
Public Instance Methods
diff(other)
click to toggle source
Produces a Hash containing the differences between the calling object and the object passed in as a parameter
# File lib/diffable.rb, line 22 def diff(other) check_class_compatibility(self, other) self_attribs = self.get_attributes(self.class.excluded_fields) other_attribs = other.get_attributes(other.class.excluded_fields) change = compare_objects(self_attribs, other_attribs, self, other) #the last bit - no change, no report; simples if other.class.conditional_fields other.class.conditional_fields.each do |key| change[key.to_sym] = eval("other.#{key}") unless change.empty? end end change end
get_attributes(excluded)
click to toggle source
Fetches the attributes of the calling object, exluding the id
field and any fields specified passed as an array of symbols via the excluded
parameter
# File lib/diffable.rb, line 43 def get_attributes(excluded) attribs = attributes.dup attribs.delete_if { |key, value| (!excluded.nil? and excluded.include?(key)) or key == "id" } end
reflected_names(obj)
click to toggle source
Uses reflection to fetch the eligible associated objects for the current object, excluding parent objects and child objects that do not include the Diffable
mixin
# File lib/diffable.rb, line 53 def reflected_names(obj) classes = obj.reflections class_names = [] classes.each do |key, cl| if eval(cl.class_name).respond_to?("diffable") \ and cl.association_class != ActiveRecord::Associations::BelongsToAssociation class_names << key end end class_names end
Private Instance Methods
analyze_subobjects(current_obj, previous_obj, change={})
click to toggle source
# File lib/diffable.rb, line 144 def analyze_subobjects(current_obj, previous_obj, change={}) #need both - comparable objects need not have the same reflections current_subs = reflected_names(current_obj) previous_subs = reflected_names(previous_obj) #things that are available to the current object current_subs.each do |sub| objects = [] current_objects = current_obj.association(sub).target previous_objects = previous_obj.respond_to?(sub) ? eval("previous_obj.#{sub}.to_a") : [] current_obj_idents = map_obj_idents(current_objects) previous_obj_idents = map_obj_idents(previous_objects) #loop through the ids in the current block objects += compare_current_subs(current_obj_idents, previous_obj_idents, current_objects, previous_objects) #look for ids that only exist in the previous block objects += preserve_deleted_by_ident((previous_obj_idents - current_obj_idents), previous_subs, previous_obj, sub) #update time_blocks if any changes were found change[sub] = objects unless objects.empty? end #things that are only available to the previous object (previous_subs - current_subs).each do |sub| objects = [] previous_obj_idents = map_obj_idents(previous_obj) objects += preserve_deleted_by_ident(previous_obj_idents, (previous_subs - current_subs), previous_obj, sub) change[sub] = objects unless objects.empty? end change end
check_class_compatibility(current, other)
click to toggle source
# File lib/diffable.rb, line 67 def check_class_compatibility(current, other) current_super = current.class.superclass other_super = other.class.superclass if current_super == ActiveRecord::Base || other_super == ActiveRecord::Base if other.class != current.class && other.class != current_super && other_super != current.class raise "Unable to compare #{current.class} to #{other.class}" end else if current.class != other.class && other.class.superclass != current.class.superclass raise "Unable to compare #{current.class} to #{other.class}" end end end
compare_attributes(current, previous, current_obj, change={})
click to toggle source
# File lib/diffable.rb, line 131 def compare_attributes(current, previous, current_obj, change={}) previous.each do |key, value| change[key.to_sym] = value if value != current[key] end unless change.empty? if current_obj.class.unique_within_group unique_field = current_obj.class.unique_within_group change[unique_field.to_sym] = eval("current_obj.#{unique_field}") end end change end
compare_current_subs(current_obj_idents, previous_obj_idents, current_subs, previous_subs)
click to toggle source
# File lib/diffable.rb, line 94 def compare_current_subs(current_obj_idents, previous_obj_idents, current_subs, previous_subs) objects = [] current_obj_idents.each do |idnt| current_sub = find_in_array_by_ident(current_subs, idnt) previous_sub = find_in_array_by_ident(previous_subs, idnt) if ident_in_list?(idnt, previous_obj_idents) #pre-existing thing, compare the differences... current_attribs = current_sub.get_attributes(current_sub.class.excluded_fields) previous_attribs = previous_sub.get_attributes(previous_sub.class.excluded_fields) obj = compare_objects(current_attribs, previous_attribs, current_sub, previous_sub, obj) #...and only store if something's changed unless obj.empty? obj[:change_type] = "modified" objects << obj end else #a new thing, just need to note its arrival unique_field = current_sub.class.unique_within_group objects << {unique_field.to_sym => eval("current_sub.#{unique_field}"), :change_type => "new"} end end objects end
compare_objects(current_attribs, other_attribs, current, other, change={})
click to toggle source
# File lib/diffable.rb, line 208 def compare_objects(current_attribs, other_attribs, current, other, change={}) #compare the simple values change = compare_attributes(current_attribs, other_attribs, current) #analyse the subobjects change = analyze_subobjects(current, other, change) if other.class.conditional_fields other.class.conditional_fields.each do |key| change[key.to_sym] = eval("other.#{key}") unless change.empty? end end change end
find_in_array_by_ident(arr, value)
click to toggle source
# File lib/diffable.rb, line 81 def find_in_array_by_ident(arr, value) arr.select { |x| eval(%Q|x.#{x.class.unique_within_group}|) == value }.first end
ident_in_list?(ident, ident_list)
click to toggle source
# File lib/diffable.rb, line 89 def ident_in_list?(ident, ident_list) return true if ident_list.include?(ident) false end
map_obj_idents(obj)
click to toggle source
# File lib/diffable.rb, line 85 def map_obj_idents(obj) obj.map { |x| x.attributes[x.class.unique_within_group] } end
preserve_deleted_by_ident(deleted_idents, previous_subs, previous_obj, sub)
click to toggle source
# File lib/diffable.rb, line 121 def preserve_deleted_by_ident(deleted_idents, previous_subs, previous_obj, sub) objects = [] deleted_idents.each do |ident| previous_sub = find_in_array_by_ident(eval("previous_obj.#{sub}.to_a"), ident) obj = preserve_deleted_obj(previous_sub) objects << obj end objects end
preserve_deleted_obj(deleted, excluded_fields=self.class.excluded_fields)
click to toggle source
# File lib/diffable.rb, line 177 def preserve_deleted_obj(deleted, excluded_fields=self.class.excluded_fields) obj = {} #get attributes of object marked for deletion... attribs = deleted.get_attributes(deleted.class.excluded_fields) #...and copy them for preservation attribs.keys.each do |att| value = nil if deleted.respond_to?(att) value = eval("deleted.#{att}") end obj[att.to_sym] = value unless value.nil? end #look to see if our target object has sub-objects of its own previous_sub_keys = reflected_names(deleted) #preserve subs obj = preserve_deleted_subs(previous_sub_keys, deleted, obj) unless obj.empty? if deleted.class.conditional_fields deleted.class.conditional_fields.each do |key| obj[key.to_sym] = eval("deleted.#{key}") unless obj.empty? end end obj[:change_type] = "deleted" end obj end
preserve_deleted_subs(keys, deleted, change={})
click to toggle source
# File lib/diffable.rb, line 223 def preserve_deleted_subs(keys, deleted, change={}) keys.each do |sub| subs = [] previous_subs = deleted.respond_to?(sub) ? eval("deleted.#{sub}.to_a") : [] previous_subs.each do |deleted_sub| preserved = preserve_deleted_obj(deleted_sub) subs << preserved end change[sub] = subs unless subs.empty? end change end