Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public API that resolves constants for a given filename #270

Closed
bradgessler opened this issue Jul 20, 2023 · 9 comments
Closed

Public API that resolves constants for a given filename #270

bradgessler opened this issue Jul 20, 2023 · 9 comments

Comments

@bradgessler
Copy link

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
Copy link
Owner

fxn commented Jul 20, 2023

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
Copy link
Owner

fxn commented Jul 21, 2023

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.

@bradgessler
Copy link
Author

bradgessler commented Jul 21, 2023

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
Copy link
Owner

fxn commented Jul 21, 2023

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?

@bradgessler
Copy link
Author

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 fxn closed this as completed in 50f661c Jul 23, 2023
@fxn
Copy link
Owner

fxn commented Jul 23, 2023

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

@bradgessler
Copy link
Author

bradgessler commented Jul 24, 2023

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.

@bradgessler
Copy link
Author

*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
Copy link
Owner

fxn commented Jul 25, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants