Namespacing service objects in Rails 6 with Zeitwerk autoloader - ruby-on-rails

Rails 6 switched to Zeitwerk as the default autoloader. Zeitwerk will load all files in the /app folder, eliminating the need for namespacing. That means, a TestService service object in app/services/demo/test_service.rb can now be directly called e.g. TestService.new().call.
However, namespacing has been helpful to organize objects in more complex rails apps, e.g. API::UsersController, or for services we use Registration::CreateAccount, Registration::AddDemoData etc.
One solution suggested by the rails guide is to remove the path from the autoloader path in application.rb, e.g. config.autoload_paths -= Dir["#{config.root}/app/services/demo/"]. However, that feels like a monkey patch for shoehorning an old way or organizing objects into the new rails way.
What is the correct way of namespacing objects or a rails 6 way of organizing it without just forcing rails into the old way?

It is not true to say that Zeitwerk eliminates 'the need for namespacing'. Zeitwerk does indeed autoload all the subdirectories of app (except assets, javascripts, and views). Any directories under app are loaded into the 'root' namespace. But, Zeitwerk also 'autovivifies' modules for any directories under those roots. So:
/models/foo.rb => Foo
/services/bar.rb => Bar
/services/registration/add_demo_data.rb => Registration::AddDemoData
If you are already used to loading constants from 'non-standard' directories (by adding to config.autoload_paths), there's usually not much change. There are a couple of cases that do require a bit of tweaking, though. The first is where you are migrating a project that just adds app itself to the autoload path. In classic (pre-Rails 6), this allows you to use app/api/base.rb to contain API::Base, whereas in Zeitwerk it would expect it to contain only Base. That's the case you mention above where the recommendation is to exclude that directory from the autoload path. Another alternative would be to simply add a wrapper directory like app/api/api/base.rb.
The second issue to note is how Zeitwerk infers constants from file names. From the Rails migration guide:
classic mode infers file names from missing constant names
(underscore), whereas zeitwerk mode infers constant names from file
names (camelize). These helpers are not always inverse of each other,
in particular if acronyms are involved. For instance, "FOO".underscore
is "foo", but "foo".camelize is "Foo", not "FOO".
So, /api/api/base.rb actually equates to Api::Base in Zeitwerk, not API::Base.
Zeitwerk includes a rake task to verify autoloading in a project:
% bin/rails zeitwerk:check
Hold on, I am eager loading the application.
expected file app/api/base.rb to define constant Base

EDIT:
As clarified in comments, you actually don't need to add anything to autoload_paths. It's default behaviour for Zeitwerk in Rails when your place your code under some subdirectory in app.
Original answer:
I'm posting separate answer, but actually accepted answer has all the good information. Since my comment was bigger than allowed, I chose to add separate answer for those who are struggling with similar issue.
We have created "components" under app where we separate domain specific namespaces/packages. They co-exists with some "non-component" Rails parts, that are hard to move under components. With classic autoloader, we have added #{config.root}/app in our autoload_paths.
This setup fails for Zeitwerk and removing "#{config.root}/app" from autoload_paths didn't help. rmlockerd suggestion to move app/api/ under /app/api/api moved me thinking in creating separate 'app/components' and moving all components under this directory and add this path to autoload_paths. Zeitwerk likes this.

Related

Rails Zeitwerk not respecting class in a module

Running rails zeitwerk:check returns expected file app/api/mariana_tek_client.rb to define constant MarianaTekClient
The odd thing is that I have the following class defined in this file, which seems to follow the convention that I've seen documented: project/app/api/mariana_tek_client.rb
module Api
class MarianaTekClient
include HTTParty
end
end
If I remove the module from the file and leave the class definition only, Zeitwerk stops failing, but this is contrary to what I've seen in all its docs. Plus, I want my namespace!
This works:
class MarianaTekClient
include HTTParty
end
Would love if someone can clue me into why its failing with the namespace.
I'm guessing you've added app/api as an extra autoload path. If so, Zeitwerk will look in that folder for classes in the root namespace, and subfolders for classes in modules - so it expects app/api/mariana_tek_client.rb to contain MarianaTekClient; if you want Api::MarianaTekClient then that would need to go in app/api/api/mariana_tek_client.rb.
You could point Zeitwerk at app, and it would then look for Api::MarianaTekClient in app/api/mariana_tek_client.rb; but that is discouraged and would probably cause you more problems in the long term.
I'd recommend using the default Zeitwerk configuration, and putting your model classes under app/models; so it would then look for Api::MarianaTekClient in app/models/api/mariana_tek_client.rb - as would anyone else working on your code.

Rails 6 + Zeitwerk, loading files without class

We are upgrading to Rails 6, and have done most of the work to make our application work with Zeitwerk.
We do have two files, which are query objects, existing under app/queries and we don't define a class for them. They exists with methods only.
However, Zeitwerk expects the file to define a constant, but we don't deem it necessary.
Is there any way to tell Zeitwerk that we want to load these files as is?
You can tell the main autoloader to ignore them:
# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(
Rails.root.join("app/queries/foo.rb"),
Rails.root.join("app/queries/bar.rb")
)
And then the application is responsible for loading them using require or whatever.
If everything under app/queries should be ignored, you can ignore the directory itself:
# config/initializers/zeitwerk.rb
Rails.autoloaders.main.ignore(Rails.root.join("app/queries"))

Where is a logical place to put a file flag?

In a Ruby on Rails application, where would the most logical place be to put a "file flag."
I am attempting to externalize configuration and allow the presence of a file to be the deciding factor on whether or not something shows on the webapp.
Right now, I have a file here:
lib/
deployment_enabled
Model deployment.rb
class Deployment...
...
def deployment_enabled?
Dir["#{Rails.root}/lib/deployment_enabled"].any?
end
end
Now this works of course, but i'm not sure this follows the MVC paradigms, since the lib directory should consist of scripts. I could put it in config, but again - not sure it belongs there as rails uses this for rails specific configuration, not necessarily the webapp.
I could of course put this in our database, but that require a new table to be created, and that seems unnecessary.
Where's the most logical place to put this flag file? Does Rails have a directory that's created during the generation to put these sort of files?
I suggest using the Rails tmp directory for this purpose. Then:
File.exist?("#{Rails.root}/tmp/deployment_enabled")
Phusion Passenger use this kind of mechanism too.
https://www.phusionpassenger.com/library/walkthroughs/basics/ruby/reloading_code.html#tmp-always_restart-txt
I recommend that you follow the Twelve-Factor App guidelines and keep your code separate from your configuration. In this case you are really talking about a simple boolean value, and the presence of the file is just the mechanism you use to define the value. This should be done instead through an environment variable.
Something like:
DEPLOYMENT_ENABLED=1 RAILS_ENV=production rails server
You would then use an initializer in Rails to detect the value:
# config/initializers/deployment.rb
foo if ENV['DEPLOYMENT_ENABLED']
The value can still be modified at runtime, e.g., ENV['DEPLOYMENT_ENABLED'] = 0.

Ruby on Rails delay in updating

I'm learning rails and I've come across a little quirk that I can't seem to find the answer to anywhere:
Since I'm learning rails, I'll make a few tweaks to the code while the localhost is running (rails s) and then just refresh the browser to see if the change I wanted to make was accurate. This works for changes to the views, css, html, routing, etc.
But now I'm making changes to a controller file that is calling another ruby class that I wrote and when I make changes to the ruby class, they don't show up right away. The way I know this is that I use a variety of printf functions in the Ruby class to show the current state of things and if I add one and re-run, it won't show unless I shut the server down and restart it.
Any thoughts? Is this a known issue?
You must autoload the folder which contains your custom files:
# in config/application.rb:
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/extras)
There you write the folder which you want to be autoloaded.
WARNING: the naming is very important: files in there must be named as the class/modules they define (like models, controllers, etc):
foo.rb must define Foo costant
foo/bar.rb must define Foo:Bar costant
and you cannot autoload files which do not have this naming convention. The reason is linked to the autoload working: when in your code call f.e. the Foo constant, and the constant is missing, Rails tries to see if in its autoload paths there is a file that follows this naming convention, and if there is it loads it.

Rails project organization with many models

I'm working on a rails app that is starting to have what seems (to me) to be a lot of models. There are 15 right now, but I'm thinking about adding 3-4 more to serve as "tag" like models (I need more functionality than Acts As Taggable offers).
So, the reason this bugs me a bit, is that 7 of the 15 models belong to a common parent. Several are belong_to, and a few are has_and_belongs_to_many. All the new models I'm contemplating would belong_to the same parent as well.
So, what I'm wondering is, what is the best "Railsy" way of organizing this kind of situation?
Instead of app/models being super crowded with 6 "first-class" models and 10+ children of one of these, should/can I start using sub folders in my app folder? ie: app/models/parent/child.rb?
I know this is kind of an open-ended question, but I would really appreciate advice as to the best way to handle a rails project with a proliferation of models.
Thanks!
You can do this, I always do :)
Just beware of something: if you create a folder which has the name of one your models, it will fail. Actually, Rails will think you want to extend it.
So in your model folder, prepend the name of your class with whatever fancy you want.
Example: if you want to put models related to users, put them in models/user_related/
You'll have to add this to your application.rb file:
config.autoload_paths += Dir["#{Rails.root.to_s}/app/models/*"].find_all { |f| File.stat(f).directory? }
This will autoload all folders included in modelsdirectory.
I think apneadiving's answer is good approach
Based on research with activesupport 3.0.11, there are some rules to follow when choosing a directory name however:
The name must never match a constant in your system, or LoadError's could occur
The name must be able to be converted to a valid constant name, or NameError's will occur.
Explanation of problem #1
Apneadiving's example of a directory name app/models/user_related works as long as a
constant UserRelated is never used in your code. Otherwise a LoadError could
potentially happen.
For example, assume there was a model called UserProfile and the first time
rails sees the constant is in the UserRelated module. Rails will first try to
load a UserRelated\:\:UserProfile constant and failing that a UserProfile
constant.
If the user_profile file is at app/models/user_related/user_profile.rb, this
matches the underscored path of UserRelated\:\:UserProfile and the file would
be loaded expecting to define the UserRelated::UserProfile constant. This
would raise the following error because it really defines the UserProfile
constant.
Expected app/models/user_related/user_profile.rb to define UserRelated::UserProfile (LoadError)
This happens in the active support dependency code.
Explanation of problem #2
Another caveat is the directory name must be able turned into a valid ruby
constant name (although to follow #1 the constant should be undefined). For
example, if the directory name were app/models/user.related this would result
in the following error inside the active_support dependency code:
wrong constant name User.related (NameError)

Resources