From 7ad83b8df2d1b4d4a6ec81903eac48b7248ebd89 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 1 Jan 2005 16:14:15 +0000 Subject: Added block-style for callbacks #332 [bitsweat] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@296 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 10 +- activerecord/lib/active_record/callbacks.rb | 101 ++++++------ activerecord/test/callbacks_test.rb | 230 ++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 51 deletions(-) create mode 100644 activerecord/test/callbacks_test.rb (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 8a0d75ee8a..2de7f35a64 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,13 @@ *SVN* +* Added block-style for callbacks #332 [bitsweat]. + + Before: + before_destroy(Proc.new{ |record| Person.destroy_all "firm_id = #{record.id}" }) + + After: + before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } + * Added automated optimistic locking if the field lock_version is present. Each update to the record increments the lock_version column and the locking facilities ensure that records instantiated twice will let the last one saved raise a StaleObjectError if the first was also updated. Example: @@ -675,7 +683,7 @@ * Fixed PostgreSQL adapter so default values are displayed properly when used in conjunction with Action Pack scaffolding. -* Fixed booleans support for PostgreSQL (use real true/false on boolean fields instead of 0/1 on tinyints) [radsaq] +* Fixed booleans support for PostgreSQL (use real true/falseĀ on boolean fields instead of 0/1 on tinyints) [radsaq] *0.9.2* diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 2ffe9db693..715efe723f 100755 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -34,18 +34,19 @@ module ActiveRecord # end # # class Subscription < ActiveRecord::Base - # # Automatically assign the signup date - # def before_create - # self.signed_up_on = Date.today - # end + # before_create :record_signup + # + # private + # def record_signup + # self.signed_up_on = Date.today + # end # end # # class Firm < ActiveRecord::Base # # Destroys the associated clients and people when the firm is destroyed - # def before_destroy - # Client.destroy_all "client_of = #{id}" - # Person.destroy_all "firm_id = #{id}" - # end + # before_destroy { |record| Person.destroy_all "firm_id = #{record.id}" } + # before_destroy { |record| Client.destroy_all "client_of = #{record.id}" } + # end # # == Inheritable callback queues # @@ -169,9 +170,7 @@ module ActiveRecord alias_method :instantiate_without_callbacks, :instantiate alias_method :instantiate, :instantiate_with_callbacks end - end - base.class_eval do alias_method :initialize_without_callbacks, :initialize alias_method :initialize, :initialize_with_callbacks @@ -191,14 +190,21 @@ module ActiveRecord alias_method :destroy, :destroy_with_callbacks end - CALLBACKS.each { |cb| base.class_eval("def self.#{cb}(*methods) write_inheritable_array(\"#{cb}\", methods) end") } + CALLBACKS.each do |method| + base.class_eval <<-"end_eval" + def self.#{method}(*callbacks, &block) + callbacks << block if block_given? + write_inheritable_array(#{method.to_sym.inspect}, callbacks) + end + end_eval + end end module ClassMethods #:nodoc: def instantiate_with_callbacks(record) object = instantiate_without_callbacks(record) - object.callback(:after_find) if object.respond_to_without_attributes?(:after_find) - object.callback(:after_initialize) if object.respond_to_without_attributes?(:after_initialize) + object.send(:invoke_and_notify, :after_find) + object.send(:invoke_and_notify, :after_initialize) object end end @@ -211,7 +217,7 @@ module ActiveRecord def initialize_with_callbacks(attributes = nil) #:nodoc: initialize_without_callbacks(attributes) result = yield self if block_given? - after_initialize if respond_to_without_attributes?(:after_initialize) + invoke_and_notify(:after_initialize) result end @@ -298,45 +304,40 @@ module ActiveRecord result end - def callback(callback_method) #:nodoc: - run_callbacks(callback_method) - send(callback_method) - notify(callback_method) - end - - def run_callbacks(callback_method) - filters = self.class.read_inheritable_attribute(callback_method.to_s) - if filters.nil? then return end - filters.each do |filter| - if Symbol === filter - self.send(filter) - elsif String === filter - eval(filter, binding) - elsif filter_block?(filter) - filter.call(self) - elsif filter_class?(filter, callback_method) - filter.send(callback_method, self) - else - raise( - ActiveRecordError, - "Filters need to be either a symbol, string (to be eval'ed), proc/method, or " + - "class implementing a static filter method" - ) + private + def callback(method) + callbacks_for(method).each do |callback| + case callback + when Symbol + self.send(callback) + when String + eval(callback, binding) + when Proc, Method + callback.call(self) + else + if callback.respond_to?(method) + callback.send(method, self) + else + 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." + end + end end + + invoke_and_notify(method) end - end - def filter_block?(filter) - filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1) - end + def callbacks_for(method) + self.class.read_inheritable_attribute(method.to_sym) or [] + end - def filter_class?(filter, callback_method) - filter.respond_to?(callback_method) - end + def invoke_and_notify(method) + send(method) if respond_to_without_attributes?(method) + notify(method) + end - def notify(callback_method) #:nodoc: - self.class.changed - self.class.notify_observers(callback_method, self) - end + def notify(method) #:nodoc: + self.class.changed + self.class.notify_observers(method, self) + end end -end +end \ No newline at end of file diff --git a/activerecord/test/callbacks_test.rb b/activerecord/test/callbacks_test.rb new file mode 100644 index 0000000000..3992c0a6a0 --- /dev/null +++ b/activerecord/test/callbacks_test.rb @@ -0,0 +1,230 @@ +require 'abstract_unit' + +class CallbackDeveloper < ActiveRecord::Base + class << self + def table_name; 'developers' end + + def callback_string(callback_method) + "history << [#{callback_method.to_sym.inspect}, :string]" + end + + def callback_proc(callback_method) + Proc.new { |model| model.history << [callback_method, :proc] } + end + + def define_callback_method(callback_method) + define_method("#{callback_method}_method") do |model| + model.history << [callback_method, :method] + end + end + + def callback_object(callback_method) + klass = Class.new + klass.send(:define_method, callback_method) do |model| + model.history << [callback_method, :object] + end + klass.new + end + end + + ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| + callback_method_sym = callback_method.to_sym + define_callback_method(callback_method_sym) + send(callback_method, callback_method_sym) + send(callback_method, callback_string(callback_method_sym)) + send(callback_method, callback_proc(callback_method_sym)) + send(callback_method, callback_object(callback_method_sym)) + send(callback_method) { |model| model.history << [callback_method_sym, :block] } + end + + def history + @history ||= [] + end + + # after_initialize and after_find may not be declared using class methods. + # They are invoked only if instance methods have been defined. + def after_initialize + history << [:after_initialize, :method] + end + + def after_find + history << [:after_find, :method] + end +end + + +class CallbacksTest < Test::Unit::TestCase + def setup + @developers = create_fixtures('developers') + end + + def test_initialize + david = CallbackDeveloper.new + assert_equal [ + [ :after_initialize, :method ] + ], david.history + end + + def test_find + david = CallbackDeveloper.find(1) + assert_equal [ + [ :after_find, :method ], + [ :after_initialize, :method ] + ], david.history + end + + def test_new_valid? + david = CallbackDeveloper.new + david.valid? + assert_equal [ + [ :after_initialize, :method ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_create, :string ], + [ :before_validation_on_create, :proc ], + [ :before_validation_on_create, :object ], + [ :before_validation_on_create, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_create, :string ], + [ :after_validation_on_create, :proc ], + [ :after_validation_on_create, :object ], + [ :after_validation_on_create, :block ] + ], david.history + end + + def test_existing_valid? + david = CallbackDeveloper.find(1) + david.valid? + assert_equal [ + [ :after_find, :method ], + [ :after_initialize, :method ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_update, :string ], + [ :before_validation_on_update, :proc ], + [ :before_validation_on_update, :object ], + [ :before_validation_on_update, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_update, :string ], + [ :after_validation_on_update, :proc ], + [ :after_validation_on_update, :object ], + [ :after_validation_on_update, :block ] + ], david.history + end + + def test_create + david = CallbackDeveloper.create('name' => 'David', 'salary' => 1000000) + assert_equal [ + [ :after_initialize, :method ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_create, :string ], + [ :before_validation_on_create, :proc ], + [ :before_validation_on_create, :object ], + [ :before_validation_on_create, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_create, :string ], + [ :after_validation_on_create, :proc ], + [ :after_validation_on_create, :object ], + [ :after_validation_on_create, :block ], + [ :before_save, :string ], + [ :before_save, :proc ], + [ :before_save, :object ], + [ :before_save, :block ], + [ :before_create, :string ], + [ :before_create, :proc ], + [ :before_create, :object ], + [ :before_create, :block ], + [ :after_create, :string ], + [ :after_create, :proc ], + [ :after_create, :object ], + [ :after_create, :block ], + [ :after_save, :string ], + [ :after_save, :proc ], + [ :after_save, :object ], + [ :after_save, :block ] + ], david.history + end + + def test_save + david = CallbackDeveloper.find(1) + david.save + assert_equal [ + [ :after_find, :method ], + [ :after_initialize, :method ], + [ :before_validation, :string ], + [ :before_validation, :proc ], + [ :before_validation, :object ], + [ :before_validation, :block ], + [ :before_validation_on_update, :string ], + [ :before_validation_on_update, :proc ], + [ :before_validation_on_update, :object ], + [ :before_validation_on_update, :block ], + [ :after_validation, :string ], + [ :after_validation, :proc ], + [ :after_validation, :object ], + [ :after_validation, :block ], + [ :after_validation_on_update, :string ], + [ :after_validation_on_update, :proc ], + [ :after_validation_on_update, :object ], + [ :after_validation_on_update, :block ], + [ :before_save, :string ], + [ :before_save, :proc ], + [ :before_save, :object ], + [ :before_save, :block ], + [ :before_update, :string ], + [ :before_update, :proc ], + [ :before_update, :object ], + [ :before_update, :block ], + [ :after_update, :string ], + [ :after_update, :proc ], + [ :after_update, :object ], + [ :after_update, :block ], + [ :after_save, :string ], + [ :after_save, :proc ], + [ :after_save, :object ], + [ :after_save, :block ] + ], david.history + end + + def test_destroy + david = CallbackDeveloper.find(1) + david.destroy + assert_equal [ + [ :after_find, :method ], + [ :after_initialize, :method ], + [ :before_destroy, :string ], + [ :before_destroy, :proc ], + [ :before_destroy, :object ], + [ :before_destroy, :block ], + [ :after_destroy, :string ], + [ :after_destroy, :proc ], + [ :after_destroy, :object ], + [ :after_destroy, :block ] + ], david.history + end + + def test_delete + david = CallbackDeveloper.find(1) + CallbackDeveloper.delete(david.id) + assert_equal [ + [ :after_find, :method ], + [ :after_initialize, :method ] + ], david.history + end +end -- cgit v1.2.3