I have a concern that creates a class macro that I want available for all the models in my Rails application. So I'm including it in ApplicationRecord. The code is as follows:
# application_record.rb
class ApplicationRecord < ActiveRecord::Base
include ::TestConcern
end
# app/concerns/test_concern.rb
module TestConcern
extend ActiveSupport::Concern
class_methods do
def some_class_macro_all_models_must_have
User.some_class_instance_variable << self
end
end
included do
User.include(UserModule)
end
module UserModule
def self.included(base)
base.class_eval do
def self.some_class_instance_variable
@some_class_instance_variable ||= Set.new
end
end
end
end
end
As you can see, the class macro will actually interact with a class instance variable in the model User.
So that's why, on the included hook of the concern, I'm trying to class_eval
the User model to have that class instance variable initialized. The plan was to do it like this because otherwise any model can be invoking the class macro BEFORE the class instance variable is initialized in the User model.
However, this errors out with Circular dependency detected while autoloading constant User
. As far as I can understand, ApplicationRecord
loads, it includes the module, the module included
hooks is called, it references the User
model, and so the User model is loaded, which inherits from ApplicationRecord (which didn't finish loading yet), so it causes the circular dependency.
How to avoid this circular dependency paradox, knowing that many models will invoke this class macro, and those classes might be loaded before the User class itself, so I can't even count on defining the some_class_instance_variable
class method in the User model itself?
After giving it some extra thought, I decided to simply store the some_class_instance_variable in the concern itself, and since the model
User
also called thesome_class_macro_all_models_must_have
, I decided to include theUserModule
when it was invoked, effectively eliminating both the circular dependency and the load order issue.The real code is much more complex than this contrived example, but the end result was something like this: