Zeitwerk + Rails 6.1 - controller modules not autoloading - ruby-on-rails

I'm working with a legacy app with controllers where the basic CRUD actions are split into modules. The modules are within the app/controllers folder and each module is separately included in the controller class.
(Yes, it's because the modules are long. I know, fat controllers. I didn't write them. I'm going to be refactoring them, but first i just want to see if i can get the app to load under Zeitwerk correctly.)
app/
controllers/
posts_controller/
destroy.rb
edit_and_update.rb
index.rb
new_and_create.rb
show.rb
posts_controller.rb
The modules are included in the controller class:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
include Index
include Show
include NewAndCreate
include EditAndUpdate
include Destroy
The modules are like:
# app/controllers/posts_controller/index.rb
module PostsController::Index
When I run
rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!
However, when I interrupt my posts_controller_test via binding.break and check the controller class itself:
(rdbg) Post.const_get("PostsController")
eval error: uninitialized constant PostsController::Index
From what I understand, everything in app/controllers is autoloaded, and my filenames and classes correspond. So I don't believe this should be necessary, but if I add the app/controllers/posts_controller sub-folder to config.eager_load_paths, i get
eval error: expected file .../app/controllers/posts_controller/index.rb to define constant Index, but didn't
What am I doing wrong here? (other than having fat controllers!)

Related

Rails modules as strict namespaces

I'm quite new to rails and I'm a bit confused of how modules work here. I have a project structure like this:
# app/models/foo.rb
class Foo < ActiveRecord
# lib/external_service/foo.rb
module ExternalService
class Foo
# lib/external_service/bar.rb
module ExternalService
class Bar
attribute :foo, Foo # not the model
I have worked with many coding languages before and I expected it to be easily possible to use 'Foo' inside Bar and ExternalService just like that but
LoadError: Unable to autoload constant Foo, expected lib/external_service/foo.rb to define it
The ExternalService::Foo should normally not even be visible outside of ExternalService but the whole project dies on this thing
Am I just missing a kinda 'strict mode'-notation or anything to make sure that I obviously mean ExternalService::Foo inside the service and prevent the service from killing my model?
I know I can just prepend the module but i wanna keep the code readable.
so you are using rails 4
if you want to create a module, first you need to import or autoload your lib folder
for example in application.rb you can add lib folder to autoload:
config.autoload_paths << Rails.root.join('lib')
after that because you are using rails you should create a folder hierarchy with snake cased name of your module hierarchy
for example if you have:
module ExternalService
class Foo
...
end
end
your foo.rb file should be in a folder with name 'external_service'
{{project_root}}/lib/external_service/foo.rb
folder hierarchy is convention of rails.
Ruby behaves just like this and it's totally ok.
In this case the Foo-Model is already loaded, so ruby prefers this instead of the local one. Also alphabetically app/ is before lib/
A not so beautiful but quick fix is just to call it like this:
attribute :foo, ExternalService::Foo

Way to load folder as module constant in rails app directory

So have a rails 5 project and would like to load a directory like this
/app
/services
/user
foo.rb
as the constant ::Services::User::Foo
Does anyone have experience in getting rails autoload paths to load the constants in this manner?
foo.rb
module Services
module User
class Foo
end
end
end
SOLUTION
Add this to your application.rb file
config.autoload_paths << Rails.root.join('app')
See discussions here on autoloading
https://github.com/rails/rails/issues/14382#issuecomment-37763348
https://github.com/trailblazer/trailblazer/issues/89#issuecomment-149367035
Auto loading
You need to define Services::User::Foo inside app/services/services/user/foo.rb
If you don't want this weird subfolder duplication, you could also move services to app/models/services or lib/services.
You could also leave foo.rb in app/services/user/foo.rb, but it should define User::Foo.
Eager loading
If you don't need any magic with namespaces and class names, it is pretty straightforward :
Dir[Rails.root.join('app/services/**/*.rb')].each{|rb| require rb}
This will eagerly load any Ruby script inside app/services and any subfolder.

How to put methods definition into a single file and include it in rails application_controller?

There are a few method definitions in our rails 3.2 application_controller. Those methods are for all controllers. We would like to put those method definitions into a single file search_stats_actions.rb and include it in the application_controller. Into which subdir the file search_stats_actions.rb would be dropped and how the file should be included in application_controller? We are looking for preferred practice.
The file is in rails engine and not in rails app.
To me, this would be a module that would be put under the lib directory. Make sure that in your config/application.rb you've included the lib directory in the config.autoloads_path. See the Rails app configuration guide for details.
Then, in your ApplicationController, you can include the module you just created.
class ApplicationController
include <module name>
...
end
Better place this module (say MY_CUSTOM_MODULE) in the "lib" folders that's included in config.autoloads_path. You can then include the module in ApplicationController as follows
class ApplicationController
include <MY_CUSTOM_MODULE>
... excluded for brevity...
end

Organizing models/controllers and classes in rails in subfolders

I am working in a Rails project in which i have used the below names for model/controller and class files
/app/models/friends/friend.rb
/app/controllers/friends/friends_controller.rb
/lib/classes/friends/friend.rb
I tried to add all the models, controllers and class files in autoload path in application.rb.
But i am facing issues since the class names are same.
How should i handle this? and organize files in such a way that files are organized with name spaces.
Thanks,
Balan
A much better approach would be to use Rails Engines & divide your app in isolated modules.
rails plugin new friends --full --mountable --dummy-path spec/dummy
the above command will generate a full mountable engine with isolated namespace, meaning that all the controllers and models from this engine will be isolated within the namespace of the engine. For instance, the Post model later will be called Friends::Post, and not simply Post. to mount this app inside your main rails app, you need do two things:
Add entry to Gemfile
gem 'friends', path: "/path/to/friends/engine"
And then add route to config/routes.rb
mount Friends::Engine, at: "/friends"
For more information on this approch, checkout:
Rails Guide to Engines
Taming Rails Apps with Engines
RailsCast #277 Mountable Engines
The class names are same but path's are different, and you don't need to add classes to autoload except /lib/classes/friends/friend.rb
Did you tried the following way:
# app/models/friends/friend.rb
class Friends::Friends
#...
end
# Friends::Friends.new
# app/controllers/friends/friends_controller.rb
class Friends::FriendsController < ApplicationController
#...
end
# lib/classes/friends/friend.rb
module Classes
module Friends
class Friends
#...
end
end
end
# Classes::Friends::Friends.new
To add lib files to autoload add following to your applicaion.rb
config.autoload_paths += %W(#{config.root}/lib)

Rails class loading skips namespaced class when another class of same name in root namespace is loaded

I have two namespaces, each with its own controller and presenter classes:
Member::DocumentsController
Member::DocumentPresenter
Guest::DocumentsController
Guest::DocumentPresenter
Both presenters inherit from ::DocumentPresenter.
Controllers access their respective presenters without namespace specified, e.g.:
class Guest::DocumentsController < ActionController::Base
def show
DocumentPresenter.new(find_document)
end
end
This usually calls presenter within same namespace. However sometimes in development environment I see base ::DocumentPresenter is being used.
I suspect the cause is that base ::DocumentPresenter is already loaded, so Rails class auto-loading doesn't bother to look further. Is this likely the case? Can it happen in production environment too?
I can think of two solutions:
rename base class to DocumentPresenterBase
explicitly require appropriate presenter files in controller files
Is there a better solution?
You are correct in your assumptions - If you do not specify namespace, Ruby starts from current namespace and works its way up to find the class, and because the namespaced class is not autoloaded yet, the ::DocumentPresenter is found and autoloader does not trigger.
As a solution I would recommend renaming ::DocumentPresenter to DocumentPresenterBase, because this protects you from bugs when you forget namespacing or explicit requiring somewhere.
The second option to consider would actually be using specific namespaced classnames all over the place, but this suffers from bugs when you accidentally forget to namespace some call.
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
Third option would be your second - explicitly require all the classes in initializer beforehand. I have done this with Rails API which receives embedded models in JSON and Rails tends to namespace them when the actual models are not loaded yet.
Option 3.5 You could probably trick autoloader to do the heavy lifting (though, this might seem more like a hack):
class Guest::DocumentsController < ActionController::Base
# trigger autoload
Guest::DocumentPresenter
def show
# This should refer Guest::DocumentPresenter
DocumentPresenter.new(find_document)
end
def show
# As will this
DocumentPresenter.new(find_document)
end
end
Still the cleanest would be to rename the base class.
I think in 3 solutions if you want to mantein the name, one is your second solution.
1) explicitly require appropriate presenter files in controller files
2) Execute the full environment class path, like:
class Guest::DocumentsController < ActionController::Base
def show
Guest::DocumentPresenter.new(find_document)
end
end
3) Create a file on initialize directory and execute require manually (the worst options :S)

Resources