module VestalVersions
# The control feature allows use of several code blocks that provide finer control over whether
# a new version is created, or a previous version is updated.
module Control
extend ActiveSupport::Concern
included do
class_attribute :_skip_version, :instance_writer => false
end
# Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with
# other instances of the versioned class whose behavior could be inadvertently altered within
# a control block.
module InstanceMethods
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
# ActiveRecord model while ignoring all new version creation. The :if and
# :unless conditions (if given) will not be evaulated inside a +skip_version+ block.
#
# When the block closes, the instance is automatically saved, so explicitly saving the
# object within the block is unnecessary.
#
# == Example
#
# user = User.find_by_first_name("Steve")
# user.version # => 1
# user.skip_version do
# user.first_name = "Stephen"
# end
# user.version # => 1
def skip_version
_with_version_flag(:_skip_version) do
yield if block_given?
save
end
end
# Behaving almost identically to the +skip_version+ block, the only difference with the
# +skip_version!+ block is that the save automatically performed at the close of the block
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
def skip_version!
_with_version_flag(:_skip_version) do
yield if block_given?
save!
end
end
# Merging versions with the +merge_version+ block will take all of the versions that would
# be created within the block and merge them into one version and pushing that single version
# onto the ActiveRecord::Base instance's version history. A new version will be created and
# the instance's version number will be incremented.
#
# == Example
#
# user = User.find_by_first_name("Steve")
# user.version # => 1
# user.merge_version do
# user.update_attributes(:first_name => "Steven", :last_name => "Tyler")
# user.update_attribute(:first_name, "Stephen")
# user.update_attribute(:last_name, "Richert")
# end
# user.version # => 2
# user.versions.last.changes
# # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]}
#
# See VestalVersions::Changes for an explanation on how changes are appended.
def merge_version
_with_version_flag(:merge_version) do
yield if block_given?
end
save
end
# Behaving almost identically to the +merge_version+ block, the only difference with the
# +merge_version!+ block is that the save automatically performed at the close of the block
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
def merge_version!
_with_version_flag(:merge_version) do
yield if block_given?
end
save!
end
# A convenience method for determining whether a versioned instance is set to merge its next
# versions into one before version creation.
def merge_version?
!!@merge_version
end
# Appending versions with the +append_version+ block acts similarly to the +merge_version+
# block in that all would-be version creations within the block are defered until the block
# closes. The major difference is that with +append_version+, a new version is not created.
# Rather, the cumulative changes are appended to the serialized changes of the instance's
# last version. A new version is not created, so the version number is not incremented.
#
# == Example
#
# user = User.find_by_first_name("Steve")
# user.version # => 2
# user.versions.last.changes
# # => {"first_name" => ["Stephen", "Steve"]}
# user.append_version do
# user.last_name = "Jobs"
# end
# user.versions.last.changes
# # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]}
# user.version # => 2
#
# See VestalVersions::Changes for an explanation on how changes are appended.
def append_version
_with_version_flag(:merge_version) do
yield if block_given?
end
_with_version_flag(:append_version) do
save
end
end
# Behaving almost identically to the +append_version+ block, the only difference with the
# +append_version!+ block is that the save automatically performed at the close of the block
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
def append_version!
_with_version_flag(:merge_version) do
yield if block_given?
end
_with_version_flag(:append_version) do
save!
end
end
# A convenience method for determining whether a versioned instance is set to append its next
# version's changes into the last version changes.
def append_version?
!!@append_version
end
# Used for each control block, the +_with_version_flag+ method sets a given variable to
# true and then executes the given block, ensuring that the variable is returned to a nil
# value before returning. This is useful to be certain that one of the control flag
# instance variables isn't inadvertently left in the "on" position by execution within the
# block raising an exception.
def _with_version_flag(flag)
instance_variable_set("@#{flag}", true)
yield
ensure
remove_instance_variable("@#{flag}")
end
# Overrides the basal +create_version?+ method to make sure that new versions are not
# created when inside any of the control blocks (until the block terminates).
def create_version?
!_skip_version? && !merge_version? && !append_version? && super
end
# Overrides the basal +update_version?+ method to allow the last version of an versioned
# ActiveRecord::Base instance to be updated at the end of an +append_version+ block.
def update_version?
append_version?
end
end
module ClassMethods
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
# ActiveRecord model while ignoring all new version creation. The :if and
# :unless conditions (if given) will not be evaulated inside a +skip_version+ block.
#
# When the block closes, the instance is automatically saved, so explicitly saving the
# object within the block is unnecessary.
#
# == Example
#
# user = User.find_by_first_name("Steve")
# user.version # => 1
# user.skip_version do
# user.first_name = "Stephen"
# end
# user.version # => 1
def skip_version
_with_version_flag(:_skip_version) do
yield if block_given?
end
end
# Used for each control block, the +with_version_flag+ method sets a given variable to
# true and then executes the given block, ensuring that the variable is returned to a nil
# value before returning. This is useful to be certain that one of the control flag
# instance variables isn't inadvertently left in the "on" position by execution within the
# block raising an exception.
def _with_version_flag(flag)
self.send("#{flag}=", true)
yield
ensure
self.send("#{flag}=", nil)
end
end
end
end