7

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.

Luke Peterson
  • 8,584
  • 8
  • 45
  • 46
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • Possible duplicate of [Rails 4: organize rails models in sub path without namespacing models?](https://stackoverflow.com/questions/18934115/rails-4-organize-rails-models-in-sub-path-without-namespacing-models) – jon1467 Oct 18 '17 at 14:01
  • @jon1467: no, not a duplicate of that. Unless I'm missing something. – Sergio Tulentsev Oct 18 '17 at 14:08
  • Ah sorry, I think I misread your question as asking how to place your user model in the directory `presenters`. My bad. – jon1467 Oct 18 '17 at 14:42
  • I think [this issue](https://github.com/rails/rails/issues/3841#issuecomment-3002705) could describe your problem, no indication if this was ever fixed/changed. – jon1467 Oct 18 '17 at 14:47
  • @jon1467: closer, but still no. `Module.nesting` does not affect things here. In the sample app it is `AdminArea::WelcomeController`, but in my actual app, it's a proper nesting (`module AdminArea; class WelcomeController`). Both exhibit the same behavior. – Sergio Tulentsev Oct 18 '17 at 15:03

1 Answers1

5

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')
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
Laura Paakkinen
  • 1,661
  • 14
  • 22
  • 1
    The only missing piece, I think, is link to the code that automagically creates modules. – Sergio Tulentsev Dec 20 '17 at 19:50
  • 1
    There you go http://guides.rubyonrails.org/autoloading_and_reloading_constants.html#automatic-modules :) "If autoload_paths has a file called admin.rb Rails is going to load that one, but if there's no such file and a directory called admin is found, Rails creates an empty module and assigns it to the Admin constant on the fly." – Laura Paakkinen Dec 20 '17 at 20:59
  • 1
    That's it, thanks! I'll award the bounty as soon as it lets me :) – Sergio Tulentsev Dec 20 '17 at 21:07