fxn/zeitwerk

Public API that resolves constants for a given filename

bradgessler opened this issue · 9 comments

I've started using Guard again for local development and found myself thinking it would be cool if I could run touch ./lib/my-gem/fizz/buzz.rb, open up the file, and see this:

module MyGem::Fizz
  class Buzz
  end
end

To accomplish that, I'd need some way to pass Zeitwerk a path, ./lib/my-gem/fizz/buzz.rb in my example, and get back a list of constants such as [:MyGem, :Fizz, :Buzz] that my Guard plugin could interpret to generate the output above.

During my initial analysis of the Zeitwerk gem, I got to https://github.com//fxn/zeitwerk/blob/3a84e57bed93e6193851dd89042755264e516ed2/lib/zeitwerk/loader/callbacks.rb#L11-L14, and discovered these APIs are private and not as straight forward as passing it a path.

This ticket opens the discussion for such an API, per @fxn's request at https://twitter.com/fxn/status/1682078644955389952, to uncover other use cases and determine if its worth implementing & supporting.

fxn commented

Yes, this makes sense.

To be coherent with the existing API, this method would return a string with a constant path, "MyGem::Fizz::Buzz", and could accept also paths that do not end in ".rb" (for namespaces). You'd just split by "::" if you need the segments.

I guess the point is precisely that the file may not exist yet, so that should not be validated.

There will be edge cases like, it is a descendant of an ignored directory, or it is under no root directory, ..., that kind of thing. But these are details.

fxn commented

Maybe this needs the argument to exist, because if the path

/full/path/to/changelog

does not exist, there's no way to know if that would be a file or a directory. If it was a file, there is no constant path expected there, because only (non-hidden) files with ".rb" extension are processed. That is, we'd return nil or a constant path, but we can't tell from a virtual path.

The only reason I can think of to return the namespaces for a directory would be passing in a value like ./lib/my-gem/fizz from my example above and getting back MyGem::Fizz. In practice, I can't think of when I'd actually need to do that, and it doesn't seem like a great idea to break the inverse consistency of the APIs unless there's a very compelling reason to do so.

fxn commented

Let me elaborate a bit more.

The API I am drafting is loader.cpath_at(path). The way I see it, this method should return a string with a constant path if the loader expects that path to define one, or nil otherwise. Examples:

loader.cpath_at('app/models')                                # => "Object"
loader.cpath_at('app/controllers/admin')                     # => "Admin"
loader.cpath_at('app/controllers/admin/users_controller.rb') # => "Admin::UsersController"
loader.cpath_at('/')                                         # => nil, not a descendant of root directories
loader.cpath_at('lib/extensions/kernel.rb')                  # => nil, assuming the file is ignored
loader.cpath_at('lib/.DS_STORE')                             # => nil, it is a hidden file
loader.cpath_at('lib/README.md')                             # => nil, extension is not "rb"

If we require the argument to exist, that is doable.

But if we don't, then there is an edge case in virtual paths that do not have extensions, because

loader.cpath_at('foo')

should return "Foo" if "foo" is a directory, and nil otherwise. However, the path is virtual and you don't know.

In the example in the description you touch the file first, right?

In the example in the description you touch the file first, right?

Correct! In my example I'd touch to create a file with 0 bytes, then Guard would see the new file, see that it's 0 bytes, then put the module block in the file.

fxn commented

It's in main. If all is good, I'll release soon.

Just created guard-zeitwerk at https://github.com/rubymonolith/guard-zeitwerk and this works beautifully, thanks!

I'm still refining the plugin. When you cut a release with the new cpath_at API I'll be able to release my plugin with a > 2.6.8 dependency requirement.

Can't wait to use this for my Rails and Gem projects.

*Now renamed to expected_cpath_at.

I checked this into a demo repo at https://github.com/rubymonolith/demo/blob/main/Guardfile. If anybody is interested in playing around with it, clone https://github.com/rubymonolith/demo and run bundle exec guard, then create a file like ./app/model/foo.rb.

fxn commented

It's out, version 2.6.9.

The method got a final rename after sleeping on it and exchanging impressions with @matthewd, it's been published as cpath_expected_at.