module VestalVersions # Provides the ability to manipulate hashes in the specific format that ActiveRecord gives to # dirty attribute changes: string keys and unique, two-element array values. module Changes def self.included(base) # :nodoc: Hash.send(:include, HashMethods) base.class_eval do include InstanceMethods after_update :merge_version_changes end end # Methods available to versioned ActiveRecord::Base instances in order to manage changes used # for version creation. module InstanceMethods # Collects an array of changes from a record's versions between the given range and compiles # them into one summary hash of changes. The +from+ and +to+ arguments can each be either a # version number, a symbol representing an association proxy method, a string representing a # version tag or a version object itself. def changes_between(from, to) from_number, to_number = versions.number_at(from), versions.number_at(to) return {} if from_number == to_number chain = versions.between(from_number, to_number).reject(&:initial?) return {} if chain.empty? backward = from_number > to_number backward ? chain.pop : chain.shift unless from_number == 1 || to_number == 1 chain.inject({}) do |changes, version| changes.append_changes!(backward ? version.changes.reverse_changes : version.changes) end end private # Before a new version is created, the newly-changed attributes are appended onto a hash # of previously-changed attributes. Typically the previous changes will be empty, except in # the case that a control block is used where versions are to be merged. See # VestalVersions::Control for more information. def merge_version_changes version_changes.append_changes!(incremental_version_changes) end # Stores the cumulative changes that are eventually used for version creation. def version_changes @version_changes ||= {} end # Stores the incremental changes that are appended to the cumulative changes before version # creation. Incremental changes are reset when the record is saved because they represent # a subset of the dirty attribute changes, which are reset upon save. def incremental_version_changes changes.slice(*versioned_columns) end # Simply resets the cumulative changes after version creation. def reset_version_changes @version_changes = nil end end # Instance methods included into Hash for dealing with manipulation of hashes in the specific # format of ActiveRecord::Base#changes. module HashMethods # When called on a hash of changes and given a second hash of changes as an argument, # +append_changes+ will run the second hash on top of the first, updating the last element # of each array value with its own, or creating its own key/value pair for missing keys. # Resulting non-unique array values are removed. # # == Example # # first = { # "first_name" => ["Steve", "Stephen"], # "age" => [25, 26] # } # second = { # "first_name" => ["Stephen", "Steve"], # "last_name" => ["Richert", "Jobs"], # "age" => [26, 54] # } # first.append_changes(second) # # => { # "last_name" => ["Richert", "Jobs"], # "age" => [25, 54] # } def append_changes(changes) changes.inject(self) do |new_changes, (attribute, change)| new_change = [new_changes.fetch(attribute, change).first, change.last] new_changes.merge(attribute => new_change) end.reject do |attribute, change| change.first == change.last end end # Destructively appends a given hash of changes onto an existing hash of changes. def append_changes!(changes) replace(append_changes(changes)) end # Appends the existing hash of changes onto a given hash of changes. Relates to the # +append_changes+ method in the same way that Hash#reverse_merge relates to # Hash#merge. def prepend_changes(changes) changes.append_changes(self) end # Destructively prepends a given hash of changes onto an existing hash of changes. def prepend_changes!(changes) replace(prepend_changes(changes)) end # Reverses the array values of a hash of changes. Useful for reversion both backward and # forward through a record's history of changes. def reverse_changes inject({}){|nc,(a,c)| nc.merge!(a => c.reverse) } end # Destructively reverses the array values of a hash of changes. def reverse_changes! replace(reverse_changes) end end end end