require 'active_support/core_ext/array/extract_options'
module ActiveSupport
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
# before or after an alteration of the object state.
#
# Mixing in this module allows you to define callbacks in your class.
#
# Example:
# class Storage
# include ActiveSupport::DeprecatedCallbacks
#
# define_callbacks :before_save, :after_save
# end
#
# class ConfigStorage < Storage
# before_save :saving_message
# def saving_message
# puts "saving..."
# end
#
# after_save do |object|
# puts "saved"
# end
#
# def save
# run_callbacks(:before_save)
# puts "- save"
# run_callbacks(:after_save)
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# saving...
# - save
# saved
#
# Callbacks from parent classes are inherited.
#
# Example:
# class Storage
# include ActiveSupport::DeprecatedCallbacks
#
# define_callbacks :before_save, :after_save
#
# before_save :prepare
# def prepare
# puts "preparing save"
# end
# end
#
# class ConfigStorage < Storage
# before_save :saving_message
# def saving_message
# puts "saving..."
# end
#
# after_save do |object|
# puts "saved"
# end
#
# def save
# run_callbacks(:before_save)
# puts "- save"
# run_callbacks(:after_save)
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# preparing save
# saving...
# - save
# saved
module DeprecatedCallbacks
class CallbackChain < Array
def self.build(kind, *methods, &block)
methods, options = extract_options(*methods, &block)
methods.map! { |method| Callback.new(kind, method, options) }
new(methods)
end
def run(object, options = {}, &terminator)
enumerator = options[:enumerator] || :each
unless block_given?
send(enumerator) { |callback| callback.call(object) }
else
send(enumerator) do |callback|
result = callback.call(object)
break result if terminator.call(result, object)
end
end
end
# TODO: Decompose into more Array like behavior
def replace_or_append!(chain)
if index = index(chain)
self[index] = chain
else
self << chain
end
self
end
def find(callback, &block)
select { |c| c == callback && (!block_given? || yield(c)) }.first
end
def delete(callback)
super(callback.is_a?(Callback) ? callback : find(callback))
end
private
def self.extract_options(*methods, &block)
methods.flatten!
options = methods.extract_options!
methods << block if block_given?
return methods, options
end
def extract_options(*methods, &block)
self.class.extract_options(*methods, &block)
end
end
class Callback
attr_reader :kind, :method, :identifier, :options
def initialize(kind, method, options = {})
@kind = kind
@method = method
@identifier = options[:identifier]
@options = options
end
def ==(other)
case other
when Callback
(self.identifier && self.identifier == other.identifier) || self.method == other.method
else
(self.identifier && self.identifier == other) || self.method == other
end
end
def eql?(other)
self == other
end
def dup
self.class.new(@kind, @method, @options.dup)
end
def hash
if @identifier
@identifier.hash
else
@method.hash
end
end
def call(*args, &block)
evaluate_method(method, *args, &block) if should_run_callback?(*args)
rescue LocalJumpError
raise ArgumentError,
"Cannot yield from a Proc type filter. The Proc must take two " +
"arguments and execute #call on the second argument."
end
private
def evaluate_method(method, *args, &block)
case method
when Symbol
object = args.shift
object.send(method, *args, &block)
when String
eval(method, args.first.instance_eval { binding })
when Proc, Method
method.call(*args, &block)
else
if method.respond_to?(kind)
method.send(kind, *args, &block)
else
raise ArgumentError,
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
"a block to be invoked, or an object responding to the callback method."
end
end
end
def should_run_callback?(*args)
[options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
end
end
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def define_callbacks(*callbacks)
ActiveSupport::Deprecation.warn('ActiveSupport::DeprecatedCallbacks has been deprecated in favor of ActiveSupport::Callbacks', caller)
callbacks.each do |callback|
class_eval <<-"end_eval", __FILE__, __LINE__ + 1
def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block)
callbacks = CallbackChain.build(:#{callback}, *methods, &block) # callbacks = CallbackChain.build(:before_save, *methods, &block)
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
@#{callback}_callbacks.concat callbacks # @before_save_callbacks.concat callbacks
end # end
#
def self.#{callback}_callback_chain # def self.before_save_callback_chain
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
#
if superclass.respond_to?(:#{callback}_callback_chain) # if superclass.respond_to?(:before_save_callback_chain)
CallbackChain.new( # CallbackChain.new(
superclass.#{callback}_callback_chain + # superclass.before_save_callback_chain +
@#{callback}_callbacks # @before_save_callbacks
) # )
else # else
@#{callback}_callbacks # @before_save_callbacks
end # end
end # end
end_eval
end
end
end
# Runs all the callbacks defined for the given options.
#
# If a block is given it will be called after each callback receiving as arguments:
#
# * the result from the callback
# * the object which has the callback
#
# If the result from the block evaluates to +true+, the callback chain is stopped.
#
# Example:
# class Storage
# include ActiveSupport::DeprecatedCallbacks
#
# define_callbacks :before_save, :after_save
# end
#
# class ConfigStorage < Storage
# before_save :pass
# before_save :pass
# before_save :stop
# before_save :pass
#
# def pass
# puts "pass"
# end
#
# def stop
# puts "stop"
# return false
# end
#
# def save
# result = run_callbacks(:before_save) { |result, object| result == false }
# puts "- save" if result
# end
# end
#
# config = ConfigStorage.new
# config.save
#
# Output:
# pass
# pass
# stop
def run_callbacks(kind, options = {}, &block)
self.class.send("#{kind}_callback_chain").run(self, options, &block)
end
end
end