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

feat: support binding to generated package #285

Conversation

costela
Copy link

@costela costela commented Jul 18, 2023

This allows one to generate code in an existing package without causing import loops.

It is now possible to use something like the following (assuming we're in module github.com/foo/bar):

generated: generated/genqlient.go
package_bindings:
  - github.com/foo/bar/generated

or (shorthand variant of the example above)

generated: generated/genqlient.go
package_bindings:
  - .

or

generated: generated/genqlient.go
bindings:
  Bar:
    type: github.com/foo/bar/generated.Bar

The last example assumes some file in generated/ defines the type Bar.

Closes #283

Copy link
Collaborator

@benjaminjkraft benjaminjkraft left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks for figuring this out! I'm very happy with how little code it turned out to be, and the tests are very convincing that it works.

One question I have is whether we should allow any valid relative path in the genqlient.yaml, i.e. you could say ./mypkg/subpkg if that's what you want. That seems like it could be quite convenient rather than writing out the name of the current package a bunch of times. In that case we'd probably want to say . means the directory with the genqlient.yaml. Although, maybe it's better to say you have to write it out, like you do in Go import blocks. In fact I think these changes would solve the problem without allowing special . syntax at all -- you would just do what you did in the first two test cases. I think I'd prefer to either fully support the usual, or stick to what we have, since config syntax is always confusing and can be more annoying to change later.

Beyond that I think my questions are about whether running this in the real world could hit weird cases we don't really see in a test. I put some comments inline. But probably if we can't figure those out we can just see if anyone complains.

Comment on lines +168 to +170
if err != nil {
return fmt.Errorf("loading generated package: %w", err)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this is safe. Like would this break things if the directory where you're putting the output is empty (as it would be if your output is in a separate package and this is the first run of genqlient)? On the other hand swallowing the error seems dangerous, and it's not clear how easy it is to know if we actually need packagePath. Maybe instead we should just look at baseDir and then add the suffix ourselves? (Is that safe?)

(For that matter, does this require that baseDir is a valid Go package? Or at least one with at least one Go file with valid package clause? Maybe that's fine; if you have some kind of monorepo where all the Go is in a subdirectory you can just put genqlient.yaml in that directory.)

If we don't know the answer, it's probably not a huge deal; it doesn't really affect the API surface we are committing to so if we break someone's workflow we can fix it then.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this break things if the directory where you're putting the output is empty

Yes, it would break, but that would be the same as trying to bind to an empty package, right?

does this require that baseDir is a valid Go package?

It shouldn't: it's just used as a base for the package search.

@@ -203,6 +223,7 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error {

mode := packages.NeedDeps | packages.NeedTypes
pkgs, err := packages.Load(&packages.Config{
Dir: c.baseDir,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud: this should be ok for third-party packages because it's just the directory where we run the build tool, so if binding.Package is absolute then it'll still return the right package (and, if relevant, will now respect any versions/replaces in the module, if they differ from those in the current directory, which is probably actually better than current behavior, in the rare case where it matters).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, exactly. This is just needed to support the "relative import" hack we use above, which isn't usual, but is supported.

@@ -21,24 +21,41 @@ const (
// buildGoFile returns an error if the given Go code is not valid.
//
// namePrefix is used for the temp-file, and is just for debugging.
func buildGoFile(namePrefix string, content []byte) error {
func buildGoFile(namePrefix string, content []byte, extraFiles ...string) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

Comment on lines +227 to +229
#
# Using "." is a shorthand for the package containing the generated file.
# Both allow the generated code to live in the same package as its types.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I would have some concerns about actually using the generated package here. I worry it would end up being circular in ways that are hard to understand -- in that now genqlient depends on genqlient-generated types. I don't think we need to ban it, but maybe we should say in the comment here that it's not a good idea? Or does it actually work fine?

Also, whatever we settle on for the syntax, can you add it to the docs for bindings above too?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now genqlient depends on genqlient-generated types

Yes, a warning might be appropriate, but it should work as long as the user avoids name conflicts.
I've added a "double generate" test to try to cover this case and made it more explicit in the comment.

can you add it to the docs for bindings above too

The shorthand is intentionally only supported in the "whole package" mode. Otherwise one would end up with ..ID or some other weird syntax to reference the types inside the package. Just didn't feel like it was worth it. WDYT?

This allows one to generate code in an existing package without causing
import loops.
@costela costela force-pushed the leo.antunes/allow-generating-bindings-for-local-package branch from ae9f207 to 18cb77e Compare August 24, 2023 13:26
Copy link
Author

@costela costela left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback and sorry for the late reply! Vacation got in the way!

PTAL again! 🙏

Comment on lines +227 to +229
#
# Using "." is a shorthand for the package containing the generated file.
# Both allow the generated code to live in the same package as its types.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now genqlient depends on genqlient-generated types

Yes, a warning might be appropriate, but it should work as long as the user avoids name conflicts.
I've added a "double generate" test to try to cover this case and made it more explicit in the comment.

can you add it to the docs for bindings above too

The shorthand is intentionally only supported in the "whole package" mode. Otherwise one would end up with ..ID or some other weird syntax to reference the types inside the package. Just didn't feel like it was worth it. WDYT?

Comment on lines +168 to +170
if err != nil {
return fmt.Errorf("loading generated package: %w", err)
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this break things if the directory where you're putting the output is empty

Yes, it would break, but that would be the same as trying to bind to an empty package, right?

does this require that baseDir is a valid Go package?

It shouldn't: it's just used as a base for the package search.

@@ -203,6 +223,7 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error {

mode := packages.NeedDeps | packages.NeedTypes
pkgs, err := packages.Load(&packages.Config{
Dir: c.baseDir,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, exactly. This is just needed to support the "relative import" hack we use above, which isn't usual, but is supported.

@StevenACoffman StevenACoffman removed their request for review October 12, 2023 18:38
@benjaminjkraft
Copy link
Collaborator

Closing in favor of #316 which makes this as a narrower change to support existing syntax. If you're interested, you could revisit adding the shorthand (package_bindings: . etc.); I think doing that separately will make some of the still-questions about what syntax we should support clearer.

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

Successfully merging this pull request may close these issues.

Binding to a type in the same package as generated code produces invalid output
2 participants