diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index ed32a89971..f14016027c 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -30,6 +30,7 @@ module ActiveModel extend ActiveSupport::Autoload autoload :AttributeMethods + autoload :Callbacks autoload :Conversion autoload :DeprecatedErrorMethods autoload :Dirty diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb new file mode 100644 index 0000000000..f66a1ddcaa --- /dev/null +++ b/activemodel/lib/active_model/callbacks.rb @@ -0,0 +1,91 @@ +require 'active_support/callbacks' + +module ActiveModel + module Callbacks + def self.extended(base) + base.class_eval do + include ActiveSupport::Callbacks + end + end + + # Define callbacks similar to ActiveRecord ones. It means: + # + # * The callback chain is aborted whenever the block given to + # _run_callbacks returns false. + # + # * If a class is given to the fallback, it will search for + # before_create, around_create and after_create methods. + # + # == Usage + # + # First you need to define which callbacks your model will have: + # + # class MyModel + # define_model_callbacks :create + # end + # + # This will define three class methods: before_create, around_create, + # and after_create. They accept a symbol, a string, an object or a block. + # + # After you create a callback, you need to tell when they are executed. + # For example, you could do: + # + # def create + # _run_create_callbacks do + # super + # end + # end + # + # == Options + # + # define_model_callbacks accepts all options define_callbacks does, in + # case you want to overwrite a default. Besides that, it also accepts + # an :only option, where you can choose if you want all types (before, + # around or after) or just some: + # + # define_model_callbacks :initializer, :only => :after + # + def define_model_callbacks(*callbacks) + options = callbacks.extract_options! + options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options) + + types = Array(options.delete(:only)) + types = [:before, :around, :after] if types.empty? + + callbacks.each do |callback| + define_callbacks(callback, options) + + types.each do |type| + send(:"_define_#{type}_model_callback", self, callback) + end + end + end + + def _define_before_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.before_#{callback}(*args, &block) + set_callback(:#{callback}, :before, *args, &block) + end + CALLBACK + end + + def _define_around_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.around_#{callback}(*args, &block) + set_callback(:#{callback}, :around, *args, &block) + end + CALLBACK + end + + def _define_after_model_callback(klass, callback) #:nodoc: + klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + def self.after_#{callback}(*args, &block) + options = args.extract_options! + options[:prepend] = true + options[:if] = Array(options[:if]) << "!halted && value != false" + set_callback(:#{callback}, :after, *(args << options), &block) + end + CALLBACK + end + end +end \ No newline at end of file diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb new file mode 100644 index 0000000000..b996f51d6b --- /dev/null +++ b/activemodel/test/cases/callbacks_test.rb @@ -0,0 +1,70 @@ +require "cases/helper" + +class CallbacksTest < ActiveModel::TestCase + + class CallbackValidator + def around_create(model) + model.callbacks << :before_around_create + yield + model.callbacks << :after_around_create + end + end + + class ModelCallbacks + attr_reader :callbacks + extend ActiveModel::Callbacks + + define_model_callbacks :create + define_model_callbacks :initialize, :only => :after + + before_create :before_create + around_create CallbackValidator.new + + after_create do |model| + model.callbacks << :after_create + end + + after_create "@callbacks << :final_callback" + + def initialize(valid=true) + @callbacks, @valid = [], valid + end + + def before_create + @callbacks << :before_create + end + + def create + _run_create_callbacks do + @callbacks << :create + @valid + end + end + end + + test "complete callback chain" do + model = ModelCallbacks.new + model.create + assert_equal model.callbacks, [ :before_create, :before_around_create, :create, + :after_around_create, :after_create, :final_callback] + end + + test "after callbacks are always appended" do + model = ModelCallbacks.new + model.create + assert_equal model.callbacks.last, :final_callback + end + + test "after callbacks are not executed if the block returns false" do + model = ModelCallbacks.new(false) + model.create + assert_equal model.callbacks, [ :before_create, :before_around_create, + :create, :after_around_create] + end + + test "only selects which types of callbacks should be created" do + assert !ModelCallbacks.respond_to?(:before_initialize) + assert !ModelCallbacks.respond_to?(:around_initialize) + assert ModelCallbacks.respond_to?(:after_initialize) + end +end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index b25893a1c3..e1d772bd95 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -1,5 +1,3 @@ -require 'observer' - module ActiveRecord # Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic # before or after an alteration of the object state. This can be used to make sure that associated and @@ -210,7 +208,6 @@ module ActiveRecord # instead of quietly returning +false+. module Callbacks extend ActiveSupport::Concern - include ActiveSupport::Callbacks CALLBACKS = [ :after_initialize, :after_find, :before_validation, :after_validation, @@ -224,60 +221,14 @@ module ActiveRecord alias_method_chain method, :callbacks end - define_callbacks :initialize, :find, :save, :create, :update, :destroy, - :validation, :terminator => "result == false", :scope => [:kind, :name] + extend ActiveModel::Callbacks + + define_model_callbacks :initialize, :find, :only => :after + define_model_callbacks :save, :create, :update, :destroy + define_model_callbacks :validation, :only => [:before, :after] end module ClassMethods - def after_initialize(*args, &block) - options = args.extract_options! - options[:prepend] = true - set_callback(:initialize, :after, *(args << options), &block) - end - - def after_find(*args, &block) - options = args.extract_options! - options[:prepend] = true - set_callback(:find, :after, *(args << options), &block) - end - - [:save, :create, :update, :destroy].each do |callback| - module_eval <<-CALLBACKS, __FILE__, __LINE__ - def before_#{callback}(*args, &block) - set_callback(:#{callback}, :before, *args, &block) - end - - def around_#{callback}(*args, &block) - set_callback(:#{callback}, :around, *args, &block) - end - - def after_#{callback}(*args, &block) - options = args.extract_options! - options[:prepend] = true - options[:if] = Array(options[:if]) << "!halted && value != false" - set_callback(:#{callback}, :after, *(args << options), &block) - end - CALLBACKS - end - - def before_validation(*args, &block) - options = args.extract_options! - if options[:on] - options[:if] = Array(options[:if]) - options[:if] << "@_on_validate == :#{options[:on]}" - end - set_callback(:validation, :before, *(args << options), &block) - end - - def after_validation(*args, &block) - options = args.extract_options! - options[:if] = Array(options[:if]) - options[:if] << "!halted" - options[:if] << "@_on_validate == :#{options[:on]}" if options[:on] - options[:prepend] = true - set_callback(:validation, :after, *(args << options), &block) - end - def method_added(meth) super if CALLBACKS.include?(meth.to_sym)