diff options
Diffstat (limited to 'activesupport/lib/active_support/callbacks.rb')
-rw-r--r-- | activesupport/lib/active_support/callbacks.rb | 241 |
1 files changed, 145 insertions, 96 deletions
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 06505bddf9..d2911a254c 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -71,24 +71,28 @@ module ActiveSupport # order. # # If the callback chain was halted, returns +false+. Otherwise returns the - # result of the block, or +true+ if no block is given. + # result of the block, +nil+ if no callbacks have been set, or +true+ + # if callbacks have been set but no block is given. # # run_callbacks :save do # save # end def run_callbacks(kind, &block) - cbs = send("_#{kind}_callbacks") - if cbs.empty? - yield if block_given? + send "_run_#{kind}_callbacks", &block + end + + private + + def _run_callbacks(callbacks, &block) + if callbacks.empty? + block.call if block else - runner = cbs.compile + runner = callbacks.compile e = Filters::Environment.new(self, false, nil, block) runner.call(e).value end end - private - # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. @@ -117,22 +121,22 @@ 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 @@ -144,13 +148,14 @@ module ActiveSupport 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 @@ -162,57 +167,59 @@ module ActiveSupport 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 @@ -220,122 +227,124 @@ 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 = run.call env env.value } + env else - next_callback.call env + run.call env 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 env else user_callback.call(target, value) { - env = next_callback.call env + env = run.call env env.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 = run.call env env.value } env else - next_callback.call env + run.call env 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 = run.call env env.value } env - } + end end private_class_method :simple end @@ -388,17 +397,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 @@ -415,15 +424,8 @@ module ActiveSupport # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Symbols:: Already methods. - # Strings:: class_eval'd into methods. - # Procs:: using define_method compiled into methods. - # Objects:: - # a method is created that calls the before_foo method - # on the object. + # All of these objects are converted into a lambda and handled + # the same after this point. def make_lambda(filter) case filter when Symbol @@ -470,6 +472,42 @@ 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 @@ -514,8 +552,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 @@ -562,7 +601,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 @@ -572,7 +611,7 @@ module ActiveSupport # # 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 } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This @@ -662,16 +701,17 @@ 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 @@ -722,12 +762,21 @@ 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! names.each do |name| 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) + end + RUBY end end |