Search code examples
ruby-on-railsrubyinitializationvalue-objects

Prevent "initialization autoloaded the constant" for value objects in initializer


In a Rails initializer, I assign additional application configuration as follows:

module MyApp
  class Application

    config.x.email = {
      default: '[email protected]',
      invoice: '[email protected]'
    }

  end
end

Recently, however, I had to introduce a value object class for those email addresses to encapsulate some logic needed to properly align with SPF/DKIM/DMARC:

module MyApp
  class Application

    config.x.email = {
      default: EmailAddress.new('[email protected]'),
      invoice: EmailAddress.new('[email protected]')
    }

  end
end

On Rails 6.1, this triggers a warning:

DEPRECATION WARNING: Initialization autoloaded the constant EmailAddress.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload EmailAddress, for example,
the expected changes won’t be reflected in that stale Class object.

This autoloaded constant has been unloaded.

In order to autoload safely at boot time, please wrap your code in a reloader
callback this way:

    Rails.application.reloader.to_prepare do
      # Autoload classes and modules needed at boot time here.
    end

That block runs when the application boots, and every time there is a reload.
For historical reasons, it may run twice, so it has to be idempotent.

Check the "Autoloading and Reloading Constants" guide to learn more about how
Rails autoloads and reloads.

I'm not sure how to follow this advice in this particular spot.

As a workaround, adding require "email_address" to the top of the initializer gets rid of the warning, but it's not really solving the issue at its core.

Any ideas how to tackle this?

Thanks for your help!


Solution

  • You have to wrap the whole config part in to_prepare block:

    # config/initializers/email_config.rb
    
    Rails.application.reloader.to_prepare do
      Rails.application.config.x.email = {
        default: EmailAddress.new("[email protected]"),
        invoice: EmailAddress.new("[email protected]")
      }
    end
    

    You can also put your email_address.rb in lib directory and just require it, once you figure out all the behaivor for EmailAddress you won't really need it to be reloadable. Restart will be required if you make any changes to EmailAddress in this case:

    # config/initializers/email_config.rb
    
    require "email_address" # => loads lib/email_address.rb
    
    Rails.application.config.x.email = {
      default: EmailAddress.new("[email protected]"),
      invoice: EmailAddress.new("[email protected]")
    }
    

    Another option is to have email_address.rb in autoload_once_paths, which is the same as requiring it but without the require:

    # config/application.rb
    
    config.autoload_once_paths << Rails.root.join("app/value_objects")
    
    # config/initializers/email_config.rb
    
    # EmailAddress will be loaded from app/value_objects/email_address.rb only once
    
    Rails.application.config.x.email = {
      default: EmailAddress.new("[email protected]"),
      invoice: EmailAddress.new("[email protected]")
    }
    

    https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-when-the-application-boots