Rails Zeitwerk snakecase - ruby-on-rails

When we have a directory under app/ that we want Zeitwerk to work off of, and say that naming happens to be something like
app/stuff/graphql.rb
app/stuff/graphql_error.rb
then Zeitwerk is looking for some module Stuff that has some module or class Graphql. But in my code, I am always writing my modules and classes as GraphQL to match that convention. So Zeitwerk is now throwing Zeitwerk::NameError as it tries to work with the code. I don't want to use Stuff::GraphqlError, I want to use Stuff::GraphQLError. How do I trick Zeitwerk here?

I believe Zeitwerk has inflectors that can be used for this:
https://github.com/fxn/zeitwerk#inflection

In order for app/stuff to act as a namespace, you have to put app itself as an autoload path. This is a bit tricky, please have a look at https://guides.rubyonrails.org/classic_to_zeitwerk_howto.html#having-app-in-the-autoload-paths.

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.

Namespacing service objects in Rails 6 with Zeitwerk autoloader

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.

How to ignore a folder in Zeitwerk for Rails 6?

Simple question, but somehow the answer escapes me.
In moving to Rails 6 with Zeitwerk, I get:
Please, check the "Autoloading and Reloading Constants" guide for solutions.
(called from <top (required)> at APP_ROOT/config/environment.rb:7)
rails aborted!
Zeitwerk::NameError: wrong constant name Enforce-calls-to-come-from-aws inferred by Module from directory
APP_ROOT/app/junkyard/enforce-calls-to-come-from-aws
Possible ways to address this:
* Tell Zeitwerk to ignore this particular directory.
* Tell Zeitwerk to ignore one of its parent directories.
* Rename the directory to comply with the naming conventions.
Which seems great: that's a junk folder and should never be loaded, so ignoring it makes perfect sense.
The Zeitwerk docs at https://github.com/fxn/zeitwerk say
tests = "#{__dir__}/**/*_test.rb"
loader.ignore(tests)
loader.setup
is how you ignore a folder. Fair enough. But how do I find loader? The Rails guide on Zeitwerk autoloading (https://guides.rubyonrails.org/autoloading_and_reloading_constants.html) doesn't mention how to ignore folders directly, but does mention the autoloader stashed at Rails.autoloaders.main, so I figured that
Rails.autoloaders.main.ignore("#{__dir__}/junkyard/**/*.rb")
or
Rails.autoloaders.main.ignore("#{__dir__}/app/junkyard/**/*.rb")
would be the way to go. No luck. I've tried putting this in application.rb and in initializers/zeitwerk.rb and neither worked.
Any ideas where and how to ignore a folder with Zeitwerk within Rails?
PS: yes, I know I should just remove this from app, and I will. But the question still vexes.
I ran into this same issue, and it turns out it was complaining about a folder name.
Adding this to application.rb may work for you:
Rails.autoloaders.main.ignore(Rails.root.join('app/junkyard'))
I added this to config/initializers/zeitwerk.rb:
Rails.autoloaders.each do |autoloader|
autoloader.ignore(Rails.root.join('app/ui'))
...

Use relative root for i18n in presenter files

Working with a file like app/presenters/foo.rb, I want to be able to have an i18n key foo.whatever and reference it inside foo.rb as I18n.t('.whatever'), in a way that's similar to doing it with views.
Is that possible? I dug through the i18n guide on Rails and searched through the internet ("add relative roots to i18n") pretty thoroughly to no avail.
Is foo a class or a module? you could make all your presenters extend a base module with something like:
def t(key)
scope = "presenters.#{self.class.to_s.underscore.gsub('/', '.')}"
I18n.t(key, scope: scope, default: I18n.t(key))
end
EDIT: changed to be correct Rails syntax and work with module namespaces

Classes in subfolders in app/models don't get loaded?

I would like to have a folder of related Models in my Rails 4 app. Why don't classes in app/models/debt_models get loaded?
I guess there is a work around by how come Rails just doesn't recursively go through folders under app/models? Is there a reason? Seems like this is something Rails should do without me having to tell Rails to do that.
You just have to follow conventions. If you put models in debt_models, it means you namespace them like:
module DebtModels
class Foo
Path: app/models/debt_models/foo.rb

Resources