aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/callbacks.rb
blob: ac9f1a9d5f1b86dffdba368096d6195fe9353deb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
module ActiveSupport
  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

    def run_callbacks(kind, options = {}, &block)
      Callback.run(self.class.send("#{kind}_callback_chain"), self, options, &block)
    end
  end
end