Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails' autoloading/constant resolution is creating ghost modules

Here's a brand new Rails 5.1.4 app, with a model and a couple of routes and controllers.

A namespaced controller is referencing a top level model:

class AdminArea::WelcomeController < ApplicationController
  def index
    @user = User.new(name: 'Sergio')
  end
end

So far so good. You can check out the master, navigate to http://localhost:3000/admin_area/welcome and see it work.

BUT if we were to add an empty directory app/presenters/admin_area/user/ *, then things get weird. All of a sudden, User in that controller is not my model, but a non-existing module!

NoMethodError (undefined method `new' for AdminArea::User:Module):

app/controllers/admin_area/welcome_controller.rb:3:in `index'

Naturally, this module doesn't have any [non-built-in] methods and can't be pinned to a source file on disk.

Question: why adding an empty directory causes rails to mysteriously conjure a module out of thin air instead of correctly resolving name User to my model?


* actually, if you check out that branch as-is, you'll get a different error.

NameError (uninitialized constant AdminArea::WelcomeController::User)

because git wouldn't let me commit an empty directory, so I added a .keep file in there. But as soon as you delete that file, you get the behaviour described above.

like image 329
Sergio Tulentsev Avatar asked Oct 18 '17 13:10

Sergio Tulentsev


1 Answers

This a consequence of ruby constant lookup and how Rails resolves autoloading.

The constant User in the controller is so called "relative reference", which means it should be resolved relative to the namespace within which it occurs. For this constant, there are three possible variants where the constant can be defined:

AdminArea::WelcomeController::User
AdminArea::User
User

Rails autoloading maps these constants into file names and iterates over the autoload_paths in order to find the file where the constant is defined. E.g.:

app/assets/admin_area/welcome_controller/user.rb
app/assets/admin_area/welcome_controller/user
app/channels/admin_area/welcome_controller/user.rb
...
app/assets/admin_area/user.rb
app/assets/admin_area/user
...
app/assets/user.rb
...
app/models/user.rb #=> here it is!

When you add the admin_area/user folder into the presenters directory, you are effectively defining such a constant. Modules in Rails are automagically created, so that you don't actually need to create files where you define these modules that only work as namespaces.

When you added the folder, the folder appeared in the Rails lookup:

...
app/assets/admin_area/user.rb
app/assets/admin_area/user
...
app/presenters/admin_area/user #=> Here Rails finds the folder

and Rails resolves the User to reference to that module.

However this is quite easy to fix, If you want the User constant that is used within AdminArea namespace to reference a top-level constant (and not the AdminArea::User module), you should change the "relative reference" into an absolute reference by preceding the constant with ::.

@user = ::User.new(name: 'Sergio')
like image 193
Laura Paakkinen Avatar answered Oct 18 '22 10:10

Laura Paakkinen