# Almost all of this callback stuff is pulled directly from ActiveSupport # in the interest of support rails 2 and 3 at the same time and is the # same copyright as rails. module MongoMapper module Plugins module Callbacks def self.configure(model) model.class_eval do define_callbacks( :before_save, :after_save, :before_create, :after_create, :before_update, :after_update, :before_validation, :after_validation, :before_validation_on_create, :after_validation_on_create, :before_validation_on_update, :after_validation_on_update, :before_destroy, :after_destroy ) end end module ClassMethods def define_callbacks(*callbacks) callbacks.each do |callback| class_eval <<-"end_eval" def self.#{callback}(*methods, &block) callbacks = CallbackChain.build(:#{callback}, *methods, &block) @#{callback}_callbacks ||= CallbackChain.new @#{callback}_callbacks.concat callbacks end def self.#{callback}_callback_chain @#{callback}_callbacks ||= CallbackChain.new if superclass.respond_to?(:#{callback}_callback_chain) CallbackChain.new( superclass.#{callback}_callback_chain + @#{callback}_callbacks ) else @#{callback}_callbacks end end end_eval end end end module InstanceMethods def valid? action = new? ? 'create' : 'update' run_callbacks(:before_validation) run_callbacks("before_validation_on_#{action}".to_sym) result = super run_callbacks("after_validation_on_#{action}".to_sym) run_callbacks(:after_validation) result end def destroy run_callbacks(:before_destroy) result = super run_callbacks(:after_destroy) result end def run_callbacks(kind, options = {}, &block) self.class.send("#{kind}_callback_chain").run(self, options, &block) end private def create_or_update(*args) run_callbacks(:before_save) if result = super run_callbacks(:after_save) end result end def create(*args) run_callbacks(:before_create) result = super run_callbacks(:after_create) result end def update(*args) run_callbacks(:before_update) result = super run_callbacks(:after_update) result end end 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 end end end