Skip to content
This repository
Browse code

Add support for callbacks

  • Loading branch information...
commit c16c7a8de4e543a92de10a138bdd7caa5ac902d7 1 parent ee80dad
Yehuda Katz wycats authored
6 actionpack/lib/action_controller/abstract/base.rb
@@ -15,10 +15,14 @@ def initialize
15 15
16 16 def process(action_name)
17 17 @_action_name = action_name
18   - send(action_name)
  18 + process_action
19 19 self.response_obj[:body] = self.response_body
20 20 self
21 21 end
22 22
  23 + def process_action
  24 + send(action_name)
  25 + end
  26 +
23 27 end
24 28 end
38 actionpack/lib/action_controller/abstract/callbacks.rb
... ... @@ -1,5 +1,43 @@
1 1 module AbstractController
2 2 module Callbacks
  3 + def self.included(klass)
  4 + klass.class_eval do
  5 + include ActiveSupport::NewCallbacks
  6 + define_callbacks :process_action
  7 + extend ClassMethods
  8 + end
  9 + end
3 10
  11 + def process_action
  12 + _run_process_action_callbacks(action_name) do
  13 + super
  14 + end
  15 + end
  16 +
  17 + module ClassMethods
  18 + def _normalize_callback_options(options)
  19 + if only = options[:only]
  20 + only = Array(only).map {|o| "action_name == :#{o}"}.join(" && ")
  21 + options[:per_key] = {:if => only}
  22 + end
  23 + if except = options[:except]
  24 + except = Array(except).map {|e| "action_name == :#{e}"}.join(" && ")
  25 + options[:per_key] = {:unless => except}
  26 + end
  27 + end
  28 +
  29 + [:before, :after, :around].each do |filter|
  30 + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  31 + def #{filter}_filter(*names, &blk)
  32 + options = names.last.is_a?(Hash) ? names.pop : {}
  33 + _normalize_callback_options(options)
  34 + names.push(blk) if block_given?
  35 + names.each do |name|
  36 + process_action_callback(:#{filter}, name, options)
  37 + end
  38 + end
  39 + RUBY_EVAL
  40 + end
  41 + end
4 42 end
5 43 end
1  activesupport/lib/active_support.rb
@@ -32,6 +32,7 @@ def self.load_all!
32 32 autoload :BufferedLogger, 'active_support/buffered_logger'
33 33 autoload :Cache, 'active_support/cache'
34 34 autoload :Callbacks, 'active_support/callbacks'
  35 + autoload :NewCallbacks, 'active_support/new_callbacks'
35 36 autoload :ConcurrentHash, 'active_support/concurrent_hash'
36 37 autoload :Deprecation, 'active_support/deprecation'
37 38 autoload :Duration, 'active_support/duration'
477 activesupport/lib/active_support/new_callbacks.rb
... ... @@ -0,0 +1,477 @@
  1 +module ActiveSupport
  2 + # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
  3 + # before or after an alteration of the object state.
  4 + #
  5 + # Mixing in this module allows you to define callbacks in your class.
  6 + #
  7 + # Example:
  8 + # class Storage
  9 + # include ActiveSupport::Callbacks
  10 + #
  11 + # define_callbacks :save
  12 + # end
  13 + #
  14 + # class ConfigStorage < Storage
  15 + # save_callback :before, :saving_message
  16 + # def saving_message
  17 + # puts "saving..."
  18 + # end
  19 + #
  20 + # save_callback :after do |object|
  21 + # puts "saved"
  22 + # end
  23 + #
  24 + # def save
  25 + # _run_save_callbacks do
  26 + # puts "- save"
  27 + # end
  28 + # end
  29 + # end
  30 + #
  31 + # config = ConfigStorage.new
  32 + # config.save
  33 + #
  34 + # Output:
  35 + # saving...
  36 + # - save
  37 + # saved
  38 + #
  39 + # Callbacks from parent classes are inherited.
  40 + #
  41 + # Example:
  42 + # class Storage
  43 + # include ActiveSupport::Callbacks
  44 + #
  45 + # define_callbacks :save
  46 + #
  47 + # save_callback :before, :prepare
  48 + # def prepare
  49 + # puts "preparing save"
  50 + # end
  51 + # end
  52 + #
  53 + # class ConfigStorage < Storage
  54 + # save_callback :before, :saving_message
  55 + # def saving_message
  56 + # puts "saving..."
  57 + # end
  58 + #
  59 + # save_callback :after do |object|
  60 + # puts "saved"
  61 + # end
  62 + #
  63 + # def save
  64 + # _run_save_callbacks do
  65 + # puts "- save"
  66 + # end
  67 + # end
  68 + # end
  69 + #
  70 + # config = ConfigStorage.new
  71 + # config.save
  72 + #
  73 + # Output:
  74 + # preparing save
  75 + # saving...
  76 + # - save
  77 + # saved
  78 + module NewCallbacks
  79 + def self.included(klass)
  80 + klass.extend ClassMethods
  81 + end
  82 +
  83 + def run_callbacks(kind, options = {}, &blk)
  84 + send("_run_#{kind}_callbacks", &blk)
  85 + end
  86 +
  87 + class Callback
  88 + @@_callback_sequence = 0
  89 +
  90 + attr_accessor :filter, :kind, :name, :options, :per_key, :klass
  91 + def initialize(filter, kind, options, klass, name)
  92 + @kind, @klass = kind, klass
  93 + @name = name
  94 +
  95 + normalize_options!(options)
  96 +
  97 + @per_key = options.delete(:per_key)
  98 + @raw_filter, @options = filter, options
  99 + @filter = _compile_filter(filter)
  100 + @compiled_options = _compile_options(options)
  101 + @callback_id = next_id
  102 +
  103 + _compile_per_key_options
  104 + end
  105 +
  106 + def clone(klass)
  107 + obj = super()
  108 + obj.klass = klass
  109 + obj.per_key = @per_key.dup
  110 + obj.options = @options.dup
  111 + obj.per_key[:if] = @per_key[:if].dup
  112 + obj.per_key[:unless] = @per_key[:unless].dup
  113 + obj.options[:if] = @options[:if].dup
  114 + obj.options[:unless] = @options[:unless].dup
  115 + obj
  116 + end
  117 +
  118 + def normalize_options!(options)
  119 + options[:if] = Array(options[:if])
  120 + options[:unless] = Array(options[:unless])
  121 +
  122 + options[:per_key] ||= {}
  123 + options[:per_key][:if] = Array(options[:per_key][:if])
  124 + options[:per_key][:unless] = Array(options[:per_key][:unless])
  125 + end
  126 +
  127 + def next_id
  128 + @@_callback_sequence += 1
  129 + end
  130 +
  131 + def matches?(_kind, _name, _filter)
  132 + @kind == _kind &&
  133 + @name == _name &&
  134 + @filter == _filter
  135 + end
  136 +
  137 + def _update_filter(filter_options, new_options)
  138 + filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
  139 + filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
  140 + end
  141 +
  142 + def recompile!(_options, _per_key)
  143 + _update_filter(self.options, _options)
  144 + _update_filter(self.per_key, _per_key)
  145 +
  146 + @callback_id = next_id
  147 + @filter = _compile_filter(@raw_filter)
  148 + @compiled_options = _compile_options(@options)
  149 + _compile_per_key_options
  150 + end
  151 +
  152 + def _compile_per_key_options
  153 + key_options = _compile_options(@per_key)
  154 +
  155 + @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  156 + def _one_time_conditions_valid_#{@callback_id}?
  157 + true #{key_options[0]}
  158 + end
  159 + RUBY_EVAL
  160 + end
  161 +
  162 + # This will supply contents for before and around filters, and no
  163 + # contents for after filters (for the forward pass).
  164 + def start(key = nil, options = {})
  165 + object, terminator = (options || {}).values_at(:object, :terminator)
  166 +
  167 + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
  168 +
  169 + terminator ||= false
  170 +
  171 + # options[0] is the compiled form of supplied conditions
  172 + # options[1] is the "end" for the conditional
  173 +
  174 + if @kind == :before || @kind == :around
  175 + if @kind == :before
  176 + # if condition # before_save :filter_name, :if => :condition
  177 + # filter_name
  178 + # end
  179 + filter = <<-RUBY_EVAL
  180 + unless halted
  181 + result = #{@filter}
  182 + halted ||= (#{terminator})
  183 + end
  184 + RUBY_EVAL
  185 + [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
  186 + else
  187 + # Compile around filters with conditions into proxy methods
  188 + # that contain the conditions.
  189 + #
  190 + # For `around_save :filter_name, :if => :condition':
  191 + #
  192 + # def _conditional_callback_save_17
  193 + # if condition
  194 + # filter_name do
  195 + # yield self
  196 + # end
  197 + # else
  198 + # yield self
  199 + # end
  200 + # end
  201 +
  202 + name = "_conditional_callback_#{@kind}_#{next_id}"
  203 + txt = <<-RUBY_EVAL
  204 + def #{name}(halted)
  205 + #{@compiled_options[0] || "if true"} && !halted
  206 + #{@filter} do
  207 + yield self
  208 + end
  209 + else
  210 + yield self
  211 + end
  212 + end
  213 + RUBY_EVAL
  214 + @klass.class_eval(txt)
  215 + "#{name}(halted) do"
  216 + end
  217 + end
  218 + end
  219 +
  220 + # This will supply contents for around and after filters, but not
  221 + # before filters (for the backward pass).
  222 + def end(key = nil, options = {})
  223 + object = (options || {})[:object]
  224 +
  225 + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
  226 +
  227 + if @kind == :around || @kind == :after
  228 + # if condition # after_save :filter_name, :if => :condition
  229 + # filter_name
  230 + # end
  231 + if @kind == :after
  232 + [@compiled_options[0], @filter, @compiled_options[1]].compact.join("\n")
  233 + else
  234 + "end"
  235 + end
  236 + end
  237 + end
  238 +
  239 + private
  240 + # Options support the same options as filters themselves (and support
  241 + # symbols, string, procs, and objects), so compile a conditional
  242 + # expression based on the options
  243 + def _compile_options(options)
  244 + return [] if options[:if].empty? && options[:unless].empty?
  245 +
  246 + conditions = []
  247 +
  248 + unless options[:if].empty?
  249 + conditions << Array(_compile_filter(options[:if]))
  250 + end
  251 +
  252 + unless options[:unless].empty?
  253 + conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
  254 + end
  255 +
  256 + ["if #{conditions.flatten.join(" && ")}", "end"]
  257 + end
  258 +
  259 + # Filters support:
  260 + # Arrays:: Used in conditions. This is used to specify
  261 + # multiple conditions. Used internally to
  262 + # merge conditions from skip_* filters
  263 + # Symbols:: A method to call
  264 + # Strings:: Some content to evaluate
  265 + # Procs:: A proc to call with the object
  266 + # Objects:: An object with a before_foo method on it to call
  267 + #
  268 + # All of these objects are compiled into methods and handled
  269 + # the same after this point:
  270 + # Arrays:: Merged together into a single filter
  271 + # Symbols:: Already methods
  272 + # Strings:: class_eval'ed into methods
  273 + # Procs:: define_method'ed into methods
  274 + # Objects::
  275 + # a method is created that calls the before_foo method
  276 + # on the object.
  277 + def _compile_filter(filter)
  278 + method_name = "_callback_#{@kind}_#{next_id}"
  279 + case filter
  280 + when Array
  281 + filter.map {|f| _compile_filter(f)}
  282 + when Symbol
  283 + filter
  284 + when Proc
  285 + @klass.send(:define_method, method_name, &filter)
  286 + method_name << (filter.arity == 1 ? "(self)" : "")
  287 + when String
  288 + @klass.class_eval <<-RUBY_EVAL
  289 + def #{method_name}
  290 + #{filter}
  291 + end
  292 + RUBY_EVAL
  293 + method_name
  294 + else
  295 + kind, name = @kind, @name
  296 + @klass.send(:define_method, method_name) do
  297 + filter.send("#{kind}_#{name}", self)
  298 + end
  299 + method_name
  300 + end
  301 + end
  302 + end
  303 +
  304 + # This method_missing is supplied to catch callbacks with keys and create
  305 + # the appropriate callback for future use.
  306 + def method_missing(meth, *args, &blk)
  307 + if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/
  308 + return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
  309 + end
  310 + super
  311 + end
  312 +
  313 + # An Array with a compile method
  314 + class CallbackChain < Array
  315 + def initialize(symbol)
  316 + @symbol = symbol
  317 + end
  318 +
  319 + def compile(key = nil, options = {})
  320 + method = []
  321 + method << "halted = false"
  322 + each do |callback|
  323 + method << callback.start(key, options)
  324 + end
  325 + method << "yield self if block_given?"
  326 + reverse_each do |callback|
  327 + method << callback.end(key, options)
  328 + end
  329 + method.compact.join("\n")
  330 + end
  331 +
  332 + def clone(klass)
  333 + chain = CallbackChain.new(@symbol)
  334 + chain.push(*map {|c| c.clone(klass)})
  335 + end
  336 + end
  337 +
  338 + module ClassMethods
  339 + CHAINS = {:before => :before, :around => :before, :after => :after}
  340 +
  341 + # Make the _run_save_callbacks method. The generated method takes
  342 + # a block that it'll yield to. It'll call the before and around filters
  343 + # in order, yield the block, and then run the after filters.
  344 + #
  345 + # _run_save_callbacks do
  346 + # save
  347 + # end
  348 + #
  349 + # The _run_save_callbacks method can optionally take a key, which
  350 + # will be used to compile an optimized callback method for each
  351 + # key. See #define_callbacks for more information.
  352 + def _define_runner(symbol, str, options)
  353 + str = <<-RUBY_EVAL
  354 + def _run_#{symbol}_callbacks(key = nil)
  355 + if key
  356 + send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? }
  357 + else
  358 + #{str}
  359 + end
  360 + end
  361 + RUBY_EVAL
  362 +
  363 + class_eval str, __FILE__, __LINE__ + 1
  364 +
  365 + before_name, around_name, after_name =
  366 + options.values_at(:before, :after, :around)
  367 + end
  368 +
  369 + # This is called the first time a callback is called with a particular
  370 + # key. It creates a new callback method for the key, calculating
  371 + # which callbacks can be omitted because of per_key conditions.
  372 + def _create_and_run_keyed_callback(klass, kind, key, obj, &blk)
  373 + @_keyed_callbacks ||= {}
  374 + @_keyed_callbacks[[kind, key]] ||= begin
  375 + str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator"))
  376 +
  377 + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  378 + def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks
  379 + #{str}
  380 + end
  381 + RUBY_EVAL
  382 +
  383 + true
  384 + end
  385 +
  386 + obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
  387 + end
  388 +
  389 + # Define callbacks.
  390 + #
  391 + # Creates a <name>_callback method that you can use to add callbacks.
  392 + #
  393 + # Syntax:
  394 + # save_callback :before, :before_meth
  395 + # save_callback :after, :after_meth, :if => :condition
  396 + # save_callback :around {|r| stuff; yield; stuff }
  397 + #
  398 + # The <name>_callback method also updates the _run_<name>_callbacks
  399 + # method, which is the public API to run the callbacks.
  400 + #
  401 + # Also creates a skip_<name>_callback method that you can use to skip
  402 + # callbacks.
  403 + #
  404 + # When creating or skipping callbacks, you can specify conditions that
  405 + # are always the same for a given key. For instance, in ActionPack,
  406 + # we convert :only and :except conditions into per-key conditions.
  407 + #
  408 + # before_filter :authenticate, :except => "index"
  409 + # becomes
  410 + # dispatch_callback :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
  411 + #
  412 + # Per-Key conditions are evaluated only once per use of a given key.
  413 + # In the case of the above example, you would do:
  414 + #
  415 + # run_dispatch_callbacks(action_name) { ... dispatch stuff ... }
  416 + #
  417 + # In that case, each action_name would get its own compiled callback
  418 + # method that took into consideration the per_key conditions. This
  419 + # is a speed improvement for ActionPack.
  420 + def define_callbacks(*symbols)
  421 + terminator = symbols.pop if symbols.last.is_a?(String)
  422 + symbols.each do |symbol|
  423 + self.class_inheritable_accessor("_#{symbol}_terminator")
  424 + self.send("_#{symbol}_terminator=", terminator)
  425 + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
  426 + class_inheritable_accessor :_#{symbol}_callbacks
  427 + self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
  428 +
  429 + def self.#{symbol}_callback(*filters, &blk)
  430 + type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
  431 + options = filters.last.is_a?(Hash) ? filters.pop : {}
  432 + filters.unshift(blk) if block_given?
  433 +
  434 + filters.map! do |filter|
  435 + # overrides parent class
  436 + self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)}
  437 + Callback.new(filter, type, options.dup, self, :#{symbol})
  438 + end
  439 + self._#{symbol}_callbacks.push(*filters)
  440 + _define_runner(:#{symbol},
  441 + self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
  442 + options)
  443 + end
  444 +
  445 + def self.skip_#{symbol}_callback(*filters, &blk)
  446 + type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
  447 + options = filters.last.is_a?(Hash) ? filters.pop : {}
  448 + filters.unshift(blk) if block_given?
  449 + filters.each do |filter|
  450 + self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
  451 +
  452 + filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
  453 + per_key = options[:per_key] || {}
  454 + if filter
  455 + filter.recompile!(options, per_key)
  456 + else
  457 + self._#{symbol}_callbacks.delete(filter)
  458 + end
  459 + _define_runner(:#{symbol},
  460 + self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
  461 + options)
  462 + end
  463 +
  464 + end
  465 +
  466 + def self.reset_#{symbol}_callbacks
  467 + self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
  468 + _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {})
  469 + end
  470 +
  471 + self.#{symbol}_callback(:before)
  472 + RUBY_EVAL
  473 + end
  474 + end
  475 + end
  476 + end
  477 +end
115 activesupport/test/new_callback_inheritance_test.rb
... ... @@ -0,0 +1,115 @@
  1 +require 'test/unit'
  2 +$:.unshift "#{File.dirname(__FILE__)}/../lib"
  3 +require 'active_support'
  4 +
  5 +class GrandParent
  6 + include ActiveSupport::NewCallbacks
  7 +
  8 + attr_reader :log, :action_name
  9 + def initialize(action_name)
  10 + @action_name, @log = action_name, []
  11 + end
  12 +
  13 + define_callbacks :dispatch
  14 + dispatch_callback :before, :before1, :before2, :per_key => {:if => proc {|c| c.action_name == "index" || c.action_name == "update" }}
  15 + dispatch_callback :after, :after1, :after2, :per_key => {:if => proc {|c| c.action_name == "update" || c.action_name == "delete" }}
  16 +
  17 + def before1
  18 + @log << "before1"
  19 + end
  20 +
  21 + def before2
  22 + @log << "before2"
  23 + end
  24 +
  25 + def after1
  26 + @log << "after1"
  27 + end
  28 +
  29 + def after2
  30 + @log << "after2"
  31 + end
  32 +
  33 + def dispatch
  34 + _run_dispatch_callbacks(action_name) do
  35 + @log << action_name
  36 + end
  37 + self
  38 + end
  39 +end
  40 +
  41 +class Parent < GrandParent
  42 + skip_dispatch_callback :before, :before2, :per_key => {:unless => proc {|c| c.action_name == "update" }}
  43 + skip_dispatch_callback :after, :after2, :per_key => {:unless => proc {|c| c.action_name == "delete" }}
  44 +end
  45 +
  46 +class Child < GrandParent
  47 + skip_dispatch_callback :before, :before2, :per_key => {:unless => proc {|c| c.action_name == "update" }}, :if => :state_open?
  48 +
  49 + def state_open?
  50 + @state == :open
  51 + end
  52 +
  53 + def initialize(action_name, state)
  54 + super(action_name)
  55 + @state = state
  56 + end
  57 +end
  58 +
  59 +
  60 +class BasicCallbacksTest < Test::Unit::TestCase
  61 + def setup
  62 + @index = GrandParent.new("index").dispatch
  63 + @update = GrandParent.new("update").dispatch
  64 + @delete = GrandParent.new("delete").dispatch
  65 + @unknown = GrandParent.new("unknown").dispatch
  66 + end
  67 +
  68 + def test_basic_per_key1
  69 + assert_equal %w(before1 before2 index), @index.log
  70 + end
  71 +
  72 + def test_basic_per_key2
  73 + assert_equal %w(before1 before2 update after2 after1), @update.log
  74 + end
  75 +
  76 + def test_basic_per_key3
  77 + assert_equal %w(delete after2 after1), @delete.log
  78 + end
  79 +end
  80 +
  81 +class InheritedCallbacksTest < Test::Unit::TestCase
  82 + def setup
  83 + @index = Parent.new("index").dispatch
  84 + @update = Parent.new("update").dispatch
  85 + @delete = Parent.new("delete").dispatch
  86 + @unknown = Parent.new("unknown").dispatch
  87 + end
  88 +
  89 + def test_inherited_excluded
  90 + assert_equal %w(before1 index), @index.log
  91 + end
  92 +
  93 + def test_inherited_not_excluded
  94 + assert_equal %w(before1 before2 update after1), @update.log
  95 + end
  96 +
  97 + def test_partially_excluded
  98 + assert_equal %w(delete after2 after1), @delete.log
  99 + end
  100 +end
  101 +
  102 +class InheritedCallbacksTest2 < Test::Unit::TestCase
  103 + def setup
  104 + @update1 = Child.new("update", :open).dispatch
  105 + @update2 = Child.new("update", :closed).dispatch
  106 + end
  107 +
  108 + def test_crazy_mix_on
  109 + assert_equal %w(before1 update after2 after1), @update1.log
  110 + end
  111 +
  112 + def test_crazy_mix_off
  113 + assert_equal %w(before1 before2 update after2 after1), @update2.log
  114 + end
  115 +end
382 activesupport/test/new_callbacks_test.rb
... ... @@ -0,0 +1,382 @@
  1 +# require 'abstract_unit'
  2 +require 'test/unit'
  3 +$:.unshift "#{File.dirname(__FILE__)}/../lib"
  4 +require 'active_support'
  5 +
  6 +class Record
  7 + include ActiveSupport::NewCallbacks
  8 +
  9 + define_callbacks :save
  10 +
  11 + def self.before_save(*filters, &blk)
  12 + save_callback(:before, *filters, &blk)
  13 + end
  14 +
  15 + def self.after_save(*filters, &blk)
  16 + save_callback(:after, *filters, &blk)
  17 + end
  18 +
  19 + class << self
  20 + def callback_symbol(callback_method)
  21 + returning(:"#{callback_method}_method") do |method_name|
  22 + define_method(method_name) do
  23 + history << [callback_method, :symbol]
  24 + end
  25 + end
  26 + end
  27 +
  28 + def callback_string(callback_method)
  29 + "history << [#{callback_method.to_sym.inspect}, :string]"
  30 + end
  31 +
  32 + def callback_proc(callback_method)
  33 + Proc.new { |model| model.history << [callback_method, :proc] }
  34 + end
  35 +
  36 + def callback_object(callback_method)
  37 + klass = Class.new
  38 + klass.send(:define_method, callback_method) do |model|
  39 + model.history << [callback_method, :object]
  40 + end
  41 + klass.new
  42 + end
  43 + end
  44 +
  45 + def history
  46 + @history ||= []
  47 + end
  48 +end
  49 +
  50 +class Person < Record
  51 + [:before_save, :after_save].each do |callback_method|
  52 + callback_method_sym = callback_method.to_sym
  53 + send(callback_method, callback_symbol(callback_method_sym))
  54 + send(callback_method, callback_string(callback_method_sym))
  55 + send(callback_method, callback_proc(callback_method_sym))
  56 + send(callback_method, callback_object(callback_method_sym))
  57 + send(callback_method) { |model| model.history << [callback_method_sym, :block] }
  58 + end
  59 +
  60 + def save
  61 + _run_save_callbacks {}
  62 + end
  63 +end
  64 +
  65 +class PersonSkipper < Person
  66 + skip_save_callback :before, :before_save_method, :if => :yes
  67 + skip_save_callback :after, :before_save_method, :unless => :yes
  68 + skip_save_callback :after, :before_save_method, :if => :no
  69 + skip_save_callback :before, :before_save_method, :unless => :no
  70 + def yes; true; end
  71 + def no; false; end
  72 +end
  73 +
  74 +class ParentController
  75 + include ActiveSupport::NewCallbacks
  76 +
  77 + define_callbacks :dispatch
  78 +
  79 + dispatch_callback :before, :log, :per_key => {:unless => proc {|c| c.action_name == :index || c.action_name == :show }}
  80 + dispatch_callback :after, :log2
  81 +
  82 + attr_reader :action_name, :logger
  83 + def initialize(action_name)
  84 + @action_name, @logger = action_name, []
  85 + end
  86 +
  87 + def log
  88 + @logger << action_name
  89 + end
  90 +
  91 + def log2
  92 + @logger << action_name
  93 + end
  94 +
  95 + def dispatch
  96 + _run_dispatch_callbacks(action_name) {
  97 + @logger << "Done"
  98 + }
  99 + self
  100 + end
  101 +end
  102 +
  103 +class Child < ParentController
  104 + skip_dispatch_callback :before, :log, :per_key => {:if => proc {|c| c.action_name == :update} }
  105 + skip_dispatch_callback :after, :log2
  106 +end
  107 +
  108 +class OneTimeCompile < Record
  109 + @@starts_true, @@starts_false = true, false
  110 +
  111 + def initialize
  112 + super
  113 + end
  114 +
  115 + before_save Proc.new {|r| r.history << [:before_save, :starts_true, :if] }, :per_key => {:if => :starts_true}
  116 + before_save Proc.new {|r| r.history << [:before_save, :starts_false, :if] }, :per_key => {:if => :starts_false}
  117 + before_save Proc.new {|r| r.history << [:before_save, :starts_true, :unless] }, :per_key => {:unless => :starts_true}
  118 + before_save Proc.new {|r| r.history << [:before_save, :starts_false, :unless] }, :per_key => {:unless => :starts_false}
  119 +
  120 + def starts_true
  121 + if @@starts_true
  122 + @@starts_true = false
  123 + return true
  124 + end
  125 + @@starts_true
  126 + end
  127 +
  128 + def starts_false
  129 + unless @@starts_false
  130 + @@starts_false = true
  131 + return false
  132 + end
  133 + @@starts_false
  134 + end
  135 +
  136 + def save
  137 + _run_save_callbacks(:action) {}
  138 + end
  139 +end
  140 +
  141 +class OneTimeCompileTest < Test::Unit::TestCase
  142 + def test_optimized_first_compile
  143 + around = OneTimeCompile.new
  144 + around.save
  145 + assert_equal [
  146 + [:before_save, :starts_true, :if],
  147 + [:before_save, :starts_true, :unless]
  148 + ], around.history
  149 + end
  150 +end
  151 +
  152 +class ConditionalPerson < Record
  153 + # proc
  154 + before_save Proc.new { |r| r.history << [:before_save, :proc] }, :if => Proc.new { |r| true }
  155 + before_save Proc.new { |r| r.history << "b00m" }, :if => Proc.new { |r| false }
  156 + before_save Proc.new { |r| r.history << [:before_save, :proc] }, :unless => Proc.new { |r| false }
  157 + before_save Proc.new { |r| r.history << "b00m" }, :unless => Proc.new { |r| true }
  158 + # symbol
  159 + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :if => :yes
  160 + before_save Proc.new { |r| r.history << "b00m" }, :if => :no
  161 + before_save Proc.new { |r| r.history << [:before_save, :symbol] }, :unless => :no
  162 + before_save Proc.new { |r| r.history << "b00m" }, :unless => :yes
  163 + # string
  164 + before_save Proc.new { |r| r.history << [:before_save, :string] }, :if => 'yes'
  165 + before_save Proc.new { |r| r.history << "b00m" }, :if => 'no'
  166 + before_save Proc.new { |r| r.history << [:before_save, :string] }, :unless => 'no'
  167 + before_save Proc.new { |r| r.history << "b00m" }, :unless => 'yes'
  168 + # Combined if and unless
  169 + before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, :if => :yes, :unless => :no
  170 + before_save Proc.new { |r| r.history << "b00m" }, :if => :yes, :unless => :yes
  171 +
  172 + def yes; true; end
  173 + def other_yes; true; end
  174 + def no; false; end
  175 + def other_no; false; end
  176 +
  177 + def save
  178 + _run_save_callbacks {}
  179 + end
  180 +end
  181 +
  182 +class MySuper
  183 + include ActiveSupport::NewCallbacks
  184 + define_callbacks :save
  185 +end
  186 +
  187 +class AroundPerson < MySuper
  188 + attr_reader :history
  189 +
  190 + save_callback :before, :nope, :if => :no
  191 + save_callback :before, :nope, :unless => :yes
  192 + save_callback :after, :tweedle
  193 + save_callback :before, "tweedle_dee"
  194 + save_callback :before, proc {|m| m.history << "yup" }
  195 + save_callback :before, :nope, :if => proc { false }
  196 + save_callback :before, :nope, :unless => proc { true }
  197 + save_callback :before, :yup, :if => proc { true }
  198 + save_callback :before, :yup, :unless => proc { false }
  199 + save_callback :around, :tweedle_dum
  200 + save_callback :around, :w0tyes, :if => :yes
  201 + save_callback :around, :w0tno, :if => :no
  202 + save_callback :around, :tweedle_deedle
  203 +
  204 + def no; false; end
  205 + def yes; true; end
  206 +
  207 + def nope
  208 + @history << "boom"
  209 + end
  210 +
  211 + def yup
  212 + @history << "yup"
  213 + end
  214 +
  215 + def w0tyes
  216 + @history << "w0tyes before"
  217 + yield
  218 + @history << "w0tyes after"
  219 + end
  220 +
  221 + def w0tno
  222 + @history << "boom"
  223 + yield
  224 + end
  225 +
  226 + def tweedle_dee
  227 + @history << "tweedle dee"
  228 + end
  229 +
  230 + def tweedle_dum
  231 + @history << "tweedle dum pre"
  232 + yield
  233 + @history << "tweedle dum post"
  234 + end
  235 +
  236 + def tweedle
  237 + @history << "tweedle"
  238 + end
  239 +
  240 + def tweedle_deedle
  241 + @history << "tweedle deedle pre"
  242 + yield
  243 + @history << "tweedle deedle post"
  244 + end
  245 +
  246 + def initialize
  247 + @history = []
  248 + end
  249 +
  250 + def save
  251 + _run_save_callbacks do
  252 + @history << "running"
  253 + end
  254 + end
  255 +end
  256 +
  257 +class AroundCallbacksTest < Test::Unit::TestCase
  258 + def test_save_around
  259 + around = AroundPerson.new
  260 + around.save
  261 + assert_equal [
  262 + "tweedle dee",
  263 + "yup", "yup", "yup",
  264 + "tweedle dum pre",
  265 + "w0tyes before",
  266 + "tweedle deedle pre",
  267 + "running",
  268 + "tweedle deedle post",
  269 + "w0tyes after",
  270 + "tweedle dum post",
  271 + "tweedle"
  272 + ], around.history
  273 + end
  274 +end
  275 +
  276 +class SkipCallbacksTest < Test::Unit::TestCase
  277 + def test_skip_person
  278 + person = PersonSkipper.new
  279 + assert_equal [], person.history
  280 + person.save
  281 + assert_equal [
  282 + [:before_save, :string],
  283 + [:before_save, :proc],
  284 + [:before_save, :object],
  285 + [:before_save, :block],
  286 + [:after_save, :block],
  287 + [:after_save, :object],
  288 + [:after_save, :proc],
  289 + [:after_save, :string],
  290 + [:after_save, :symbol]
  291 + ], person.history
  292 + end
  293 +end
  294 +
  295 +class CallbacksTest < Test::Unit::TestCase
  296 + def test_save_person
  297 + person = Person.new
  298 + assert_equal [], person.history
  299 + person.save
  300 + assert_equal [
  301 + [:before_save, :symbol],
  302 + [:before_save, :string],
  303 + [:before_save, :proc],
  304 + [:before_save, :object],
  305 + [:before_save, :block],
  306 + [:after_save, :block],
  307 + [:after_save, :object],
  308 + [:after_save, :proc],
  309 + [:after_save, :string],
  310 + [:after_save, :symbol]
  311 + ], person.history
  312 + end
  313 +end
  314 +
  315 +class ConditionalCallbackTest < Test::Unit::TestCase
  316 + def test_save_conditional_person
  317 + person = ConditionalPerson.new
  318 + person.save
  319 + assert_equal [
  320 + [:before_save, :proc],
  321 + [:before_save, :proc],
  322 + [:before_save, :symbol],
  323 + [:before_save, :symbol],
  324 + [:before_save, :string],
  325 + [:before_save, :string],
  326 + [:before_save, :combined_symbol],
  327 + ], person.history
  328 + end
  329 +end
  330 +
  331 +class CallbackTerminator
  332 + include ActiveSupport::NewCallbacks
  333 +
  334 + define_callbacks :save, "result == :halt"
  335 +
  336 + save_callback :before, :first
  337 + save_callback :before, :second
  338 + save_callback :around, :around_it
  339 + save_callback :before, :third
  340 + save_callback :after, :first
  341 + save_callback :around, :around_it
  342 + save_callback :after, :second
  343 + save_callback :around, :around_it
  344 + save_callback :after, :third
  345 +
  346 +
  347 + attr_reader :history
  348 + def initialize
  349 + @history = []
  350 + end
  351 +
  352 + def around_it
  353 + @history << "around1"
  354 + yield
  355 + @history << "around2"
  356 + end
  357 +
  358 + def first
  359 + @history << "first"
  360 + end
  361 +
  362 + def second
  363 + @history << "second"
  364 + :halt
  365 + end
  366 +
  367 + def third
  368 + @history << "third"
  369 + end
  370 +
  371 + def save
  372 + _run_save_callbacks
  373 + end
  374 +end
  375 +
  376 +class CallbackTerminatorTest < Test::Unit::TestCase
  377 + def test_termination
  378 + terminator = CallbackTerminator.new
  379 + terminator.save
  380 + assert_equal ["first", "second", "third", "second", "first"], terminator.history
  381 + end
  382 +end

0 comments on commit c16c7a8

Please sign in to comment.
Something went wrong with that request. Please try again.