aboutsummaryrefslogblamecommitdiffstats
path: root/activesupport/lib/active_support/callbacks.rb
blob: 9831e82319113a97e6b3ae823209a96fd79aa3a8 (plain) (tree)
1
2
3
4
5
                    


                                                                                       
                                                                       







































































                                                  























































































                                                                                                        

                                                            
                                                                                       



                                          
                                                                                     



































                                                                                     




                                                                                    
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::Callbacks
  #
  #     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::Callbacks
  #
  #     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 Callbacks
    class Callback
      def self.run(callbacks, object, options = {}, &terminator)
        enumerator  = options[:enumerator] || :each

        unless block_given?
          callbacks.send(enumerator) { |callback| callback.call(object) }
        else
          callbacks.send(enumerator) do |callback|
            result = callback.call(object)
            break result if terminator.call(result, object)
          end
        end
      end

      attr_reader :kind, :method, :identifier, :options

      def initialize(kind, method, options = {})
        @kind       = kind
        @method     = method
        @identifier = options[:identifier]
        @options    = options
      end

      def call(object)
        evaluate_method(method, object) if should_run_callback?(object)
      end

      private
        def evaluate_method(method, object)
          case method
            when Symbol
              object.send(method)
            when String
              eval(method, object.instance_eval { binding })
            when Proc, Method
              method.call(object)
            else
              if method.respond_to?(kind)
                method.send(kind, object)
              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?(object)
          if options[:if]
            evaluate_method(options[:if], object)
          elsif options[:unless]
            !evaluate_method(options[:unless], object)
          else
            true
          end
        end
    end

    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def define_callbacks(*callbacks)
        callbacks.each do |callback|
          class_eval <<-"end_eval"
            def self.#{callback}(*methods, &block)
              options = methods.extract_options!
              methods << block if block_given?
              callbacks = methods.map { |method| Callback.new(:#{callback}, method, options) }
              (@#{callback}_callbacks ||= []).concat callbacks
            end

            def self.#{callback}_callback_chain
              @#{callback}_callbacks ||= []

              if superclass.respond_to?(:#{callback}_callback_chain)
                superclass.#{callback}_callback_chain + @#{callback}_callbacks
              else
                @#{callback}_callbacks
              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 false, the callback chain is stopped.
    #
    # Example:
    #   class Storage
    #     include ActiveSupport::Callbacks
    #   
    #     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)
      Callback.run(self.class.send("#{kind}_callback_chain"), self, options, &block)
    end
  end
end