`ActiveSupport::Calbacks#halted_callback_hook` receive callback name:

- The `halted_callback_hook` method is called whenever the
  `terminator` halt the callback execution.
  Usually, this translate to when a `before` callback throw
  an `:abort`.

  <details>
    <summary> Example </summary>

    ```ruby
      class Foo
        include ActiveSupport::Callbacks

	define_callbacks :save
	set_callback(:save, :before) { throw(:abort) }

	def run
	  run_callbacks(:save) do
	    'hello'
	  end
	end

	def halted_callback_hook(filter)
	  # filter is the proc passed to `set_callback` above
	end
      end
    ```
  </details>

  ### Problem

  When a class has multiple callbacks, (i.e. `save`, `validate` ...),
  it's impossible to tell in the halted_callback_hook which type of
  callback halted the execution.
  This is useful to take different action based on the callback.

  <details>
    <summary> Use Case </summary>

    ```ruby
      class Foo
        include ActiveSupport::Callbacks

	define_callbacks :save
	define_callbacks :validate

	set_callback(:save, :before) { throw(:abort) }
	set_callback(:validate, :before) { throw(:abort) }

	def run
	  run_callbacks(:validate) do
	    ...
	  end

	  run_callbacks(:save) do
	    ...
	  end
	end

	def halted_callback_hook(filter)
	  Rails.logger.warn("Couldn't save the record, the ??? callback halted the execution")
	end
      end
    ```
  </details>

  ### Solution

  Allow `halted_callback_hook` to receive a second argument which is
  the name of the callback being run.
This commit is contained in:
Edouard CHIN 2020-02-25 22:09:00 -04:00
parent 51d73fb84b
commit 06dd162fb3
6 changed files with 58 additions and 15 deletions

View File

@ -72,7 +72,7 @@ module ActionController
private
# A hook invoked every time a before callback is halted.
def halted_callback_hook(filter)
def halted_callback_hook(filter, _)
ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
end

View File

@ -29,7 +29,7 @@ module ActiveJob
"#{operation}.active_job", payload.merge(adapter: queue_adapter, job: self), &enhanced_block
end
def halted_callback_hook(_)
def halted_callback_hook(*)
super
@_halted_callback_hook_called = true
end

View File

@ -1,3 +1,26 @@
* [Breaking change] `ActiveSupport::Callbacks#halted_callback_hook` now receive a 2nd argument:
`ActiveSupport::Callbacks#halted_callback_hook` now receive the name of the callback
being halted as second argument.
This change will allow you to differentiate which callbacks halted the chain
and act accordingly.
```ruby
class Book < ApplicationRecord
before_save { throw(:abort) }
before_create { throw(:abort) }
def halted_callback_hook(filter, callback_name)
Rails.logger.info("Book couldn't be #{callback_name}d")
end
Book.create # => "Book couldn't be created"
book.save # => "Book couldn't be saved"
end
```
*Edouard Chin*
* Support `prepend` with `ActiveSupport::Concern`.
Allows a module with `extend ActiveSupport::Concern` to be prepended.
@ -14,7 +37,7 @@
prepend Imposter
end
Class methods are prepended to the base class, concerning is also
Class methods are prepended to the base class, concerning is also
updated: `concerning :Imposter, prepend: true do`.
*Jason Karns, Elia Schito*

View File

@ -143,7 +143,7 @@ module ActiveSupport
# A hook invoked every time a before callback is halted.
# This can be overridden in ActiveSupport::Callbacks implementors in order
# to provide better debugging/logging.
def halted_callback_hook(filter)
def halted_callback_hook(filter, name)
end
module Conditionals # :nodoc:
@ -159,17 +159,17 @@ module ActiveSupport
Environment = Struct.new(:target, :halted, :value)
class Before
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter, name)
halted_lambda = chain_config[:terminator]
if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
else
halting(callback_sequence, user_callback, halted_lambda, filter)
halting(callback_sequence, user_callback, halted_lambda, filter, name)
end
end
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter, name)
callback_sequence.before do |env|
target = env.target
value = env.value
@ -179,7 +179,7 @@ module ActiveSupport
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
target.send :halted_callback_hook, filter, name
end
end
@ -188,7 +188,7 @@ module ActiveSupport
end
private_class_method :halting_and_conditional
def self.halting(callback_sequence, user_callback, halted_lambda, filter)
def self.halting(callback_sequence, user_callback, halted_lambda, filter, name)
callback_sequence.before do |env|
target = env.target
value = env.value
@ -197,9 +197,8 @@ module ActiveSupport
unless halted
result_lambda = -> { user_callback.call target, value }
env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
target.send :halted_callback_hook, filter, name
end
end
@ -337,7 +336,7 @@ module ActiveSupport
case kind
when :before
Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter, name)
when :after
Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
when :around

View File

@ -609,7 +609,7 @@ module CallbacksTest
set_callback :save, :after, :third
end
attr_reader :history, :saved, :halted
attr_reader :history, :saved, :halted, :callback_name
def initialize
@history = []
end
@ -639,8 +639,9 @@ module CallbacksTest
end
end
def halted_callback_hook(filter)
def halted_callback_hook(filter, name)
@halted = filter
@callback_name = name
end
end
@ -823,6 +824,7 @@ module CallbacksTest
terminator = CallbackTerminator.new
terminator.save
assert_equal :second, terminator.halted
assert_equal :save, terminator.callback_name
end
def test_block_never_called_if_terminated

View File

@ -127,6 +127,25 @@ which formats your action accepts, i.e.
format.any(:xml, :json) { render request.format.to_sym => @people }
```
### `ActiveSupport::Callbacks#halted_callback_hook` now receive a second argument
Active Support allows you to override the `halted_callback_hook` whenever a callback
halts the chain. This method now receive a second argument which is the name of the callback being halted.
If you have classes that override this method, make sure it accepts two arguments. Note that this is a breaking
change without a prior deprecation cycle (for performance reasons).
Example:
```ruby
class Book < ApplicationRecord
before_save { throw(:abort) }
before_create { throw(:abort) }
def halted_callback_hook(filter, callback_name) # => This method now accepts 2 arguments instead of 1
Rails.logger.info("Book couldn't be #{callback_name}d")
end
end
```
Upgrading from Rails 5.2 to Rails 6.0
-------------------------------------