lib/active_support/callbacks.rb in activesupport-4.2.11.3 vs lib/active_support/callbacks.rb in activesupport-5.0.0.beta1
- old
+ new
@@ -2,10 +2,13 @@
require 'active_support/descendants_tracker'
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/module/attribute_accessors'
+require 'active_support/core_ext/string/filters'
+require 'active_support/deprecation'
require 'thread'
module ActiveSupport
# Callbacks are code hooks that are run at key points in an object's life cycle.
# The typical use case is to have a base class define a set of callbacks
@@ -62,10 +65,16 @@
extend ActiveSupport::DescendantsTracker
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
+ # If true, Active Record and Active Model callbacks 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+.
+ mattr_accessor(:halt_and_display_warning_on_return_false) { true }
+
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
# the block (if given one), and then runs the after callbacks in reverse
# order.
@@ -122,30 +131,26 @@
class Before
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?
+ if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
- elsif chain_config.key? :terminator
- halting(callback_sequence, user_callback, halted_lambda, filter)
- elsif user_conditions.any?
- conditional(callback_sequence, user_callback, user_conditions)
else
- simple callback_sequence, user_callback
+ halting(callback_sequence, user_callback, halted_lambda, filter)
end
end
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
@@ -159,57 +164,31 @@
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
env
end
end
private_class_method :halting
-
- 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
-
- env
- end
- end
- private_class_method :conditional
-
- def self.simple(callback_sequence, user_callback)
- callback_sequence.before do |env|
- user_callback.call env.target, env.value
-
- env
- end
- end
- private_class_method :simple
end
class After
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?
+ if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions)
- elsif chain_config.key?(:terminator)
- halting(callback_sequence, user_callback)
- elsif user_conditions.any?
- conditional callback_sequence, user_callback, user_conditions
else
- simple callback_sequence, user_callback
+ halting(callback_sequence, user_callback)
end
else
if user_conditions.any?
conditional callback_sequence, user_callback, user_conditions
else
@@ -268,18 +247,14 @@
private_class_method :simple
end
class Around
def self.build(callback_sequence, user_callback, user_conditions, chain_config)
- if chain_config.key?(:terminator) && user_conditions.any?
+ if user_conditions.any?
halting_and_conditional(callback_sequence, user_callback, user_conditions)
- elsif chain_config.key? :terminator
- halting(callback_sequence, user_callback)
- elsif user_conditions.any?
- conditional(callback_sequence, user_callback, user_conditions)
else
- simple(callback_sequence, user_callback)
+ halting(callback_sequence, user_callback)
end
end
def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
callback_sequence.around do |env, &run|
@@ -287,17 +262,15 @@
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call(target, value) {
- env = run.call env
- env.value
+ run.call.value
}
-
env
else
- run.call env
+ run.call
end
end
end
private_class_method :halting_and_conditional
@@ -305,55 +278,32 @@
callback_sequence.around do |env, &run|
target = env.target
value = env.value
if env.halted
- run.call env
+ run.call
else
user_callback.call(target, value) {
- env = run.call env
- env.value
+ run.call.value
}
env
end
end
end
private_class_method :halting
-
- 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 = run.call env
- env.value
- }
- env
- else
- run.call env
- end
- end
- end
- private_class_method :conditional
-
- def self.simple(callback_sequence, user_callback)
- callback_sequence.around do |env, &run|
- user_callback.call(env.target, env.value) {
- env = run.call env
- env.value
- }
- env
- end
- end
- private_class_method :simple
end
end
class Callback #:nodoc:#
def self.build(chain, filter, kind, options)
+ if filter.is_a?(String)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing string to define callback is deprecated and will be removed
+ in Rails 5.1 without replacement.
+ MSG
+ end
+
new chain.name, filter, kind, options, chain.config
end
attr_accessor :kind, :name
attr_reader :chain_config
@@ -369,18 +319,18 @@
end
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
def matches?(_kind, _filter)
@@ -491,21 +441,21 @@
@after.push(after)
self
end
def around(&around)
- CallbackSequence.new do |*args|
- around.call(*args) {
- self.call(*args)
+ CallbackSequence.new do |arg|
+ around.call(arg) {
+ self.call(arg)
}
end
end
- def call(*args)
- @before.each { |b| b.call(*args) }
- value = @call.call(*args)
- @after.each { |a| a.call(*args) }
+ def call(arg)
+ @before.each { |b| b.call(arg) }
+ value = @call.call(arg)
+ @after.each { |a| a.call(arg) }
value
end
end
# An Array with a compile method.
@@ -515,11 +465,12 @@
attr_reader :name, :config
def initialize(name, config)
@name = name
@config = {
- :scope => [ :kind ]
+ scope: [:kind],
+ terminator: default_terminator
}.merge!(config)
@chain = []
@callbacks = nil
@mutex = Mutex.new
end
@@ -586,10 +537,21 @@
def remove_duplicates(callback)
@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_lambda.call if result_lambda.is_a?(Proc)
+ terminate = false
+ end
+ terminate
+ end
+ end
end
module ClassMethods
def normalize_callback_params(filters, block) # :nodoc:
type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
@@ -611,18 +573,18 @@
#
# set_callback :save, :before, :before_meth
# set_callback :save, :after, :after_meth, if: :condition
# set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
- # The second arguments indicates whether the callback is to be run +:before+,
+ # The second argument indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
# means the first example above can also be written as:
#
# set_callback :save, :before_meth
#
# The callback can be specified as a symbol naming an instance method; as a
- # proc, lambda, or block; as a string to be instance evaluated; or as an
+ # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an
# object that responds to a certain method determined by the <tt>:scope</tt>
# argument to +define_callbacks+.
#
# If a proc, lambda, or block is given, its body is evaluated in the context
# of the current object. It can also optionally accept the current object as
@@ -662,23 +624,31 @@
# callback is skipped.
#
# class Writer < Person
# skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 }
# end
+ #
+ # An <tt>ArgumentError</tt> will be raised if the callback has not
+ # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>).
def skip_callback(name, *filter_list, &block)
type, filters, options = normalize_callback_params(filter_list, block)
+ options[:raise] = true unless options.key?(:raise)
__update_callbacks(name) do |target, chain|
filters.each do |filter|
- filter = chain.find {|c| c.matches?(type, filter) }
+ callback = chain.find {|c| c.matches?(type, filter) }
- if filter && options.any?
- new_filter = filter.merge(chain, options)
- chain.insert(chain.index(filter), new_filter)
+ if !callback && options[:raise]
+ raise ArgumentError, "#{type.to_s.capitalize} #{name} callback #{filter.inspect} has not been defined"
end
- chain.delete(filter)
+ if callback && (options.key?(:if) || options.key?(:unless))
+ new_callback = callback.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
+ chain.insert(chain.index(callback), new_callback)
+ end
+
+ chain.delete(callback)
end
target.set_callbacks name, chain
end
end
@@ -701,25 +671,27 @@
# define_callbacks :initialize, :save, :destroy
#
# ===== 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.
- # The current object and the return result of the callback will be called
- # with the lambda.
+ # 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 result lambda of the callback will be provided
+ # to the terminator lambda.
#
- # define_callbacks :validate, terminator: ->(target, result) { result == false }
+ # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == 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.
#
+ # The default terminator halts the chain when a callback throws +:abort+.
+ #
# * <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
# object is used as a callback.
#
@@ -768,11 +740,11 @@
# `!`, `?` or `=`.
def define_callbacks(*names)
options = names.extract_options!
names.each do |name|
- class_attribute "_#{name}_callbacks", instance_writer: false
+ class_attribute "_#{name}_callbacks"
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
__run_callbacks__(_#{name}_callbacks, &block)
@@ -781,15 +753,39 @@
end
end
protected
- def get_callbacks(name)
+ def get_callbacks(name) # :nodoc:
send "_#{name}_callbacks"
end
- def set_callbacks(name, callbacks)
+ def set_callbacks(name, callbacks) # :nodoc:
send "_#{name}_callbacks=", callbacks
+ end
+
+ def deprecated_false_terminator # :nodoc:
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result = result_lambda.call if result_lambda.is_a?(Proc)
+ if Callbacks.halt_and_display_warning_on_return_false && result == false
+ display_deprecation_warning_for_false_terminator
+ else
+ terminate = false
+ end
+ end
+ terminate
+ end
+ end
+
+ private
+
+ def display_deprecation_warning_for_false_terminator
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails.
+ To explicitly halt the callback chain, please use `throw :abort` instead.
+ MSG
end
end
end
end