Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 6: Zeitwerk::NameError doesn't load class from module

I have a file which looks like this

#app/services/account/authenticate/base.rb
module Account
  module Authenticate
    AuthenticateError = Class.new(StandardError)

    class Base < ::Account::Base
      def self.call(*attrs)
        raise NotImplementedError
      end
    end
  end
end

Now when I will run the code from rails c I have an error

> ::Account::Authenticate::AuthenticateError
=> NameError (uninitialized constant Account::Authenticate::AuthenticateError)
> ::Account::Authenticate.constants
=> [:Base, :ViaToken]

So rails doesn't see AuthenticateError class. But when I will create a nested class from this folder like

=> Account::Authenticate::ViaToken
> ::Account::Authenticate.constants
=> [:Base, :AuthenticateError, :ViaToken]

AuthenticateError class is now visible

> ::Account::Authenticate::AuthenticateError
=> Account::Authenticate::AuthenticateError

The solution for this problem is to create a separate file authenticate_error.rb which will work from the beginning but this solution is not ideal for me. Is there any solution to preload all classes or smth?

(Ruby 2.6 with Rails 6.0.0.rc2)

like image 637
updater Avatar asked Jul 30 '19 17:07

updater


3 Answers

I experienced this same issue when deploying a Rails 6.0.2 application to an Ubuntu 18.04 server.

Unable to load application: Zeitwerk::NameError: expected file /home/deploy/myapp/app/models/concerns/designation.rb to define constant Designation, but didn't

I found out that the issue was with zeitwerk. Zeitwerk is the new code loader engine used in Rails 6. It’s meant to be the new default for all Rails 6+ projects replacing the old classic engine. Zeitwerk provides the features of code autoloading, eager loading, and reloading.

Here's how I solved it:

Navigate to the config/application.rb file on your project.

Add this line within your application module to switch to classic mode for autoloading:

config.autoloader = :classic

Here's an example:

module MyApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0

    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration can go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded after loading
    # the framework and any gems in your application.

    config.autoloader = :classic
  end
end

You can read up more on zeitwerk in this article: Understanding Zeitwerk in Rails 6

That's all.

I hope this helps

like image 150
Promise Preston Avatar answered Oct 30 '22 18:10

Promise Preston


updating a Rails app from 5.2 to 6.0 and also hit the Zeitwerk bump!

If you want to continue to use the autoloading mode you're currently using, avoiding Zeitwerk, then add this line to your application.rb file (@PromisePreston answer and Rails doc)

config.autoloader = :classic

If you want to upgrade to Zeitwerk then a command to use is bin/rails zeitwerk:check (From this guide article).

Scenario we hit closest to this particular question was where we had a file within a subfolder like this:

#presenters/submission_files/base.rb
module Presenters
  module SubmissionFiles
    class Base < Showtime::Presenter
      def method_call
        #code_here
      end
    end
  end
end

Removing an extra modules to have:

 #presenters/submission_files/base.rb
 module Presenters
   class SubmissionFiles::Base < Showtime::Presenter
      def method_call
        #code_here
      end
   end
 end

Then, when calling the method in other ruby files in the app use: Presenters::SubmissionFiles::Base.method_call

like image 4
Jeff Spicoli Avatar answered Oct 30 '22 18:10

Jeff Spicoli


zeitwerk follow conventional file structure which is able to load your project's classes and modules on demand (autoloading) as long as you follow it's rule.

# service/account/authenticate/base.rb
module Account
  module Authenticate
    puts "load service ....."
    AuthenticateError = Class.new(StandardError)

    class Base
    end
  end
end

::Account::Authenticate::AuthenticateError # uninitialized constant
::Account::Authenticate::Base # load service ....
::Account::Authenticate::AuthenticateError # OK

as you can see, the first time you attempt to reach the constant AuthenticateError, the log load service ... does not show, that because you don't play zeitwerk rule:

  • whenever it gets a request to load the const ::Account::Authenticate::AuthenticateError, first it'll check and return if that constant already loaded, otherwise it'll look for the file /account/authenticate/authenticate_error.rb which corresponding to the constant ::Account::Authenticate::AuthenticateError to find that constant definition, but it could not find it.

  • on step 2, when you call ::Account::Authenticate::Base, it could find the file /account/authenticate/base.rb and load it, during this time, it's also load the constant AuthenticateError which is defined on that file, now we have the constant ::Account::Authenticate::AuthenticateError, and of course, it's OK on step 3.

now let try to play with the zeitwerk rule, i create a file /account/authenticate/authenticate_error.rb as below

# service/account/authenticate/authenticate_error.rb
module Account
  module Authenticate
    puts "load error ....."
    AuthenticateError = Class.new(StandardError)
  end
end

and try to attempt that constant at the step 1

$ spring stop
$ rails c
> ::Account::Authenticate::AuthenticateError
load error .....
=> Account::Authenticate::AuthenticateError

it worked since zeitwerk found the file account/authenticate/authenticate_error.rb. (note that the file name /____authenticate_error.rb still work)

my thought: i think you could work safely with the constant AuthenticateError inside the module ::Account::Authenticate, in case of you want to expose those error constants to outside, you could create file /account/authenticate/error.rb

# service/account/authenticate/error.rb
module Account
  module Authenticate
    module Error
     AuthenticateError = Class.new(StandardError)
    end
  end
end

then you could access ::Account::Authenticate::Error::AuthenticateError, in my opinion, it even clearer than put AuthenticateError inside base.rb.

like image 2
Lam Phan Avatar answered Oct 30 '22 19:10

Lam Phan