fxn/zeitwerk

How to autoload one single file?

yetrun opened this issue · 11 comments

The loader.push_dir autoloads based on one directory, but how to autoload a single file. for example, I have a file app/demo_app.rb which defines a class called DemoApp, but I do not want to load the whole directory app.

This is work, but it is too many side effects:

loader.push_dir('app')
fxn commented

Just use Ruby for that. At some point issue an autoload call:

autoload :DemoAppp, "app/demo" # or whatever
fxn commented

Going to close this one. Please do not hesitate to followup if more help is needed.

Going to close this one. Please do not hesitate to followup if more help is needed.

Can I continue providing more information here when the issue has been closed?

The autoload is not managed by zeitwerk so it cannot use the benefits provided by zeitwerk, such as logging, reloading.

On other side, the reloading of using autoload has some issues, the following is a example:

autoload :Foo, './foo.rb'
Foo.new.foo # works

Object.send(:remove_const, :Foo)
Foo.new.foo # doesn't autoload, raising error

So do you plan to add such a feature in future?

fxn commented

Supporting individual files is unclear, in your example only the basename matters, but problem is supporting namespaces, because in Ruby you need all parents, one by one, before you can reach the bottom and set an autoload.

What I had in mind with my reply was, you don't need Zeitwerk in the project to do this.

If you have Zeitwerk for the rest of the project, then I'd like to understand your use case better. Why do you have Zeitwerk and a directory you don't want to autoload except for one file? What's in that directory, who loads that code? Why is it not reloadable?

My code organization in my project is:

  • api/: placing the api DSLs, and the DSLs are written to a class, so it can use Zeitwerk's autoload. The namespace is API.
  • policies/: placing the pundit policies, needing autoload, and the namespace is Object.
  • models/: placing the active records, needing autoload, and the namespace is Object.
  • helpers/: placing some helper functions or helper classes, which requires it by hand, such as require_relative 'helpers/string_utils'.
  • demo_app.rb: placing the entry app of my project, and it is a class named 'DemoApp'.

I have firstly tried using zeitwerk to organize it like this:

module API; end

loader = Zeitwerk::Loader.new
loader.push_dir('app/api', namespace: API)
loader.push_dir('app/models')
loader.push_dir('app/entities')
# and I want to add autoload for `demo_app.rb`.
fxn commented

Got it. Let me ask a few more questions.

  • Where do you instantiate the loader?
  • Why aren't helpers managed by the loader?
  • You want DemoApp to be reloadable, but not the helpers?
  • Who is executing demo_app.rb and how?
  • My project has some setuping code for environment, like many web framework does, but I do it simply by hand. In a nutshell,you can believe it that I instantiate the loader before the app has setup.
  • Because the helpers has many legacy code which violates the conventional file structure.
  • It is better the if helpers can be reloadable. But if it cannot, I can accept because changing helpers is not often.
  • Because my project is a rack project and DemoApp is a rack middleware, so I can run it in config.ru. Combined with the first question, the config.rb is like below:
    # initiate the loader
    loader = Zeitwerk::Loader.new
    # ... and configure the autoload and reloadable
    
    # load the rack app
    run ->(env) { DemoApp.run(env) } # Because I want to be reloadable, so not `run DemoApp`
fxn commented

OK, I understand better your project now. Thank you.

So, Zeitwerk won't support autoloading files. The reason for this is that the fundamental unit of work in this problem is the namespace. Constants belong to namespaces in Ruby. So, the interface is just a mirror of this way in which Ruby works, you first give me namespaces in form of directories, and then we can work out the files.

Your project deviates from the guidelines, but you can still make it work if you want.

One way to make it work would be to write a taylor-made configuration for your project using ignore, collapse, and/or nested namespaces. Optionally moving demo_app.rb below app or below some directory you can push_dir.

If you don't want to configure the loader like that, you can still hack it all yourself with something like this (not tested):

demo_app_rb = File.expand_path('demo_app.rb', __dir__)

loader.on_setup do
  autoload :DemoApp, demo_app_rb
end

loader.on_unload do
  Object.send(:remove_const, :DemoApp)
  $LOADED_FEATURES.delete(demo_app_rb)
end

None of this is particularly elegant and simple, the way to make it so would be to deviate less from the conventions. But, at least, there are ways to make it work the way you have it.

fxn commented

So, Zeitwerk won't support autoloading files.

I don't mean ever here, I mean immediately. There are APIs that could work, but have to think about the implications.

Thank you very much, and I will reconsider my project structure for new future projects. The current project i will take your advise and maybe remove the helpers directory from base app directory, so the project structure would be:

  • app/: which managed by zeitwerk
    • api/: the namespace is API
    • entities/: the namespace is Entities
    • policies/: the namespace is Object
    • models/: the namespace is Object
    • demo_app.rb: the class DemoApp
  • helpers/: placing the lagecy helpers and need require them by hand
  • Other directories and files the project needs

And I would load and configure zeitwerk like this:

loader = Zeitwerk::Loader.new
loader.inflector.inflect("api" => "API")
loader.push_dir('app')
loader.push_dir('app/models')
loader.push_dir('app/policies')

I make the whole app to obey the file structure zeitwerk needs, and make the directories app/models and app/policies to use another regulation which namespace is Object but not Models or Policies. I have tested it ok, but I don't know zeitwerk supports it definitely because they are some overlap.

fxn commented

That looks good.

Yes, nested root directories is a feature you can leverage to have this layout. In this case app/demo_app.rb is expected to define DemoApp, but app/policies is expected to represent Object because it is a root directory. From the point of view of Zeitwerk, root directories are separate trees even if they are not in the file system.

And if you wanted app/helpers, you could configure loader.ignore('app/helpers') and it would work too.

Glad that you found a layout that works and you like. I might think about an API to load a file given a namespace for it, but need to ponder it carefully and I am not sure it is going to make it.