Rails 7.1 resolve conflicting Zeitwerk Inflection rules

90 views Asked by At

I have a Rails 7.1 app and multiple (Rails Engine) Gems with conflicting inflection rules. There is Gem1::Api::Gem1Controller under app/controllers/gem1/api/gem1_controller.rb and Gem2::API::Gem2Controller under app/controllers/gem2/api/gem2_controller.rb. As I understand it, Zeitwerk uses global autoloaders for the whole Rails app including all Rails Engines (which are more or less treated as part of the main app).

By default, Zeitwerk can load Gem1::Api::Gem1Controller, but fails to load Gem2::API::Gem2Controller, because the inflection rule for "api" => "API" is missing.

However, using the "normal" approach of adding custom inflection rules will not work, since then it will fail to load Gem1::Api::Gem1Controller because it expects to find Gem1::API::Gem1Controller.

# config/initializers/zeitwerk.rb
Rails.application.autoloaders.each do |autoloader|
  autoloader.inflector.inflect("api" => "API") # works for Gem2, but breaks Gem1 as a result
end

Is there any way to define inflection rules that include the whole namespace? Something like

# config/initializers/zeitwerk.rb
Rails.application.autoloaders.each do |autoloader|
  autoloader.inflector.inflect("gem2/api/gem2_controller" => "Gem2::API::Gem2Controller")
end

Or maybe there is a way to define a per-Gem or per-Rails-Engine inflector? Keep in mind it still needs to work as a Rails Engine.

1

There are 1 answers

0
Brauser On BEST ANSWER

I found a solution for this problem via a custom Inflector. The regular Rails::Autoloader::Inflector#camelize ignores its second argument, but incorporating the absolute filepath into the camelization enables me to use both Gem1::Api and Gem2::API.

# config/initializers/autoloaders.rb
module PathnameSuffixInflector
  @overrides = {}
  @max_overrides_depth = 0

  def self.camelize(basename, abspath)
    return @overrides[[basename]] || basename.camelize if @max_overrides_depth <= 1

    filenames = Pathname.new(abspath).each_filename.to_a[0..-2] + [basename]
    @max_overrides_depth.downto(1).each do |suffix_length|
      suffix = filenames.last(suffix_length)
      return @overrides[suffix] if @overrides.key?(suffix)
    end

    return basename.camelize
  end

  def self.inflect(overrides)
    @overrides.merge!(overrides.transform_keys { Pathname.new(_1).each_filename.to_a })
    @max_overrides_depth = @overrides.keys.map(&:length).max || 0
  end
end
PathnameSuffixInflector.inflect("gem2/api" => "API")

Rails.application.autoloaders.each do |autoloader|
  autoloader.inflector = PathnameSuffixInflector
end