1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/actionpack/lib/abstract_controller/callbacks.rb
Jess Bees 60f7d49033 Raise error when callback's only/unless symbols aren't methods
When `before_action :callback, only: :action_name` is declared on a
controller that doesn't respond to `action_name`, raise an exception
at request time. This is a safety measure to ensure that typos or
forgetfulness don't prevent a crucial callback from being run when it
should.

Include names of filters for more useful error messages

The error message of the raised exception will be more useful if it
indicates the names of the callbacks that have the missing conditinoal
action.

The way the callbacks get shoehorned into `_normalize_callback_options`
options parameter is a little awkward, but done this way to avoid
changing the method's signature, since it is a publicly documented
method.
2021-12-14 15:22:08 -05:00

253 lines
8.8 KiB
Ruby

# frozen_string_literal: true
module AbstractController
# = Abstract Controller Callbacks
#
# Abstract Controller provides hooks during the life cycle of a controller action.
# Callbacks allow you to trigger logic during this cycle. Available callbacks are:
#
# * <tt>after_action</tt>
# * <tt>append_after_action</tt>
# * <tt>append_around_action</tt>
# * <tt>append_before_action</tt>
# * <tt>around_action</tt>
# * <tt>before_action</tt>
# * <tt>prepend_after_action</tt>
# * <tt>prepend_around_action</tt>
# * <tt>prepend_before_action</tt>
# * <tt>skip_after_action</tt>
# * <tt>skip_around_action</tt>
# * <tt>skip_before_action</tt>
#
# NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
#
module Callbacks
extend ActiveSupport::Concern
# Uses ActiveSupport::Callbacks as the base functionality. For
# more details on the whole callback system, read the documentation
# for ActiveSupport::Callbacks.
include ActiveSupport::Callbacks
included do
define_callbacks :process_action,
terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
skip_after_callbacks_if_terminated: true
mattr_accessor :raise_on_missing_callback_actions, default: false
end
class ActionFilter # :nodoc:
def initialize(filters, conditional_key, actions)
@filters = filters.to_a
@conditional_key = conditional_key
@actions = Array(actions).map(&:to_s).to_set
end
def match?(controller)
if controller.raise_on_missing_callback_actions
missing_action = @actions.find { |action| !controller.available_action?(action) }
if missing_action
filter_names = @filters.length == 1 ? @filters.first.inspect : @filters.inspect
message = "The #{missing_action} action could not be found for the #{filter_names} callback on #{controller.class.name}, but it is listed in its #{@conditional_key.inspect} option"
raise ActionNotFound.new(message, controller, missing_action)
end
end
@actions.include?(controller.action_name)
end
alias after match?
alias before match?
alias around match?
end
module ClassMethods
# If +:only+ or +:except+ are used, convert the options into the
# +:if+ and +:unless+ options of ActiveSupport::Callbacks.
#
# The basic idea is that <tt>:only => :index</tt> gets converted to
# <tt>:if => proc {|c| c.action_name == "index" }</tt>.
#
# Note that <tt>:only</tt> has priority over <tt>:if</tt> in case they
# are used together.
#
# only: :index, if: -> { true } # the :if option will be ignored.
#
# Note that <tt>:if</tt> has priority over <tt>:except</tt> in case they
# are used together.
#
# except: :index, if: -> { true } # the :except option will be ignored.
#
# ==== Options
# * <tt>only</tt> - The callback should be run only for this action.
# * <tt>except</tt> - The callback should be run for all actions except this action.
def _normalize_callback_options(options)
_normalize_callback_option(options, :only, :if)
_normalize_callback_option(options, :except, :unless)
end
def _normalize_callback_option(options, from, to) # :nodoc:
if from_value = options.delete(from)
filters = options[:filters]
from_value = ActionFilter.new(filters, from, from_value)
options[to] = Array(options[to]).unshift(from_value)
end
end
# Take callback names and an optional callback proc, normalize them,
# then call the block with each callback. This allows us to abstract
# the normalization across several methods that use it.
#
# ==== Parameters
# * <tt>callbacks</tt> - An array of callbacks, with an optional
# options hash as the last parameter.
# * <tt>block</tt> - A proc that should be added to the callbacks.
#
# ==== Block Parameters
# * <tt>name</tt> - The callback to be added.
# * <tt>options</tt> - A hash of options to be used when adding the callback.
def _insert_callbacks(callbacks, block = nil)
options = callbacks.extract_options!
callbacks.push(block) if block
options[:filters] = callbacks
_normalize_callback_options(options)
options.delete(:filters)
callbacks.each do |callback|
yield callback, options
end
end
##
# :method: before_action
#
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: prepend_before_action
#
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: skip_before_action
#
# :call-seq: skip_before_action(names)
#
# Skip a callback before actions. See _insert_callbacks for parameter details.
##
# :method: append_before_action
#
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
#
# If the callback renders or redirects, the action will not run. If there
# are additional callbacks scheduled to run after that callback, they are
# also cancelled.
##
# :method: after_action
#
# :call-seq: after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
##
# :method: prepend_after_action
#
# :call-seq: prepend_after_action(names, block)
#
# Prepend a callback after actions. See _insert_callbacks for parameter details.
##
# :method: skip_after_action
#
# :call-seq: skip_after_action(names)
#
# Skip a callback after actions. See _insert_callbacks for parameter details.
##
# :method: append_after_action
#
# :call-seq: append_after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
##
# :method: around_action
#
# :call-seq: around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
##
# :method: prepend_around_action
#
# :call-seq: prepend_around_action(names, block)
#
# Prepend a callback around actions. See _insert_callbacks for parameter details.
##
# :method: skip_around_action
#
# :call-seq: skip_around_action(names)
#
# Skip a callback around actions. See _insert_callbacks for parameter details.
##
# :method: append_around_action
#
# :call-seq: append_around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
# set up before_action, prepend_before_action, skip_before_action, etc.
# for each of before, after, and around.
[:before, :after, :around].each do |callback|
define_method "#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, callback, name, options)
end
end
define_method "prepend_#{callback}_action" do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, callback, name, options.merge(prepend: true))
end
end
# Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
define_method "skip_#{callback}_action" do |*names|
_insert_callbacks(names) do |name, options|
skip_callback(:process_action, callback, name, options)
end
end
# *_action is the same as append_*_action
alias_method :"append_#{callback}_action", :"#{callback}_action"
end
end
private
# Override <tt>AbstractController::Base#process_action</tt> to run the
# <tt>process_action</tt> callbacks around the normal behavior.
def process_action(*)
run_callbacks(:process_action) do
super
end
end
end
end