uninitialized constant in My::Engine after file change - ruby-on-rails

I'm developing a gem/engine. The way I do this is by bundling it in a test RailsApp from source:
# Gemfile
gem 'my-engine', path: '../local/path/to/gem'
This works fine so far.
But, after I change a file in my gem (add a space or a break for example) the Engine is unloaded. Causing the following error:
uninitialized constant My::Engine
This error is thrown by the file that does the first call to My::Engine. ( I need to call that to get the root: My::Engine.root ) If I delete that line, there are no error thrown, but just an empty page is rendered, and this is happening because all my SQL's change and no content is loaded from the database. I think this is because the files in the lib dir are unloaded, because in these files I dynamically create active-record models..
I already checked out the autoload_paths and watchable_dirs:
# engine.rb
module My
class Engine < Rails::Engine
engine_name 'my-engine'
initializer "my-engine.load_config" do |app|
app.config.autoload_paths += %W(#{Engine.root}/lib)
app.config.watchable_dirs["#{Engine.root}/lib"] = [:rb]
end
end
end
I'm not sure if I'm implementing these the right way, but they don't seem to solve my problems the way I'm using them.

I think you may need to require 'my/engine' before calling My::Engine.root, or change the order of your requires so that 'my/engine' is required prior to the file that makes a call to My::Engine.

Related

Why is Rails/Zeitwerk expecting `burgerfrenchfry_coupon.rb` to define the constant `BurgerFrenchfryCoupon`?

I'm working on upgrading a large legacy Rails application from 5.2 to 6.0. In the process of fixing the autoload paths, I've run into an issue where Rails/Zeitwerk seem to be breaking their own rules about how the names of constants are defined in relation to their filenames. I can't share actual code from this application, but the situation is essentially this:
In config/application.rb:
config.autoload_paths << "#{config.root}/app/models/coupons"
In app/models/coupons/burgerfrenchfry_coupon.rb:
class BurgerfrenchfryCoupon << ApplicationRecord
end
When another class in the application references the BurgerfrenchfryCoupon class, a NameError is thrown with BurgerFrenchfryCoupon as a suggested classname (that class does not exist in the application). When I require the app/models/coupons/burgerfrenchfry_coupon path directly in the file referencing BurgerfrenchfryCoupon I get a Zeitwerk error: Zeitwerk::NameError: expected file /redacted/app/models/coupons/burgerfrenchfry_coupon.rb to define constant BurgerFrenchfryCoupon, but didn't
I've done a thorough search of the application to find anywhere where the expectation could have been customized and I've come up with nothing. Does anyone have any ideas about the follow:
Why this is happening?
Where or how an override on the constant name expectation might have been made?
How I can configure Rails to recognize that this constant should be defined in this file without changing all of the references to it in the application to BurgerFrenchfryCoupon?
The problem is that, for some reason, the autoloader's inflector is configured to camelize "burgerfrenchfry_coupon" as "BurgerFrenchfryCoupon". If using Active Support inflections (the default), there's some custom inflection rule somewhere that affects this.
You can fix this particular one without affecting the rest of the application by overriding this way:
# config/initializers/autoloading.rb
inflector = Rails.autoloaders.main.inflector
inflector.inflect("burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon")
That sets a special mapping in the autoloader's inflector that ignores everything else.
The answer here ended up being a custom inflection that had been added to activesupport.
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'BurgerFrenchfry'
end
As this inflection was necessary for other parts of the application,
to fix the Zeitwerk error I added the file config/initializers/zeitwerk.rb with the following content:
Rails.autoloaders.each do |autoloader|
autoloader.inflector.inflect(
"burgerfrenchfry_coupon" => "BurgerfrenchfryCoupon"
)
end
Which overrides the inflection for this one file

Accessing modules in Rails lib folder

There are probably hundreds of these questions on here but I haven't been able to get one to work yet. I'm using Rails 6 and I always have trouble writing custom modules.
I have a file for creating email tokens: lib/account_management/email_token.rb
module AccountManagement
module EmailToken
def create_email_token(user)
....
end
end
end
Then in my controller I have tried all kinds of things:
require 'account_management/email_token'
include AccountManagement::EmailToken
Calling it with: create_and_send_email_token(user)
At one point I added config.eager_load_paths << Rails.root.join('lib/account_management/') but I didn't think you still had to do that.
Whenever I try to call the controller action I get one of a few error messages:
*** NameError Exception: uninitialized constant Accounts::SessionsController::EmailToken
#this happens if I try to manually send through the rails console
(although inside of a byebug I can call require 'account_management/email_token' and it returns
true.
Most commonly I get:
NoMethodError (undefined method `create_email_token' for #<Accounts::SessionsController:0x00007ffe8dea21d8>
Did you mean? create_email):
# this is the name of the controller method and is unrleated.
The simplest way to solve this is by placing your files in app/lib/account_management/email_token.rb. Rails already autoloads any subdirectory of the app folder*.
/lib has not been on the autoload paths since Rails 3. If you want to add it to the autoload paths you need to add /lib not /lib/account_management to the autoload/eager loading paths. In Zeitwerk terms this adds a root where Zeitwerk will index and resolve constants from.
config.autoload_paths += config.root.join('lib')
config.eager_load_paths += config.root.join('lib')
Note that eager_load_paths is only used when eager loading is turned on. Its turned off by default in development to enable code reloading.
/lib is added to $LOAD_PATHso you can also manually require the file with:
require 'account_management/email_token'
See:
Autoloading and Reloading Constants (Zeitwerk Mode)
Rails #37835

How can I create a custom exception in my Rails application?

I am trying to create custom exceptions in rails, but I 've problem with my designed solution.
Here what I've done so far:
-Create in the app/ folder a folder named errors/ with a file exceptions.rb in it.
app/errors/exceptions.rb:
module Exceptions
class AppError < StandardError; end
end
In one of my controllers, tried to raise it:
raise Exceptions::AppError.new("User is not authorized")
But when I call the controller's action, here is what I get:
NameError (uninitialized constant Exceptions::AppError
Did you mean? TypeError
KeyError
IOError
EOFError
Did you mean? TypeError
KeyError
IOError
EOFError
):
I think I don't have fully understood how to create new directories and files, and use them.
I've read that everything created in the app dir, is eager loaded, so I can't understand where is the problem.
Short version: this is about rails' automated code loading - the fact that in this case the files contain exceptions doesn't matter (see guide on the subject for more details)
Rails would try to load this from exceptions/app_error.rb, in any of the files that are on its auto load path. Because you file naming doesn't match this, it can't find the definition and you get a NameError.
If you don't care about code reloading (and you might not for this sort of content) then you can keep the files as they are but require them in an initialiser (ensure that app/errors is in the load path):
require 'exceptions'
If not then you'll have to rearrange your files to match. If you add app/errors to rails' autoload path and keep the files as is, then it should work. If you don't want to change the autoload path then you'd have to mode it to somewhere in the autoload path and ensure the nesting of modules reflects the organization on disk.
Personally I'd probably stick these in lib and require them with an initialiser

Ruby on Rails. Few classes in the same file. "Uninitialized constant" error

I've defined a few Exception classes in my app/models/core/exceptions.rb file:
class Core::Exception < Exception end
class Core::UserNotFoundException < Core::Exception end
...
Then added /config/initializers/require.rb file so Rails can find classes with names which don't meet file names:
require "#{Rails.root}/app/models/core/exceptions.rb"
When I start the app (development mode), everything works fine until I change anything to any .rb file. Then when I refresh browser page I get error "Uninitialzed constant Core::Exception". So every time I make any modification to the source code (except the views) I have to restart 'rails server'.
Any idea on why when I refresh a page my 'require' are no longer loaded? How to fix this?
Creating a core.rb file should help.
Just place the Core module definition there
# /app/models/core.rb
module Core
end
I believe the problem is the Core module. When you hit
class Core::Exception ...
rails needs to find the module or class named Core. Rails will create a module called Core automatically if it finds files with paths off the form 'core/foo.rb' in its autoload path (see the algorithm pseudo code in the guide on autoloading).
Because this Core module is autoloaded, code reloading will trigger its removal. A new Core module may be created, but this will no longer have the classes you had added to it.
One option would be to not fight Rails's code organisation and put those classes in individual files.
An alternative may be to change how you create those classes to
module Core
class Exception < ::Exception; end
end
Which means that the Core module won't be autoloaded and thus shouldn't be eligible for removal.

Rails unable to autoload constant from file despite being defined in that file

This is a tricky one to explain. I have a module in another module namespace like so:
# app/models/points/calculator.rb
module Points
module Calculator
def self.included(base)
base.send(:include, CommonMethods)
base.send(:include, "Points::Calculator::#{base}Methods".constantize)
end
end
end
So then in other classes all I need to do is:
class User
include Points::Calculator
end
I've specified this directory in application.rb to be autoloadable...(even though i think rails recurses through models...)
config.autoload_paths += Dir[ Rails.root.join('app', 'models', "points") ]
In development env, everything works fine. When running tests(and production env), I get the following error:
Unable to autoload constant Points::Calculator, expected /Users/pete/work/recognize/app/models/points/calculator.rb to define it (LoadError)
I actually followed the advice here to fix the problem: Stop Rails from unloading a module in development mode by explicitly requiring calculator.rb in application.rb.
However, why is this happening??
I stuck some debug output in ActiveSupport's dependencies.rb file and noticed that this file is being required twice. The first time its required I can see that the constant is indeed loaded.
But the 2nd time its required the constant has been unloaded as far as Rails can tell, but when the actual require is called, ruby returns false because ruby knows its already required it. Then Rails throws the "unable to autoload constant" error because the constant still isn't present and ruby didn't "re-require" the file.
Can anyone shed light on why this might be happening?
Rails augments the constant lookup mechanism of ruby.
Constant lookup in Ruby:
Similar to method missing, a Module#constant-missing is invoked when a reference to a constant fails to be resolved. When we refer to a constant in a given lexical scope, that constant is searched for in:
Each entry in Module.nesting
Each entry in Module.nesting.first.ancestors
Each entry in Object.ancestors if Module.nesting.first is nil or a module.
When we refer to a constant, Ruby first attempts to find it according to this built-in lookup rules.
When ruby fails to find... rails kicks in, and using its own lookup convention and its knowledge about which constants have already been loaded (by ruby), Rails overrides Module#const_missing to load missing constants without the need for explicit require calls by the programmer.
Its own lookup convention?
Contrasting Ruby’s autoload (which requires the location of each autoloaded constant to be specified in advance) rails following a convention that maps constants to file names.
Points::Calculator # =>points/calculator.rb
Now for the constant Points::Calculator, rails searches this file path (ie 'points/calculator.rb') within the autoload paths, defined by the autoload_paths configuration.
In this case, rails searched for file path points/calculator in its autoloaded paths, but fails to find file and hence this error/warning is shown.
This answer is an abstract from this Urbanautomation blog.
Edit:
I wrote a blog about Zeitwerk, the new code reloader in Rails. Check it out at -> https://blog.bigbinary.com/2019/10/08/rails-6-introduces-new-code-loader-called-zeitwerk.html
If someone is having this issue in rails 6 which has zeitwerk autoloader,
Change ruby constant lookup back to classic in your application.rb
# config/application.rb
#...
config.autoloader = :classic
#...
Read more details here Rails Official Guides
Calculator should be a class to be autoloaded correctly
module Points
class Calculator
...
end
end

Resources