Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #10727: activesupport_callbacks.diff

File activesupport_callbacks.diff, 19.7 kB (added by josh, 6 months ago)
  • activesupport/test/callbacks_test.rb

    old new  
     1require 'abstract_unit' 
     2 
     3class Record 
     4  include ActiveSupport::Callbacks 
     5 
     6  define_callbacks :before_save, :after_save 
     7 
     8  class << self 
     9    def callback_symbol(callback_method) 
     10      returning("#{callback_method}_method") do |method_name| 
     11        define_method(method_name) do 
     12          history << [callback_method, :symbol] 
     13        end 
     14      end 
     15    end 
     16 
     17    def callback_string(callback_method) 
     18      "history << [#{callback_method.to_sym.inspect}, :string]" 
     19    end 
     20 
     21    def callback_proc(callback_method) 
     22      Proc.new { |model| model.history << [callback_method, :proc] } 
     23    end 
     24 
     25    def callback_object(callback_method) 
     26      klass = Class.new 
     27      klass.send(:define_method, callback_method) do |model| 
     28        model.history << [callback_method, :object] 
     29      end 
     30      klass.new 
     31    end 
     32  end 
     33 
     34  def history 
     35    @history ||= [] 
     36  end 
     37end 
     38 
     39class Person < Record 
     40  [:before_save, :after_save].each do |callback_method| 
     41    callback_method_sym = callback_method.to_sym 
     42    send(callback_method, callback_symbol(callback_method_sym)) 
     43    send(callback_method, callback_string(callback_method_sym)) 
     44    send(callback_method, callback_proc(callback_method_sym)) 
     45    send(callback_method, callback_object(callback_method_sym)) 
     46    send(callback_method) { |model| model.history << [callback_method_sym, :block] } 
     47  end 
     48 
     49  def save 
     50    run_callbacks(:before_save) 
     51    run_callbacks(:after_save) 
     52  end 
     53end 
     54 
     55class ConditionalPerson < Record 
     56  before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true } 
     57  before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false } 
     58  before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false } 
     59  before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true } 
     60 
     61  def save 
     62    run_callbacks(:before_save) 
     63    run_callbacks(:after_save) 
     64  end 
     65end 
     66 
     67class CallbacksTest < Test::Unit::TestCase 
     68  def test_save_person 
     69    person = Person.new 
     70    assert_equal [], person.history 
     71    person.save 
     72    assert_equal [ 
     73      [:before_save, :symbol], 
     74      [:before_save, :string], 
     75      [:before_save, :proc], 
     76      [:before_save, :object], 
     77      [:before_save, :block], 
     78      [:after_save, :symbol], 
     79      [:after_save, :string], 
     80      [:after_save, :proc], 
     81      [:after_save, :object], 
     82      [:after_save, :block] 
     83    ], person.history 
     84  end 
     85end 
     86 
     87class ConditionalCallbackTest < Test::Unit::TestCase 
     88  def test_save_conditional_person 
     89    person = ConditionalPerson.new 
     90    person.save 
     91    assert_equal [ 
     92      [:before_save, :proc], 
     93      [:before_save, :proc] 
     94    ], person.history 
     95  end 
     96end 
  • activesupport/test/test_test.rb

    old new  
    7979  teardown :foo, :sentinel, :foo 
    8080 
    8181  def test_inherited_setup_callbacks 
    82     assert_equal [:reset_callback_record, :foo], self.class.setup_callback_chain 
     82    assert_equal [:reset_callback_record, :foo], self.class.setup_callback_chain.map(&:method) 
    8383    assert_equal [:foo], @called_back 
    84     assert_equal [:foo, :sentinel, :foo], self.class.teardown_callback_chain 
     84    assert_equal [:foo, :sentinel, :foo], self.class.teardown_callback_chain.map(&:method) 
    8585  end 
    8686 
    8787  protected 
     
    104104  teardown :bar 
    105105 
    106106  def test_inherited_setup_callbacks 
    107     assert_equal [:reset_callback_record, :foo, :bar], self.class.setup_callback_chain 
     107    assert_equal [:reset_callback_record, :foo, :bar], self.class.setup_callback_chain.map(&:method) 
    108108    assert_equal [:foo, :bar], @called_back 
    109     assert_equal [:foo, :sentinel, :foo, :bar], self.class.teardown_callback_chain 
     109    assert_equal [:foo, :sentinel, :foo, :bar], self.class.teardown_callback_chain.map(&:method) 
    110110  end 
    111111 
    112112  protected 
     
    115115    end 
    116116 
    117117    def sentinel 
    118       assert_equal [:foo, :bar, :bar, :foo], @called_back 
     118      assert_equal [:foo, :bar, :foo], @called_back 
    119119    end 
    120120end 
  • activesupport/lib/active_support/callbacks.rb

    old new  
     1module ActiveSupport 
     2  module Callbacks 
     3    class Callback 
     4      def self.run(callbacks, object, enumerator = :each) 
     5        callbacks.send(enumerator) do |callback| 
     6          result = callback.call(object) 
     7          return false if result == false 
     8        end 
     9      end 
     10 
     11      attr_reader :name, :method, :options 
     12 
     13      def initialize(name, method, options = {}) 
     14        @name    = name 
     15        @method  = method 
     16        @options = options 
     17      end 
     18 
     19      def call(object) 
     20        evaluate_method(method, object) if should_run_callback?(object) 
     21      end 
     22 
     23      private 
     24        def evaluate_method(method, object) 
     25          case method 
     26            when Symbol 
     27              object.send(method) 
     28            when String 
     29              eval(method, object.instance_eval { binding }) 
     30            when Proc, Method 
     31              method.call(object) 
     32            else 
     33              if method.respond_to?(name) 
     34                method.send(name, object) 
     35              else 
     36                raise ArgumentError, 
     37                  "Callbacks must be a symbol denoting the method to call, a string to be evaluated, " + 
     38                  "a block to be invoked, or an object responding to the callback method." 
     39              end 
     40            end 
     41        end 
     42 
     43        def should_run_callback?(object) 
     44          if options[:if] 
     45            evaluate_method(options[:if], object) 
     46          elsif options[:unless] 
     47            !evaluate_method(options[:unless], object) 
     48          else 
     49            true 
     50          end 
     51        end 
     52    end 
     53 
     54    def self.included(base) 
     55      base.extend ClassMethods 
     56    end 
     57 
     58    module ClassMethods 
     59      def define_callbacks(*callbacks) 
     60        callbacks.each do |callback| 
     61          class_eval <<-"end_eval" 
     62            def self.#{callback}(*methods, &block) 
     63              options = methods.extract_options! 
     64              methods << block if block_given? 
     65              callbacks = methods.map { |method| Callback.new(:#{callback}, method, options) } 
     66              (@#{callback}_callbacks ||= []).concat callbacks 
     67            end 
     68 
     69            def self.#{callback}_callback_chain 
     70              @#{callback}_callbacks ||= [] 
     71 
     72              if superclass.respond_to?(:#{callback}_callback_chain) 
     73                superclass.#{callback}_callback_chain + @#{callback}_callbacks 
     74              else 
     75                @#{callback}_callbacks 
     76              end 
     77            end 
     78          end_eval 
     79        end 
     80      end 
     81    end 
     82 
     83    def run_callbacks(kind, enumerator = :each) 
     84      Callback.run(self.class.send("#{kind}_callback_chain"), self) 
     85    end 
     86  end 
     87end 
  • activesupport/lib/active_support/testing/setup_and_teardown.rb

    old new  
    22  module Testing 
    33    module SetupAndTeardown 
    44      def self.included(base) 
    5         base.extend ClassMethods 
     5        base.send :include, ActiveSupport::Callbacks 
     6        base.define_callbacks :setup, :teardown 
    67 
    78        begin 
    89          require 'mocha' 
     
    1213        end 
    1314      end 
    1415 
    15       module ClassMethods 
    16         def setup(*method_names, &block) 
    17           method_names << block if block_given? 
    18           (@setup_callbacks ||= []).concat method_names 
    19         end 
    20  
    21         def teardown(*method_names, &block) 
    22           method_names << block if block_given? 
    23           (@teardown_callbacks ||= []).concat method_names 
    24         end 
    25  
    26         def setup_callback_chain 
    27           @setup_callbacks ||= [] 
    28  
    29           if superclass.respond_to?(:setup_callback_chain) 
    30             superclass.setup_callback_chain + @setup_callbacks 
    31           else 
    32             @setup_callbacks 
    33           end 
    34         end 
    35  
    36         def teardown_callback_chain 
    37           @teardown_callbacks ||= [] 
    38  
    39           if superclass.respond_to?(:teardown_callback_chain) 
    40             superclass.teardown_callback_chain + @teardown_callbacks 
    41           else 
    42             @teardown_callbacks 
    43           end 
    44         end 
    45       end 
    46  
    4716      # This redefinition is unfortunate but test/unit shows us no alternative. 
    4817      def run_with_callbacks(result) #:nodoc: 
    4918        return if @method_name.to_s == "default_test" 
     
    11180        result.add_run 
    11281        yield(Test::Unit::TestCase::FINISHED, name) 
    11382      end 
    114  
    115       protected 
    116         def run_callbacks(kind, enumerator = :each) 
    117           self.class.send("#{kind}_callback_chain").send(enumerator) do |callback| 
    118             case callback 
    119             when Proc; callback.call(self) 
    120             when String, Symbol; send!(callback) 
    121             else raise ArgumentError, "Unrecognized callback #{callback.inspect}" 
    122             end 
    123           end 
    124         end 
    12583    end 
    12684  end 
    12785end 
  • activesupport/lib/active_support.rb

    old new  
    2626require 'active_support/vendor' 
    2727require 'active_support/basic_object' 
    2828require 'active_support/inflector' 
     29require 'active_support/callbacks' 
    2930 
    3031require 'active_support/core_ext' 
    3132 
  • activerecord/test/validations_test.rb

    old new  
    10171017 
    10181018  def test_invalid_validator 
    10191019    Topic.validate 3 
    1020     assert_raise(ActiveRecord::ActiveRecordError) { t = Topic.create } 
     1020    assert_raise(ArgumentError) { t = Topic.create } 
    10211021  end 
    10221022 
    10231023  def test_throw_away_typing 
  • activerecord/lib/active_record/validations.rb

    old new  
    279279        alias_method_chain :save!, :validation 
    280280        alias_method_chain :update_attribute, :validation_skipping 
    281281      end 
     282 
     283      base.send :include, ActiveSupport::Callbacks 
     284 
     285      # TODO: Use helper ActiveSupport::Callbacks#define_callbacks instead 
     286      %w( validate validate_on_create validate_on_update ).each do |validation_method| 
     287        base.class_eval <<-"end_eval" 
     288          def self.#{validation_method}(*methods, &block) 
     289            options = methods.extract_options! 
     290            methods << block if block_given? 
     291            methods.map! { |method| Callback.new(:#{validation_method}, method, options) } 
     292            existing_methods = read_inheritable_attribute(:#{validation_method}) || [] 
     293            write_inheritable_attribute(:#{validation_method}, existing_methods | methods) 
     294          end 
     295 
     296          def self.#{validation_method}_callback_chain 
     297            read_inheritable_attribute(:#{validation_method}) || [] 
     298          end 
     299        end_eval 
     300      end 
    282301    end 
    283302 
    284303    # All of the following validations are defined in the class scope of the model that you're interested in validating. 
     
    324343      #   end 
    325344      # 
    326345      # This usage applies to #validate_on_create and #validate_on_update as well. 
    327       def validate(*methods, &block) 
    328         methods << block if block_given? 
    329         write_inheritable_set(:validate, methods) 
    330       end 
    331346 
    332       def validate_on_create(*methods, &block) 
    333         methods << block if block_given? 
    334         write_inheritable_set(:validate_on_create, methods) 
    335       end 
    336  
    337       def validate_on_update(*methods, &block) 
    338         methods << block if block_given? 
    339         write_inheritable_set(:validate_on_update, methods) 
    340       end 
    341  
    342       def condition_block?(condition) 
    343         condition.respond_to?("call") && (condition.arity == 1 || condition.arity == -1) 
    344       end 
    345  
    346       # Determine from the given condition (whether a block, procedure, method or string) 
    347       # whether or not to validate the record.  See #validates_each. 
    348       def evaluate_condition(condition, record) 
    349         case condition 
    350           when Symbol; record.send(condition) 
    351           when String; eval(condition, record.instance_eval { binding }) 
    352           else 
    353             if condition_block?(condition) 
    354               condition.call(record) 
    355             else 
    356               raise( 
    357                 ActiveRecordError, 
    358                 "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " + 
    359                 "class implementing a static validation method" 
    360               ) 
    361             end 
    362           end 
    363       end 
    364  
    365347      # Validates each attribute against a block. 
    366348      # 
    367349      #   class Person < ActiveRecord::Base 
     
    379361      #   method, proc or string should return or evaluate to a true or false value. 
    380362      # * <tt>unless</tt> - Specifies a method, proc or string to call to determine if the validation should 
    381363      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    382       #   method, proc or string should return or evaluate to a true or false value.       
     364      #   method, proc or string should return or evaluate to a true or false value. 
    383365      def validates_each(*attrs) 
    384366        options = attrs.extract_options!.symbolize_keys 
    385367        attrs   = attrs.flatten 
    386368 
    387369        # Declare the validation. 
    388         send(validation_method(options[:on] || :save)) do |record| 
    389           # Don't validate when there is an :if condition and that condition is false or there is an :unless condition and that condition is true 
    390           unless (options[:if] && !evaluate_condition(options[:if], record)) || (options[:unless] && evaluate_condition(options[:unless], record)) 
    391             attrs.each do |attr| 
    392               value = record.send(attr) 
    393               next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 
    394               yield record, attr, value 
    395             end 
     370        send(validation_method(options[:on] || :save), options) do |record| 
     371          attrs.each do |attr| 
     372            value = record.send(attr) 
     373            next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 
     374            yield record, attr, value 
    396375          end 
    397376        end 
    398377      end 
     
    515494 
    516495        # can't use validates_each here, because it cannot cope with nonexistent attributes, 
    517496        # while errors.add_on_empty can 
    518         send(validation_method(configuration[:on])) do |record| 
    519           unless (configuration[:if] && !evaluate_condition(configuration[:if], record)) || (configuration[:unless] && evaluate_condition(configuration[:unless], record)) 
    520             record.errors.add_on_blank(attr_names, configuration[:message]) 
    521           end 
    522         end 
     497        send(validation_method(configuration[:on]), configuration) do |record| 
     498          record.errors.add_on_blank(attr_names, configuration[:message]) 
     499        end 
    523500      end 
    524501 
    525502      # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: 
     
    911888        end 
    912889      end 
    913890 
    914  
    915891      private 
    916         def write_inheritable_set(key, methods) 
    917           existing_methods = read_inheritable_attribute(key) || [] 
    918           write_inheritable_attribute(key, existing_methods | methods) 
    919         end 
    920  
    921892        def validation_method(on) 
    922893          case on 
    923894            when :save   then :validate 
     
    959930    def valid? 
    960931      errors.clear 
    961932 
    962       run_validations(:validate) 
     933      run_callbacks(:validate) 
    963934      validate 
    964935 
    965936      if new_record? 
    966         run_validations(:validate_on_create) 
     937        run_callbacks(:validate_on_create) 
    967938        validate_on_create 
    968939      else 
    969         run_validations(:validate_on_update) 
     940        run_callbacks(:validate_on_update) 
    970941        validate_on_update 
    971942      end 
    972943 
     
    990961      # Overwrite this method for validation checks used only on updates. 
    991962      def validate_on_update # :doc: 
    992963      end 
    993  
    994     private 
    995       def run_validations(validation_method) 
    996         validations = self.class.read_inheritable_attribute(validation_method.to_sym) 
    997         if validations.nil? then return end 
    998         validations.each do |validation| 
    999           if validation.is_a?(Symbol) 
    1000             self.send(validation) 
    1001           elsif validation.is_a?(String) 
    1002             eval(validation, binding) 
    1003           elsif validation_block?(validation) 
    1004             validation.call(self) 
    1005           elsif validation_class?(validation, validation_method) 
    1006             validation.send(validation_method, self) 
    1007           else 
    1008             raise( 
    1009               ActiveRecordError, 
    1010               "Validations need to be either a symbol, string (to be eval'ed), proc/method, or " + 
    1011               "class implementing a static validation method" 
    1012             ) 
    1013           end 
    1014         end 
    1015       end 
    1016  
    1017       def validation_block?(validation) 
    1018         validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1) 
    1019       end 
    1020  
    1021       def validation_class?(validation, validation_method) 
    1022         validation.respond_to?(validation_method) 
    1023       end 
    1024964  end 
    1025965end 
  • activerecord/lib/active_record/callbacks.rb

    old new  
    183183        base.send :alias_method_chain, method, :callbacks 
    184184      end 
    185185 
    186       CALLBACKS.each do |method| 
    187         base.class_eval <<-"end_eval" 
    188           def self.#{method}(*callbacks, &block) 
    189             callbacks << block if block_given? 
    190             write_inheritable_array(#{method.to_sym.inspect}, callbacks) 
    191           end 
    192         end_eval 
    193       end 
     186      base.send :include, ActiveSupport::Callbacks 
     187      base.define_callbacks *CALLBACKS 
    194188    end 
    195189 
    196190    # Is called when the object was instantiated by one of the finders, like <tt>Base.find</tt>. 
     
    301295      def callback(method) 
    302296        notify(method) 
    303297 
    304         callbacks_for(method).each do |callback| 
    305           result = case callback 
    306             when Symbol 
    307               self.send(callback) 
    308             when String 
    309               eval(callback, binding) 
    310             when Proc, Method 
    311               callback.call(self) 
    312             else 
    313               if callback.respond_to?(method) 
    314                 callback.send(method, self) 
    315               else 
    316                 raise ActiveRecordError, "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." 
    317               end 
    318           end 
    319           return false if result == false 
     298        result = run_callbacks(method) 
     299 
     300        if result != false && respond_to_without_attributes?(method) 
     301          result = send(method) 
    320302        end 
    321303 
    322         result = send(method) if respond_to_without_attributes?(method) 
    323  
    324304        return result 
    325305      end 
    326306 
    327       def callbacks_for(method) 
    328         self.class.read_inheritable_attribute(method.to_sym) or [] 
    329       end 
    330  
    331       def invoke_and_notify(method) 
    332         notify(method) 
    333         send(method) if respond_to_without_attributes?(method) 
    334       end 
    335  
    336307      def notify(method) #:nodoc: 
    337308        self.class.changed 
    338309        self.class.notify_observers(method, self)