Cyril Duchon-Doris Cyril Duchon-Doris - 4 months ago 19
Ruby Question

Register a different type of callback in ApplicationController

I need more fine grain control on the order of callbacks in my controllers. Currently Rails only let you use

append|prepend_before|after_action
, but this is just extremely bad if you want to add a module with its dedicated callbacks.

I am trying to understand how AbstractController::Callbacks work, and I'm trying to register a new type of callback, that would be executed at a specific moment, taking advantage of Rail's controllers syntax for adding a callback (only/except + list of actions, etc.).

You can think of it as a custom Access Control feature, but this question isn't about access control, please refrain answerbombing with gems like Cancan.

class ApplicationController
include xxx
include MyModuleWithCallbacks
include yyy
...
end

class MyController < ApplicationController
prepend_before_action :something_before_my_callbacks
my_callback_list :custom_callback, only: [:index, :show]
before_action :something_after_my_callbacks
# Goal : the order of above callbacks should NOT matter, my_callback does not depend on ActionController process_action callback list
end

module MyModuleWithCallbacks
extend ActiveSupport::Concern
extend AbstractController::Callbacks

included do
around_action :perform_if_condition

def perform_if_condition
run_callbacks :my_callback_list do
if my_callbacks_went_good?
yield # And run the controller's before_callbacks
else
# log, render error page, etc.
end
end
end

# This is the hard part register the callback, I tried
class_methods do
define_method :my_callback_list do |*names, &blk|
_insert_callbacks(names, blk) do |name, options|
set_callback(:my_callback_list, :before, name, options)
end
end
end


The current error is


undefined method `_my_callbacks_list_callbacks' for PublicController:Class


I am taking my inspiration from the source code of AbstractController::Callbacks but I'm not sure I understand what's going on there ^^"

Answer

I saw some upvotes so here is my current solution :

With the example of a very lightweight Access control method, the original name of my_callback was access_control

# controllers/concerns/access_control.rb
    module AccessControl
      extend ActiveSupport::Concern
      class_methods do
        define_method :my_callback do |*names, &blk|
          _insert_callbacks(names, blk) do |name, options|
            set_callback(:my_callback, :before, name, options)
          end
        end
      end

      included do

        define_callbacks :my_callback

        def perform_if_access_granted
          run_callbacks :my_callback do
            if @access_denied and not @access_authorized and not god_mode?
              @request_authentication = true unless user_signed_in?
              render(
                file: File.join(Rails.root, 'app/views/errors/403.html'),
                status: 403,
                layout: 'error')
            else
              yield
            end
          end
        end

Then in your other controllers that include the module (again with Access control example)

# controllers/your_controller.rb
class YourController < SeekerController
  your_callback do
    set_entity
    allow_access_to_entity(@entity)
  end