Search code examples
ruby-on-railsmodulemodelautoloadzeitwerk

Looking for a more granular way to organize Rails models and understanding autoloader


In a test application (Rails 7.0, Ruby 3.2) I'm currently experimenting with a different structure of models than the default. The idea was to have something like this where all related code of a model lives together and each group uses same naming for support files:

app/
└── models/
    ├── accounts
    │   ├── account.rb
    │   └── support
    │       └── greetable.rb
    └── users
        ├── user.rb
        └── support
            └── greetable.rb

This was how I imagined the files of this setup:

# models/accounts/account.rb
class Account < ApplicationRecord
  include Greetable
end

# models/accounts/support/greetable.rb
module Greetable
  def greet
    "Hello, Account!"
  end
end

# models/users/user.rb
class User < ApplicationRecord
  include Greetable
end

# models/users/support/greetable.rb
module Greetable
  def greet
    "Hello, User!"
  end
end

Any idea how to define autoload config for this setup? Was hoping Zeitwerk does its magic and automatically finds the closest Greetable module but this is not how it works.

All thoughts/ideas/discussions around this topic are much appreciated.


Solution

  • You don't need to configure anything. You just have to understand how its intented to work.

    The way that Zeitwerk works is that it expects any files in subdirectories of an autoloading root (any subfolder or app and /app/models/concerns, /app/controllers/concerns) to be nested in a module (or class) with the a name according to the folder:

    # models/users/support/greetable.rb
    module Users
      module Support
        module Greetable
          def greet
            "Hello, User!"
          end
        end
      end
    end
    

    Zeitwerk will automatically create the implicit namespaces (the modules) so even if your file just contains:

    module Greetable
      def greet
        "Hello, User!"
      end
    end
    

    Zeitwerk will nest the constant as Users::Support::Greetable. I'm guessing this "didn't work" because you where expecting Greetable to be in the top level namespace.

    This is not just what Zeitwerk expects but also what other developers expect and is the accepted method of organizing your code as the encapsulation provided by module nesting prevents conflicts. This isn't actually something unique to Zeitwerk either - it really just codifies conventions that were used long before its invention.

    Plopping everything into the global namespace in Rails is something we do out of convenience while ignorning the warning bells that are going off in the back of our heads. Its far from an ideal way to organize code.

    While you could make this nested file constant a top level constant by using collapsing or adding additional autoloading roots from an organizational standpoint I would really question why you would want violate the principle of least suprise like this as it provides no real benefits.