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.
Related
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.
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 do I extend a class that is defined by a gem when I'm using rails 6 / zeitwerk?
I've tried doing it in an initializer using require to load up the class first.
I've tried doing it in an initializer and just referencing the class to let autoloading load it up first.
But both of those approaches break auto-reloading in development mode.
I've tried putting it in lib/ or app/, but that doesn't work because then the class never gets loaded from the gem, since my new file is higher up in the load order.
There is a similar question here, but that one specifically asks how to do this in an initializer. I don't care if it's done in an initializer or not, I just want to figure out how to do it some way.
What is the standard way of doing something like this?
I do have one nasty hack that seems to be working, but I don't like it (update: this doesn't work either. reloading is still broken):
the_gem_root = $LOAD_PATH.grep(/the_gem/).grep(/models/).first
require("#{the_gem_root}/the_gem/some_model")
class SomeModel
def my_extension
...
end
end
I know is late, but this was a real pain and someone could find it helpful, in this example I'll be using a modules folder located on app that will contain custom modules and monkey patches for various gems.
# config/application.rb
...
module MyApp
class Application < Rails::Application
config.load_defaults(6.0)
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").each do |override|
load override
end
end
end
end
Apparently this pattern is called the Override pattern, it will prevent the autoload of your overrides by zeitwerk and each file would be loaded manually at the end of the load.
This pattern is also documented in the Ruby on Rails guide: https://edgeguides.rubyonrails.org/engines.html#overriding-models-and-controllers
I'm working with some legacy RoR code, which has four related classes, each defined in its own file. All of these classes are parser classes, and live in app/models/parsers. Each file name ends with _parser.rb.
Here's an example class def line from file adf_parser.rb:
class Parsers::AdfParser
I'm lost as to what the Parsers:: part of that is doing.
There's no explicit module called Parsers defined anywhere that I can find.
I don't see any documentation about implicitly creating modules just by adding module specifications to class names.
The only external dependency is "require 'csv'".
There are include statements within the class def, but I don't think they have anything that would explain the class name.
I created a new RoR test project and put stubs of these files in a parallel directory, and they won't run from the command line due to a
name error.
I don't see any examples online of classes named in this way.
I'm sure this isn't rocket surgery, but I've lost most of my morning trying to figure this out, and I'd love it if someone could just tell me what's going on with it.
Update: It sounds like this is just a bit of Rails magic, based on the subdirectory name. I think the reason that I got an error in my test app is that I just ran the files through the ruby interpreter, rather than invoking them with Rails in some way.
class Parsers::AdfParser is in practice equivalent to:
module Parsers
class AdfParser
For this to work properly, and the file to be autoloaded its location should be parsers/adf_parser.rb, whether under app/models or lib. Basically the file path needs to mimic the class hierarchy.
It's in the parsers sub-directory of modules; Rails namespaces for you by convention.
I seem to be having trouble with the naming conventions of the Lib Folder in Rails, and the error messages provided to me do not help. [For example, I have received a message saying that XXX::YYY::TextBox is expected to be defined xxx/yyy/text_box.rb, even though it clearly was defined there.] I think I'm getting the convention wrong.
Let's say I am working on YourModule::MyModule::MyClass. I clearly get that this file should be located in
lib/your_module/my_module/my_class.rb
But what should the actual file here look like? Which one of these (if either) are correct?
#your_module/my_module/my_class.rb
module YourModule
module MyModule
class MyClass
...
end
end
end
Or
#your_module/my_module/my_class.rb
class MyClass
...
end
In other words, do I need to nest the class inside of the module structure or not?
The lib folder has few conventions, as it is not autoloaded. So, how you organize the files is up to you, but you do have to name the classes correctly. Your first example is correct.
To get the files included you need to specify you want them in your application.rb file, see this example: Best way to load module/class from lib folder in Rails 3?
I would recommend making a folder just called lib/modules, since you probably won't have very many. Name the file my_class.rb. Then in application.rb you need:
config.autoload_paths += %W(#{config.root}/lib/modules)
That should take care of your issue.