Skip to content

Instantly share code, notes, and snippets.

@agibralter
Last active August 3, 2023 10:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save agibralter/8a3359ae1d791aee205ef79989ceb7a8 to your computer and use it in GitHub Desktop.
Save agibralter/8a3359ae1d791aee205ef79989ceb7a8 to your computer and use it in GitHub Desktop.
Trying to figure out how to properly `include` an `inherited` hook
module Hook
def self.included(base)
# base.extend ClassMethods
base.class_eval do
# def self.inherited(baseclass)
# # super
# puts "1 #{baseclass.to_s}"
# end
end
class << base
prepend ClassMethods
end
end
module ClassMethods
def inherited(baseclass)
super
puts "2 #{baseclass.to_s}"
super
end
end
end
class Foo
include Hook
def self.inherited(baseclass)
puts "FOO #{baseclass.to_s}"
end
end
class Bar
def self.inherited(baseclass)
puts "BAR #{baseclass.to_s}"
end
include Hook
end
class Guy1 < Foo
end
puts "---"
class Guy2 < Bar
end

I want to make it so that all of our activerecord models treat a database-calculated column/attribute as read-only.

I could use attr_readonly, but the issue with that is that it is only enforced on updates. You can still write the attribute when you create a row.

However, let's say I wanted to go that route. I would do something like this:

module DbTriggers
  module Railties
    module ActiveRecord

      def self.included(base)
        base.class_eval do
          attr_readonly :__auto_created_at
        end
      end
    end
  end
end

::ActiveRecord::Base.send :include, ::DbTriggers::Railties::ActiveRecord

Boom, done. (You can also use extend and self.extended).

Ok, let's say we want to handle the case of creates. Well, one of the ways we can do this is overwrite the writer/setter method for that column __auto_created_at=.

Easy enough, right?

module DbTriggers
  module Railties
    module ActiveRecord
      def __auto_created_at=(val)
        raise ::ActiveRecord::ActiveRecordError, '__auto_modified_at is read-only'
      end
    end
  end
end

::ActiveRecord::Base.send :include, ::DbTriggers::Railties::ActiveRecord

class MyModel < ::ActiveRecord::Base
end

# ActiveRecord::DangerousAttributeError:
# __auto_created_at= is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.

Wha wha whaaaaa?? Well, thank you, ActiveRecord... not really.

So, what happens here is this defines ActiveRecord::Base#__auto_created_at=. Then, when you get around to defining MyModel, Rails goes and inspects the database and creates attribute readers/writers for all the columns on the fly. As it's doing that, it tries to recreate __auto_created_at=. Turns out that ActiveRecord has a hook after method definition on its subclasses to check if you're overwriting a method that's already been defined by ActiveRecord. It does not like that.

So what are we to do? Maybe there are other options here, but one solution is to automatically define __auto_created_at= on our own subclasses.

module DbTriggers
  module Railties
    module ActiveRecord

      def self.included(base)
        class << base
          prepend ClassMethods
        end
      end

      module ClassMethods
        def inherited(base)
          super

          base.class_eval do
            def __auto_created_at=(value)
              raise ::ActiveRecord::ActiveRecordError, '__auto_created_at is read-only'
            end
          end
        end
      end
    end
  end
end

In other words: when this module is included in a base class, make it so that any sub class that inherits from that base class has this method, __auto_created_at= defined as an instance method on that sub class.

@Yoshyn
Copy link

Yoshyn commented Oct 4, 2018

So much thanks man.
I use it to inspect the callback chain of my controller.
Not finished yet but it's something that look like :

module DebugControllerCallbackChain
  def self.included(base)
    class << base
      prepend ClassMethods
    end
  end

  module ClassMethods
    def inherited(base)
      super
      base.class_eval do
        _process_action_callbacks.each_with_index do |callback, index|
          define_method(callback.filter) do |*args|
            if index == 0
              Rails.logger.debug("#{request.uuid} : #{request.path} called the action #{controller_name}##{action_name} | ssl?(#{request.ssl?})")
              callback_list = Lms::Admin::ReportingController._process_action_callbacks.map(&:filter).map(&:to_s)
              Rails.logger.debug("#{request.uuid} : The followings callback will be called : #{callback_list.join(', ')}")
              Rails.logger.debug("#{request.uuid} : params => #{params}")
              Rails.logger.debug("#{request.uuid} : session => #{session}")
            end

            Rails.logger.debug("#{request.uuid} : Callback CALLED => #{callback.filter} with options #{callback.options}")
            Rails.logger.debug("#{request.uuid} : Callback FROM => #{method(callback.filter).to_s}")

            super(*args)
          end if callback.kind == :before
        end
      end
    end
  end
end

class ApplicationController < ActionController::Base
  include DebugControllerCallbackChain
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment