aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/callbacks.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support/callbacks.rb')
-rw-r--r--activesupport/lib/active_support/callbacks.rb264
1 files changed, 168 insertions, 96 deletions
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 0c8c1d7dfb..f32bb8a0cc 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/core_ext/string/filters'
require 'thread'
module ActiveSupport
@@ -78,7 +79,7 @@ module ActiveSupport
# save
# end
def run_callbacks(kind, &block)
- send "run_#{kind}_callbacks", &block
+ send "_run_#{kind}_callbacks", &block
end
private
@@ -121,102 +122,106 @@ module ActiveSupport
ENDING = End.new
class Before
- def self.build(next_callback, user_callback, user_conditions, chain_config, filter)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
halted_lambda = chain_config[:terminator]
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
elsif chain_config.key? :terminator
- halting(next_callback, user_callback, halted_lambda, filter)
+ halting(callback_sequence, user_callback, halted_lambda, filter)
elsif user_conditions.any?
- conditional(next_callback, user_callback, user_conditions)
+ conditional(callback_sequence, user_callback, user_conditions)
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
end
- def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
- lambda { |env|
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
+ callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
- result = user_callback.call target, value
- env.halted = halted_lambda.call(target, result)
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
end
end
- next_callback.call env
- }
+
+ env
+ end
end
private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback, halted_lambda, filter)
- lambda { |env|
+ def self.halting(callback_sequence, user_callback, halted_lambda, filter)
+ callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
unless halted
- result = user_callback.call target, value
- env.halted = halted_lambda.call(target, result)
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
+
if env.halted
target.send :halted_callback_hook, filter
end
end
- next_callback.call env
- }
+
+ env
+ end
end
private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.before do |env|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
- next_callback.call env
- }
+
+ env
+ end
end
private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.before do |env|
user_callback.call env.target, env.value
- next_callback.call env
- }
+
+ env
+ end
end
private_class_method :simple
end
class After
- def self.build(next_callback, user_callback, user_conditions, chain_config)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
if chain_config[:skip_after_callbacks_if_terminated]
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
elsif chain_config.key?(:terminator)
- halting(next_callback, user_callback)
+ halting(callback_sequence, user_callback)
elsif user_conditions.any?
- conditional next_callback, user_callback, user_conditions
+ conditional callback_sequence, user_callback, user_conditions
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
else
if user_conditions.any?
- conditional next_callback, user_callback, user_conditions
+ conditional callback_sequence, user_callback, user_conditions
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
end
end
- def self.halting_and_conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
- env = next_callback.call env
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
target = env.target
value = env.value
halted = env.halted
@@ -224,122 +229,119 @@ module ActiveSupport
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
+
env
- }
+ end
end
private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback)
- lambda { |env|
- env = next_callback.call env
+ def self.halting(callback_sequence, user_callback)
+ callback_sequence.after do |env|
unless env.halted
user_callback.call env.target, env.value
end
+
env
- }
+ end
end
private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
- env = next_callback.call env
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
+
env
- }
+ end
end
private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
- env = next_callback.call env
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.after do |env|
user_callback.call env.target, env.value
+
env
- }
+ end
end
private_class_method :simple
end
class Around
- def self.build(next_callback, user_callback, user_conditions, chain_config)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
elsif chain_config.key? :terminator
- halting(next_callback, user_callback)
+ halting(callback_sequence, user_callback)
elsif user_conditions.any?
- conditional(next_callback, user_callback, user_conditions)
+ conditional(callback_sequence, user_callback, user_conditions)
else
- simple(next_callback, user_callback)
+ simple(callback_sequence, user_callback)
end
end
- def self.halting_and_conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call(target, value) {
- env = next_callback.call env
- env.value
+ run.call.value
}
env
else
- next_callback.call env
+ run.call
end
- }
+ end
end
private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback)
- lambda { |env|
+ def self.halting(callback_sequence, user_callback)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
if env.halted
- next_callback.call env
+ run.call
else
user_callback.call(target, value) {
- env = next_callback.call env
- env.value
+ run.call.value
}
env
end
- }
+ end
end
private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call(target, value) {
- env = next_callback.call env
- env.value
+ run.call.value
}
env
else
- next_callback.call env
+ run.call
end
- }
+ end
end
private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.around do |env, &run|
user_callback.call(env.target, env.value) {
- env = next_callback.call env
- env.value
+ run.call.value
}
env
- }
+ end
end
private_class_method :simple
end
@@ -366,14 +368,14 @@ module ActiveSupport
def filter; @key; end
def raw_filter; @filter; end
- def merge(chain, new_options)
+ def merge_conditional_options(chain, if_option:, unless_option:)
options = {
:if => @if.dup,
:unless => @unless.dup
}
- options[:if].concat Array(new_options.fetch(:unless, []))
- options[:unless].concat Array(new_options.fetch(:if, []))
+ options[:if].concat Array(unless_option)
+ options[:unless].concat Array(if_option)
self.class.build chain, @filter, @kind, options
end
@@ -392,17 +394,17 @@ module ActiveSupport
end
# Wraps code with filter
- def apply(next_callback)
+ def apply(callback_sequence)
user_conditions = conditions_lambdas
user_callback = make_lambda @filter
case kind
when :before
- Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter)
+ Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
when :after
- Filters::After.build(next_callback, user_callback, user_conditions, chain_config)
+ Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
when :around
- Filters::Around.build(next_callback, user_callback, user_conditions, chain_config)
+ Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
end
end
@@ -467,16 +469,59 @@ module ActiveSupport
end
end
+ # Execute before and after filters in a sequence instead of
+ # chaining them with nested lambda calls, see:
+ # https://github.com/rails/rails/issues/18011
+ class CallbackSequence
+ def initialize(&call)
+ @call = call
+ @before = []
+ @after = []
+ end
+
+ def before(&before)
+ @before.unshift(before)
+ self
+ end
+
+ def after(&after)
+ @after.push(after)
+ self
+ end
+
+ def around(&around)
+ CallbackSequence.new do |*args|
+ around.call(*args) {
+ self.call(*args)
+ }
+ end
+ end
+
+ def call(*args)
+ @before.each { |b| b.call(*args) }
+ value = @call.call(*args)
+ @after.each { |a| a.call(*args) }
+ value
+ end
+ end
+
# An Array with a compile method.
class CallbackChain #:nodoc:#
include Enumerable
attr_reader :name, :config
+ # If true, any callback returning +false+ will halt the entire callback
+ # chain and display a deprecation message. If false, callback chains will
+ # only be halted by calling +throw :abort+. Defaults to +true+.
+ class_attribute :halt_and_display_warning_on_return_false
+ self.halt_and_display_warning_on_return_false = true
+
def initialize(name, config)
@name = name
@config = {
- :scope => [ :kind ]
+ scope: [:kind],
+ terminator: default_terminator
}.merge!(config)
@chain = []
@callbacks = nil
@@ -511,8 +556,9 @@ module ActiveSupport
def compile
@callbacks || @mutex.synchronize do
- @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback|
- callback.apply chain
+ final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
+ @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
+ callback.apply callback_sequence
end
end
end
@@ -546,6 +592,28 @@ module ActiveSupport
@callbacks = nil
@chain.delete_if { |c| callback.duplicates?(c) }
end
+
+ def default_terminator
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result = result_lambda.call if result_lambda.is_a?(Proc)
+ if halt_and_display_warning_on_return_false && result == false
+ display_deprecation_warning_for_false_terminator
+ else
+ terminate = false
+ end
+ end
+ terminate
+ end
+ end
+
+ def display_deprecation_warning_for_false_terminator
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails.
+ To explicitly halt a callback chain, please use `throw :abort` instead.
+ MSG
+ end
end
module ClassMethods
@@ -559,7 +627,7 @@ module ActiveSupport
# This is used internally to append, prepend and skip callbacks to the
# CallbackChain.
def __update_callbacks(name) #:nodoc:
- ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
+ ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
chain = target.get_callbacks name
yield target, chain.dup
end
@@ -628,7 +696,7 @@ module ActiveSupport
filter = chain.find {|c| c.matches?(type, filter) }
if filter && options.any?
- new_filter = filter.merge(chain, options)
+ new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
chain.insert(chain.index(filter), new_filter)
end
@@ -659,21 +727,22 @@ module ActiveSupport
# ===== Options
#
# * <tt>:terminator</tt> - Determines when a before filter will halt the
- # callback chain, preventing following callbacks from being called and
- # the event from being triggered. This should be a lambda to be executed.
+ # callback chain, preventing following before and around callbacks from
+ # being called and the event from being triggered.
+ # This should be a lambda to be executed.
# The current object and the return result of the callback will be called
# with the lambda.
#
# define_callbacks :validate, terminator: ->(target, result) { result == false }
#
# In this example, if any before validate callbacks returns +false+,
- # other callbacks are not executed. Defaults to +false+, meaning no value
- # halts the chain.
+ # any successive before and around callback is not executed.
+ # Defaults to +false+, meaning no value halts the chain.
#
# * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
# callbacks should be terminated by the <tt>:terminator</tt> option. By
- # default after callbacks executed no matter if callback chain was
- # terminated or not. Option makes sense only when <tt>:terminator</tt>
+ # default after callbacks are executed no matter if callback chain was
+ # terminated or not. This option makes sense only when <tt>:terminator</tt>
# option is specified.
#
# * <tt>:scope</tt> - Indicates which methods should be executed when an
@@ -719,6 +788,9 @@ module ActiveSupport
# define_callbacks :save, scope: [:name]
#
# would call <tt>Audit#save</tt>.
+ #
+ # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
+ # `!`, `?` or `=`.
def define_callbacks(*names)
options = names.extract_options!
@@ -727,7 +799,7 @@ module ActiveSupport
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def run_#{name}_callbacks(&block)
+ def _run_#{name}_callbacks(&block)
_run_callbacks(_#{name}_callbacks, &block)
end
RUBY