Module: ActiveRecord::Acts::Diffable::InstanceMethods

Defined in:
lib/acts_as_diffable.rb

Overview

Adds instance methods.

Instance Method Summary collapse

Instance Method Details

#association_ids(*instances) ⇒ Object

Helper for collecting ids to ignore.



128
129
130
# File 'lib/acts_as_diffable.rb', line 128

def association_ids(*instances)
  ['id'] + instances.collect{|i| i.class.to_s.underscore + '_id'}
end

#attributes_diff(left, right, ignore = [:id]) ⇒ Object

Helper for handing objects with an attributes hash (a la ARec).



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/acts_as_diffable.rb', line 133

def attributes_diff(left, right, ignore = [:id])
  left_attributes = case
    when left.is_a?(Hash) then 
      left
    when left.respond_to?(:attributes) then 
      left.attributes
    else 
      left.instance_values
    end
  right_attributes = case
    when right.is_a?(Hash) then
      right
    when right.respond_to?(:attributes) then
      right.attributes
    else
      right.instance_values
    end

  if left_attributes == right_attributes
    return nil
  else
    generate_diff_hash(left_attributes, right_attributes, *ignore)
  end
end

#diff(other) ⇒ Object

Return a hash of the different attributes between two hashes, such as attributes of an ActiveRecord class.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/acts_as_diffable.rb', line 40

def diff(other)
  # is other an instance or just an id?
  case other.class
  when self.class
    other
  else
    other = self.class.find(other)
  end
  # diff the top-level attributes
  differences = attributes_diff(self, other) || {}
  # has_one and belongs_to associations
  ActiveRecord::Acts::Diffable::SINGULAR_MACROS.each do |macro|
    self.class.reflect_on_all_associations(macro).each do |a|
      differences[a.name.to_s] = singular_association_diff(self, other, a.name) if a.options[:diff]
      differences.delete(a.options[:foreign_key] || "#{a.name}_id")
    end
  end
  # has_many and habtm associations
  ActiveRecord::Acts::Diffable::PLURAL_MACROS.each do |macro|
    self.class.reflect_on_all_associations(macro).each do |a|
      differences[a.name.to_s] = plural_association_diff(self, other, a.name, a.options[:diff_key]) if a.options[:diff_key]
    end
  end
  # manually defined diffs
  self.class.manual_diff_definitions.each{|d_name, d_props|
    if d_props[:diff_key]
      differences[d_name] = plural_association_diff(self, other, d_props[:eval], d_props[:diff_key])
    else
      differences[d_name] = singular_association_diff(self, other, d_props[:eval])
    end
  }
  remove_unchanged_entries differences
end

#generate_diff_hash(left, right, *ignore) ⇒ Object

Accepts a left & right hash, and an array of keys to ignore, returns a hash of the differences.

This here is the meat & potatoes!



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/acts_as_diffable.rb', line 173

def generate_diff_hash(left, right, *ignore)
  case [left.blank?, right.blank?] 
  when [false, true] # the represented object was deleted
    { '_delete' => true } # inspired by nested_attributes
  when [true, false] # the represented object was added
    (ignore + %w(created_at updated_at)).each{|k| right.delete(k.to_s) }
    return right # just return the attributes to add
  when [false, false] # the represented object changed
    # generate the attribute diffs from each side and
    # merge them together as attribute => [left_value, right_value]
    if left == right
      return nil
    else
      diff_hash = left.diff(right).merge(right.diff(left)){|k, lv, rv| [lv, rv] }
      # remove any ignored attributes
      ignore.each {|k| diff_hash.delete(k.to_s) }
      # compress created_at/updated_at duplication
      diff_hash.delete('updated_at') if diff_hash['created_at'] == diff_hash['updated_at']
      remove_unchanged_entries diff_hash
    end
  end
end

#keyify(keyset) ⇒ Object

reduce the key out of an array to a single string if only one element



118
119
120
121
122
123
124
125
# File 'lib/acts_as_diffable.rb', line 118

def keyify(keyset)
  case keyset.size
  when 1
    keyset[0]
  else
    keyset
  end    
end

#plural_association_diff(left_parent, right_parent, association, key_pattern) ⇒ Object

Helper for processing a collection of associated objects, such as has_many (or habtm) association.



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
# File 'lib/acts_as_diffable.rb', line 90

def plural_association_diff(left_parent, right_parent, association, key_pattern)
  key_pattern = Array(key_pattern)
  association = association.to_s #instance_eval doesn't like symbols.  What'ev.
  left_association_set = Array(left_parent.instance_eval(association))
  right_associaton_set = Array(right_parent.instance_eval(association))
  # construct a set of values (key_set) from the attributes defined in key_pattern
  key_sets = (
    left_association_set.collect{|i| key_pattern.collect{|k| i.send(k) } } +
    right_associaton_set.collect{|i| key_pattern.collect{|k| i.send(k) } } ).uniq
  # for each key_set, compare instances in each collection
  diff_set = {}
  key_sets.each do |key_set|
    conditions = {}   
    key_pattern.each_with_index{|k, i| conditions[k] = key_set[i] } 
    left_instance = left_association_set.find{|i|
      conditions.collect{|cf,cv| i.send(cf) == cv}.all?
    }
    right_instance = right_associaton_set.find{|i|
      conditions.collect{|cf,cv| i.send(cf) == cv}.all?
    }
    diff_set[keyify(key_set)] = attributes_diff(left_instance, right_instance, association_ids(left_parent) )
  end
  
  # clean up unchanged pairs
  remove_unchanged_entries diff_set
end

#remove_unchanged_entries(diff_hash) ⇒ Object

Helper for thinning the herd



159
160
161
162
163
164
165
166
167
# File 'lib/acts_as_diffable.rb', line 159

def remove_unchanged_entries(diff_hash)
  return nil if !diff_hash
  diff_hash.delete_if{|k,v| v.nil? }
  if diff_hash.empty?
    return nil 
  else
    return diff_hash
  end
end

#singular_association_diff(left_parent, right_parent, association) ⇒ Object

Helper for processing a single associated object, such as has_one (or belongs_to) associations.



80
81
82
83
84
85
86
# File 'lib/acts_as_diffable.rb', line 80

def singular_association_diff(left_parent, right_parent, association)
  association = association.to_s #instance_eval doesn't like symbols.  What'ev.
  attributes_diff(
    left_parent.instance_eval(association),
    right_parent.instance_eval(association),
    association_ids(left_parent) )
end

#timed_log(start_time, msg) ⇒ Object



74
75
76
# File 'lib/acts_as_diffable.rb', line 74

def timed_log(start_time, msg)
  puts "%04.2fs %s" % [(Time.now - start_time), msg]
end