From c16c7a8de4e543a92de10a138bdd7caa5ac902d7 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 27 Feb 2009 19:25:45 -0800 Subject: Add support for callbacks --- activesupport/lib/active_support.rb | 1 + activesupport/lib/active_support/new_callbacks.rb | 477 +++++++++++++++++++++ .../test/new_callback_inheritance_test.rb | 115 +++++ activesupport/test/new_callbacks_test.rb | 382 +++++++++++++++++ 4 files changed, 975 insertions(+) create mode 100644 activesupport/lib/active_support/new_callbacks.rb create mode 100644 activesupport/test/new_callback_inheritance_test.rb create mode 100644 activesupport/test/new_callbacks_test.rb (limited to 'activesupport') diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 62d538e2d5..dbd30f9271 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -32,6 +32,7 @@ module ActiveSupport autoload :BufferedLogger, 'active_support/buffered_logger' autoload :Cache, 'active_support/cache' autoload :Callbacks, 'active_support/callbacks' + autoload :NewCallbacks, 'active_support/new_callbacks' autoload :ConcurrentHash, 'active_support/concurrent_hash' autoload :Deprecation, 'active_support/deprecation' autoload :Duration, 'active_support/duration' diff --git a/activesupport/lib/active_support/new_callbacks.rb b/activesupport/lib/active_support/new_callbacks.rb new file mode 100644 index 0000000000..cf717bfbb9 --- /dev/null +++ b/activesupport/lib/active_support/new_callbacks.rb @@ -0,0 +1,477 @@ +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 :save + # end + # + # class ConfigStorage < Storage + # save_callback :before, :saving_message + # def saving_message + # puts "saving..." + # end + # + # save_callback :after do |object| + # puts "saved" + # end + # + # def save + # _run_save_callbacks do + # puts "- save" + # end + # 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 :save + # + # save_callback :before, :prepare + # def prepare + # puts "preparing save" + # end + # end + # + # class ConfigStorage < Storage + # save_callback :before, :saving_message + # def saving_message + # puts "saving..." + # end + # + # save_callback :after do |object| + # puts "saved" + # end + # + # def save + # _run_save_callbacks do + # puts "- save" + # end + # end + # end + # + # config = ConfigStorage.new + # config.save + # + # Output: + # preparing save + # saving... + # - save + # saved + module NewCallbacks + def self.included(klass) + klass.extend ClassMethods + end + + def run_callbacks(kind, options = {}, &blk) + send("_run_#{kind}_callbacks", &blk) + end + + class Callback + @@_callback_sequence = 0 + + attr_accessor :filter, :kind, :name, :options, :per_key, :klass + def initialize(filter, kind, options, klass, name) + @kind, @klass = kind, klass + @name = name + + normalize_options!(options) + + @per_key = options.delete(:per_key) + @raw_filter, @options = filter, options + @filter = _compile_filter(filter) + @compiled_options = _compile_options(options) + @callback_id = next_id + + _compile_per_key_options + end + + def clone(klass) + obj = super() + obj.klass = klass + obj.per_key = @per_key.dup + obj.options = @options.dup + obj.per_key[:if] = @per_key[:if].dup + obj.per_key[:unless] = @per_key[:unless].dup + obj.options[:if] = @options[:if].dup + obj.options[:unless] = @options[:unless].dup + obj + end + + def normalize_options!(options) + options[:if] = Array(options[:if]) + options[:unless] = Array(options[:unless]) + + options[:per_key] ||= {} + options[:per_key][:if] = Array(options[:per_key][:if]) + options[:per_key][:unless] = Array(options[:per_key][:unless]) + end + + def next_id + @@_callback_sequence += 1 + end + + def matches?(_kind, _name, _filter) + @kind == _kind && + @name == _name && + @filter == _filter + end + + def _update_filter(filter_options, new_options) + filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless) + filter_options[:unless].push(new_options[:if]) if new_options.key?(:if) + end + + def recompile!(_options, _per_key) + _update_filter(self.options, _options) + _update_filter(self.per_key, _per_key) + + @callback_id = next_id + @filter = _compile_filter(@raw_filter) + @compiled_options = _compile_options(@options) + _compile_per_key_options + end + + def _compile_per_key_options + key_options = _compile_options(@per_key) + + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def _one_time_conditions_valid_#{@callback_id}? + true #{key_options[0]} + end + RUBY_EVAL + end + + # This will supply contents for before and around filters, and no + # contents for after filters (for the forward pass). + def start(key = nil, options = {}) + object, terminator = (options || {}).values_at(:object, :terminator) + + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") + + terminator ||= false + + # options[0] is the compiled form of supplied conditions + # options[1] is the "end" for the conditional + + if @kind == :before || @kind == :around + if @kind == :before + # if condition # before_save :filter_name, :if => :condition + # filter_name + # end + filter = <<-RUBY_EVAL + unless halted + result = #{@filter} + halted ||= (#{terminator}) + end + RUBY_EVAL + [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n") + else + # Compile around filters with conditions into proxy methods + # that contain the conditions. + # + # For `around_save :filter_name, :if => :condition': + # + # def _conditional_callback_save_17 + # if condition + # filter_name do + # yield self + # end + # else + # yield self + # end + # end + + name = "_conditional_callback_#{@kind}_#{next_id}" + txt = <<-RUBY_EVAL + def #{name}(halted) + #{@compiled_options[0] || "if true"} && !halted + #{@filter} do + yield self + end + else + yield self + end + end + RUBY_EVAL + @klass.class_eval(txt) + "#{name}(halted) do" + end + end + end + + # This will supply contents for around and after filters, but not + # before filters (for the backward pass). + def end(key = nil, options = {}) + object = (options || {})[:object] + + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") + + if @kind == :around || @kind == :after + # if condition # after_save :filter_name, :if => :condition + # filter_name + # end + if @kind == :after + [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n") + else + "end" + end + end + end + + private + # Options support the same options as filters themselves (and support + # symbols, string, procs, and objects), so compile a conditional + # expression based on the options + def _compile_options(options) + return [] if options[:if].empty? && options[:unless].empty? + + conditions = [] + + unless options[:if].empty? + conditions << Array(_compile_filter(options[:if])) + end + + unless options[:unless].empty? + conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} + end + + ["if #{conditions.flatten.join(" && ")}", "end"] + end + + # Filters support: + # Arrays:: Used in conditions. This is used to specify + # multiple conditions. Used internally to + # merge conditions from skip_* filters + # Symbols:: A method to call + # Strings:: Some content to evaluate + # Procs:: A proc to call with the object + # Objects:: An object with a before_foo method on it to call + # + # All of these objects are compiled into methods and handled + # the same after this point: + # Arrays:: Merged together into a single filter + # Symbols:: Already methods + # Strings:: class_eval'ed into methods + # Procs:: define_method'ed into methods + # Objects:: + # a method is created that calls the before_foo method + # on the object. + def _compile_filter(filter) + method_name = "_callback_#{@kind}_#{next_id}" + case filter + when Array + filter.map {|f| _compile_filter(f)} + when Symbol + filter + when Proc + @klass.send(:define_method, method_name, &filter) + method_name << (filter.arity == 1 ? "(self)" : "") + when String + @klass.class_eval <<-RUBY_EVAL + def #{method_name} + #{filter} + end + RUBY_EVAL + method_name + else + kind, name = @kind, @name + @klass.send(:define_method, method_name) do + filter.send("#{kind}_#{name}", self) + end + method_name + end + end + end + + # This method_missing is supplied to catch callbacks with keys and create + # the appropriate callback for future use. + def method_missing(meth, *args, &blk) + if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/ + return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk) + end + super + end + + # An Array with a compile method + class CallbackChain < Array + def initialize(symbol) + @symbol = symbol + end + + def compile(key = nil, options = {}) + method = [] + method << "halted = false" + each do |callback| + method << callback.start(key, options) + end + method << "yield self if block_given?" + reverse_each do |callback| + method << callback.end(key, options) + end + method.compact.join("\n") + end + + def clone(klass) + chain = CallbackChain.new(@symbol) + chain.push(*map {|c| c.clone(klass)}) + end + end + + module ClassMethods + CHAINS = {:before => :before, :around => :before, :after => :after} + + # Make the _run_save_callbacks method. The generated method takes + # a block that it'll yield to. It'll call the before and around filters + # in order, yield the block, and then run the after filters. + # + # _run_save_callbacks do + # save + # end + # + # The _run_save_callbacks method can optionally take a key, which + # will be used to compile an optimized callback method for each + # key. See #define_callbacks for more information. + def _define_runner(symbol, str, options) + str = <<-RUBY_EVAL + def _run_#{symbol}_callbacks(key = nil) + if key + send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? } + else + #{str} + end + end + RUBY_EVAL + + class_eval str, __FILE__, __LINE__ + 1 + + before_name, around_name, after_name = + options.values_at(:before, :after, :around) + end + + # This is called the first time a callback is called with a particular + # key. It creates a new callback method for the key, calculating + # which callbacks can be omitted because of per_key conditions. + def _create_and_run_keyed_callback(klass, kind, key, obj, &blk) + @_keyed_callbacks ||= {} + @_keyed_callbacks[[kind, key]] ||= begin + str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator")) + + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks + #{str} + end + RUBY_EVAL + + true + end + + obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk) + end + + # Define callbacks. + # + # Creates a _callback method that you can use to add callbacks. + # + # Syntax: + # save_callback :before, :before_meth + # save_callback :after, :after_meth, :if => :condition + # save_callback :around {|r| stuff; yield; stuff } + # + # The _callback method also updates the _run__callbacks + # method, which is the public API to run the callbacks. + # + # Also creates a skip__callback method that you can use to skip + # callbacks. + # + # When creating or skipping callbacks, you can specify conditions that + # are always the same for a given key. For instance, in ActionPack, + # we convert :only and :except conditions into per-key conditions. + # + # before_filter :authenticate, :except => "index" + # becomes + # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}} + # + # Per-Key conditions are evaluated only once per use of a given key. + # In the case of the above example, you would do: + # + # run_dispatch_callbacks(action_name) { ... dispatch stuff ... } + # + # In that case, each action_name would get its own compiled callback + # method that took into consideration the per_key conditions. This + # is a speed improvement for ActionPack. + def define_callbacks(*symbols) + terminator = symbols.pop if symbols.last.is_a?(String) + symbols.each do |symbol| + self.class_inheritable_accessor("_#{symbol}_terminator") + self.send("_#{symbol}_terminator=", terminator) + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + class_inheritable_accessor :_#{symbol}_callbacks + self._#{symbol}_callbacks = CallbackChain.new(:#{symbol}) + + def self.#{symbol}_callback(*filters, &blk) + type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before + options = filters.last.is_a?(Hash) ? filters.pop : {} + filters.unshift(blk) if block_given? + + filters.map! do |filter| + # overrides parent class + self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)} + Callback.new(filter, type, options.dup, self, :#{symbol}) + end + self._#{symbol}_callbacks.push(*filters) + _define_runner(:#{symbol}, + self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator), + options) + end + + def self.skip_#{symbol}_callback(*filters, &blk) + type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before + options = filters.last.is_a?(Hash) ? filters.pop : {} + filters.unshift(blk) if block_given? + filters.each do |filter| + self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self) + + filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) } + per_key = options[:per_key] || {} + if filter + filter.recompile!(options, per_key) + else + self._#{symbol}_callbacks.delete(filter) + end + _define_runner(:#{symbol}, + self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator), + options) + end + + end + + def self.reset_#{symbol}_callbacks + self._#{symbol}_callbacks = CallbackChain.new(:#{symbol}) + _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {}) + end + + self.#{symbol}_callback(:before) + RUBY_EVAL + end + end + end + end +end diff --git a/activesupport/test/new_callback_inheritance_test.rb b/activesupport/test/new_callback_inheritance_test.rb new file mode 100644 index 0000000000..95020389b0 --- /dev/null +++ b/activesupport/test/new_callback_inheritance_test.rb @@ -0,0 +1,115 @@ +require 'test/unit' +$:.unshift "#{File.dirname(__FILE__)}/../lib" +require 'active_support' + +class GrandParent + include ActiveSupport::NewCallbacks + + attr_reader :log, :action_name + def initialize(action_name) + @action_name, @log = action_name, [] + end + + define_callbacks :dispatch + dispatch_callback :before, :before1, :before2, :per_key => {:if => proc {|c| c.action_name == "index" || c.action_name == "update" }} + dispatch_callback :after, :after1, :after2, :per_key => {:if => proc {|c| c.action_name == "update" || c.action_name == "delete" }} + + def before1 + @log << "before1" + end + + def before2 + @log << "before2" + end + + def after1 + @log << "after1" + end + + def after2 + @log << "after2" + end + + def dispatch + _run_dispatch_callbacks(action_name) do + @log << action_name + end + self + end +end + +class Parent < GrandParent + skip_dispatch_callback :before, :before2, :per_key => {:unless => proc {|c| c.action_name == "update" }} + skip_dispatch_callback :after, :after2, :per_key => {:unless => proc {|c| c.action_name == "delete" }} +end + +class Child < GrandParent + skip_dispatch_callback :before, :before2, :per_key => {:unless => proc {|c| c.action_name == "update" }}, :if => :state_open? + + def state_open? + @state == :open + end + + def initialize(action_name, state) + super(action_name) + @state = state + end +end + + +class BasicCallbacksTest < Test::Unit::TestCase + def setup + @index = GrandParent.new("index").dispatch + @update = GrandParent.new("update").dispatch + @delete = GrandParent.new("delete").dispatch + @unknown = GrandParent.new("unknown").dispatch + end + + def test_basic_per_key1 + assert_equal %w(before1 before2 index), @index.log + end + + def test_basic_per_key2 + assert_equal %w(before1 before2 update after2 after1), @update.log + end + + def test_basic_per_key3 + assert_equal %w(delete after2 after1), @delete.log + end +end + +class InheritedCallbacksTest < Test::Unit::TestCase + def setup + @index = Parent.new("index").dispatch + @update = Parent.new("update").dispatch + @delete = Parent.new("delete").dispatch + @unknown = Parent.new("unknown").dispatch + end + + def test_inherited_excluded + assert_equal %w(before1 index), @index.log + end + + def test_inherited_not_excluded + assert_equal %w(before1 before2 update after1), @update.log + end + + def test_partially_excluded + assert_equal %w(delete after2 after1), @delete.log + end +end + +class InheritedCallbacksTest2 < Test::Unit::TestCase + def setup + @update1 = Child.new("update", :open).dispatch + @update2 = Child.new("update", :closed).dispatch + end + + def test_crazy_mix_on + assert_equal %w(before1 update after2 after1), @update1.log + end + + def test_crazy_mix_off + assert_equal %w(before1 before2 update after2 after1), @update2.log + end +end \ No newline at end of file diff --git a/activesupport/test/new_callbacks_test.rb b/activesupport/test/new_callbacks_test.rb new file mode 100644 index 0000000000..6948ad23dc --- /dev/null +++ b/activesupport/test/new_callbacks_test.rb @@ -0,0 +1,382 @@ +# require 'abstract_unit' +require 'test/unit' +$:.unshift "#{File.dirname(__FILE__)}/../lib" +require 'active_support' + +class Record + include ActiveSupport::NewCallbacks + + define_callbacks :save + + def self.before_save(*filters, &blk) + save_callback(:before, *filters, &blk) + end + + def self.after_save(*filters, &blk) + save_callback(:after, *filters, &blk) + end + + class << self + def callback_symbol(callback_method) + returning(:"#{callback_method}_method") do |method_name| + define_method(method_name) do + history << [callback_method, :symbol] + end + end + 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 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 + + def history + @history ||= [] + end +end + +class Person < Record + [:before_save, :after_save].each do |callback_method| + callback_method_sym = callback_method.to_sym + send(callback_method, callback_symbol(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 save + _run_save_callbacks {} + end +end + +class PersonSkipper < Person + skip_save_callback :before, :before_save_method, :if => :yes + skip_save_callback :after, :before_save_method, :unless => :yes + skip_save_callback :after, :before_save_method, :if => :no + skip_save_callback :before, :before_save_method, :unless => :no + def yes; true; end + def no; false; end +end + +class ParentController + include ActiveSupport::NewCallbacks + + define_callbacks :dispatch + + dispatch_callback :before, :log, :per_key => {:unless => proc {|c| c.action_name == :index || c.action_name == :show }} + dispatch_callback :after, :log2 + + attr_reader :action_name, :logger + def initialize(action_name) + @action_name, @logger = action_name, [] + end + + def log + @logger << action_name + end + + def log2 + @logger << action_name + end + + def dispatch + _run_dispatch_callbacks(action_name) { + @logger << "Done" + } + self + end +end + +class Child < ParentController + skip_dispatch_callback :before, :log, :per_key => {:if => proc {|c| c.action_name == :update} } + skip_dispatch_callback :after, :log2 +end + +class OneTimeCompile < Record + @@starts_true, @@starts_false = true, false + + def initialize + super + end + + before_save Proc.new {|r| r.history << [:before_save, :starts_true, :if] }, :per_key => {:if => :starts_true} + before_save Proc.new {|r| r.history << [:before_save, :starts_false, :if] }, :per_key => {:if => :starts_false} + before_save Proc.new {|r| r.history << [:before_save, :starts_true, :unless] }, :per_key => {:unless => :starts_true} + before_save Proc.new {|r| r.history << [:before_save, :starts_false, :unless] }, :per_key => {:unless => :starts_false} + + def starts_true + if @@starts_true + @@starts_true = false + return true + end + @@starts_true + end + + def starts_false + unless @@starts_false + @@starts_false = true + return false + end + @@starts_false + end + + def save + _run_save_callbacks(:action) {} + end +end + +class OneTimeCompileTest < Test::Unit::TestCase + def test_optimized_first_compile + around = OneTimeCompile.new + around.save + assert_equal [ + [:before_save, :starts_true, :if], + [:before_save, :starts_true, :unless] + ], around.history + end +end + +class ConditionalPerson < Record + # proc + before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true } + before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false } + before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false } + before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true } + # symbol + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes + before_save Proc.new { |r| r.history << "b00m" }, :if => :no + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no + before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes + # string + before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes' + before_save Proc.new { |r| r.history << "b00m" }, :if => 'no' + before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no' + before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes' + # Combined if and unless + before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no + before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes + + def yes; true; end + def other_yes; true; end + def no; false; end + def other_no; false; end + + def save + _run_save_callbacks {} + end +end + +class MySuper + include ActiveSupport::NewCallbacks + define_callbacks :save +end + +class AroundPerson < MySuper + attr_reader :history + + save_callback :before, :nope, :if => :no + save_callback :before, :nope, :unless => :yes + save_callback :after, :tweedle + save_callback :before, "tweedle_dee" + save_callback :before, proc {|m| m.history << "yup" } + save_callback :before, :nope, :if => proc { false } + save_callback :before, :nope, :unless => proc { true } + save_callback :before, :yup, :if => proc { true } + save_callback :before, :yup, :unless => proc { false } + save_callback :around, :tweedle_dum + save_callback :around, :w0tyes, :if => :yes + save_callback :around, :w0tno, :if => :no + save_callback :around, :tweedle_deedle + + def no; false; end + def yes; true; end + + def nope + @history << "boom" + end + + def yup + @history << "yup" + end + + def w0tyes + @history << "w0tyes before" + yield + @history << "w0tyes after" + end + + def w0tno + @history << "boom" + yield + end + + def tweedle_dee + @history << "tweedle dee" + end + + def tweedle_dum + @history << "tweedle dum pre" + yield + @history << "tweedle dum post" + end + + def tweedle + @history << "tweedle" + end + + def tweedle_deedle + @history << "tweedle deedle pre" + yield + @history << "tweedle deedle post" + end + + def initialize + @history = [] + end + + def save + _run_save_callbacks do + @history << "running" + end + end +end + +class AroundCallbacksTest < Test::Unit::TestCase + def test_save_around + around = AroundPerson.new + around.save + assert_equal [ + "tweedle dee", + "yup", "yup", "yup", + "tweedle dum pre", + "w0tyes before", + "tweedle deedle pre", + "running", + "tweedle deedle post", + "w0tyes after", + "tweedle dum post", + "tweedle" + ], around.history + end +end + +class SkipCallbacksTest < Test::Unit::TestCase + def test_skip_person + person = PersonSkipper.new + assert_equal [], person.history + person.save + assert_equal [ + [:before_save, :string], + [:before_save, :proc], + [:before_save, :object], + [:before_save, :block], + [:after_save, :block], + [:after_save, :object], + [:after_save, :proc], + [:after_save, :string], + [:after_save, :symbol] + ], person.history + end +end + +class CallbacksTest < Test::Unit::TestCase + def test_save_person + person = Person.new + assert_equal [], person.history + person.save + assert_equal [ + [:before_save, :symbol], + [:before_save, :string], + [:before_save, :proc], + [:before_save, :object], + [:before_save, :block], + [:after_save, :block], + [:after_save, :object], + [:after_save, :proc], + [:after_save, :string], + [:after_save, :symbol] + ], person.history + end +end + +class ConditionalCallbackTest < Test::Unit::TestCase + def test_save_conditional_person + person = ConditionalPerson.new + person.save + assert_equal [ + [:before_save, :proc], + [:before_save, :proc], + [:before_save, :symbol], + [:before_save, :symbol], + [:before_save, :string], + [:before_save, :string], + [:before_save, :combined_symbol], + ], person.history + end +end + +class CallbackTerminator + include ActiveSupport::NewCallbacks + + define_callbacks :save, "result == :halt" + + save_callback :before, :first + save_callback :before, :second + save_callback :around, :around_it + save_callback :before, :third + save_callback :after, :first + save_callback :around, :around_it + save_callback :after, :second + save_callback :around, :around_it + save_callback :after, :third + + + attr_reader :history + def initialize + @history = [] + end + + def around_it + @history << "around1" + yield + @history << "around2" + end + + def first + @history << "first" + end + + def second + @history << "second" + :halt + end + + def third + @history << "third" + end + + def save + _run_save_callbacks + end +end + +class CallbackTerminatorTest < Test::Unit::TestCase + def test_termination + terminator = CallbackTerminator.new + terminator.save + assert_equal ["first", "second", "third", "second", "first"], terminator.history + end +end -- cgit v1.2.3