diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f0199ee0..cc902e9f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,6 +26,7 @@ When releasing a new version: - The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details. - For schemas with enum values that differ only in casing, it's now possible to disable smart-casing in genqlient.yaml; see the [documentation](genqlient.yaml) for `casing` for details. +- It is now possible to bind to the generated package both via `bindings` and `package_bindings` options. ### Bug fixes: - The presence of negative pointer directives, i.e., `# @genqlient(pointer: false)` are now respected even in the when `optional: pointer` is set in the configuration file. diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index ddfb760b..d64f36ac 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -224,6 +224,10 @@ bindings: # to the bindings map, above, for each exported type in the package. Multiple # packages may be specified, and later ones take precedence over earlier ones. # Explicit entries in bindings take precedence over all package bindings. +# +# 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. +# Note that generating code inside the bound package may cause naming conflicts. package_bindings: - package: github.com/you/yourpkg/models diff --git a/generate/config.go b/generate/config.go index 94ee12c4..b9448783 100644 --- a/generate/config.go +++ b/generate/config.go @@ -47,6 +47,9 @@ type Config struct { // The directory of the config-file (relative to which all the other paths // are resolved). Set by ValidateAndFillDefaults. baseDir string + + // Fully qualified package being generated. + packagePath string } // A TypeBinding represents a Go type to which genqlient will bind a particular @@ -153,6 +156,23 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error { c.ExportOperations = pathJoin(baseDir, c.ExportOperations) } + // Calculate the fully qualified package path + genPkgPath, err := filepath.Rel(c.baseDir, c.Generated) + if err != nil { + return fmt.Errorf("finding relative path to generated package: %w", err) + } + pkgs, err := packages.Load(&packages.Config{ + Dir: c.baseDir, + Mode: packages.NeedName | packages.NeedModule, + }, "./"+filepath.Dir(genPkgPath)) + if err != nil { + return fmt.Errorf("loading generated package: %w", err) + } + if len(pkgs) > 1 { + return fmt.Errorf("ambiguous generated package") + } + c.packagePath = pkgs[0].ID + if c.ContextType == "" { c.ContextType = "context.Context" } @@ -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, Mode: mode, }, binding.Package) if err != nil { diff --git a/generate/generate_test.go b/generate/generate_test.go index 21dcaae2..0558423c 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -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 { // We need to put this within the current module, rather than in // /tmp, so that it can access internal/testutil. - f, err := os.CreateTemp("./testdata/tmp", namePrefix+"_*.go") + d, err := os.MkdirTemp("./testdata/tmp", namePrefix+"_*") if err != nil { return err } + + f, err := os.Create(filepath.Join(d, "generated.go")) + if err != nil { + return err + } + defer func() { f.Close() - os.Remove(f.Name()) + os.RemoveAll(d) }() + for _, extraFile := range extraFiles { + data, err := os.ReadFile(extraFile) + if err != nil { + return fmt.Errorf("reading file: %w", err) + } + + if err := os.WriteFile(filepath.Join(d, filepath.Base(extraFile)), data, 0o644); err != nil { + return fmt.Errorf("writing file: %w", err) + } + } + _, err = f.Write(content) if err != nil { return err } - cmd := exec.Command("go", "build", f.Name()) + cmd := exec.Command("go", "build", d) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() @@ -151,27 +168,28 @@ func TestGenerateWithConfig(t *testing.T) { baseDir string // relative to dataDir operations []string // overrides the default set below config *Config // omits Schema and Operations, set below. + extraFiles []string // extra files to pass to buildGoFile }{ - {"DefaultConfig", "", nil, getDefaultConfig(t)}, + {"DefaultConfig", "", nil, getDefaultConfig(t), nil}, {"Subpackage", "", nil, &Config{ Generated: "mypkg/myfile.go", - }}, + }, nil}, {"SubpackageConfig", "mypkg", nil, &Config{ Generated: "myfile.go", // (relative to genqlient.yaml) - }}, + }, nil}, {"PackageName", "", nil, &Config{ Generated: "myfile.go", Package: "mypkg", - }}, + }, nil}, {"ExportOperations", "", nil, &Config{ ExportOperations: "operations.json", - }}, + }, nil}, {"CustomContext", "", nil, &Config{ ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", - }}, + }, nil}, {"CustomContextWithAlias", "", nil, &Config{ ContextType: "github.com/Khan/genqlient/internal/testutil/junk---fun.name.MyContext", - }}, + }, nil}, {"StructReferences", "", []string{"InputObject.graphql", "QueryWithStructs.graphql"}, &Config{ StructReferences: true, Bindings: map[string]*TypeBinding{ @@ -181,7 +199,7 @@ func TestGenerateWithConfig(t *testing.T) { Unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate", }, }, - }}, + }, nil}, {"StructReferencesAndOptionalPointer", "", []string{"InputObject.graphql", "QueryWithStructs.graphql"}, &Config{ StructReferences: true, Optional: "pointer", @@ -192,32 +210,32 @@ func TestGenerateWithConfig(t *testing.T) { Unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate", }, }, - }}, + }, nil}, {"PackageBindings", "", nil, &Config{ PackageBindings: []*PackageBinding{ {Package: "github.com/Khan/genqlient/internal/testutil"}, }, - }}, + }, nil}, {"NoContext", "", nil, &Config{ ContextType: "-", - }}, + }, nil}, {"ClientGetter", "", nil, &Config{ ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromContext", - }}, + }, nil}, {"ClientGetterCustomContext", "", nil, &Config{ ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromMyContext", ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", - }}, + }, nil}, {"ClientGetterNoContext", "", nil, &Config{ ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromNowhere", ContextType: "-", - }}, + }, nil}, {"Extensions", "", nil, &Config{ Extensions: true, - }}, + }, nil}, {"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ Optional: "value", - }}, + }, nil}, {"OptionalPointer", "", []string{ "ListInput.graphql", "QueryWithSlices.graphql", @@ -225,21 +243,47 @@ func TestGenerateWithConfig(t *testing.T) { "SimpleQueryNoOverride.graphql", }, &Config{ Optional: "pointer", - }}, + }, nil}, {"OptionalGeneric", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ Optional: "generic", OptionalGenericType: "github.com/Khan/genqlient/internal/testutil.Option", - }}, + }, nil}, {"EnumRawCasingAll", "", []string{"QueryWithEnums.graphql"}, &Config{ Casing: Casing{ AllEnums: CasingRaw, }, - }}, + }, nil}, {"EnumRawCasingSpecific", "", []string{"QueryWithEnums.graphql"}, &Config{ Casing: Casing{ Enums: map[string]CasingAlgorithm{"Role": CasingRaw}, }, - }}, + }, nil}, + {"PackageBindingsLocal", "mypkg", nil, &Config{ + Generated: "myfile.go", + PackageBindings: []*PackageBinding{ + {Package: "github.com/Khan/genqlient/generate/testdata/queries/mypkg"}, + }, + }, []string{"testdata/queries/mypkg/types.go"}}, + {"TypeBindingsLocal", "mypkg", nil, &Config{ + Generated: "myfile.go", + Bindings: map[string]*TypeBinding{ + "ID": { + Type: "github.com/Khan/genqlient/generate/testdata/queries/mypkg.ID", + }, + }, + }, []string{"testdata/queries/mypkg/types.go"}}, + {"PackageBindingsLocalShorthand", "mypkg", nil, &Config{ + Generated: "myfile.go", + PackageBindings: []*PackageBinding{ + {Package: "."}, + }, + }, []string{"testdata/queries/mypkg/types.go"}}, + {"PackageBindingsLocalWithAlreadyGeneratedFiles", "mypkg", nil, &Config{ + Generated: "generated.go", + PackageBindings: []*PackageBinding{ + {Package: "."}, + }, + }, []string{"testdata/queries/mypkg/types.go", "testdata/queries/mypkg/generated.go"}}, } sourceFilename := "SimpleQuery.graphql" @@ -277,8 +321,7 @@ func TestGenerateWithConfig(t *testing.T) { t.Skip("skipping build due to -short") } - err := buildGoFile(sourceFilename, - generated[config.Generated]) + err := buildGoFile(sourceFilename, generated[config.Generated], test.extraFiles...) if err != nil { t.Error(err) } @@ -330,7 +373,6 @@ func TestGenerateErrors(t *testing.T) { Schema: []string{filepath.Join(errorsDir, schemaFilename)}, Operations: []string{filepath.Join(errorsDir, sourceFilename)}, Package: "test", - Generated: os.DevNull, ContextType: "context.Context", Bindings: map[string]*TypeBinding{ "ValidScalar": {Type: "string"}, diff --git a/generate/imports.go b/generate/imports.go index 5d6f0a29..1f503019 100644 --- a/generate/imports.go +++ b/generate/imports.go @@ -99,15 +99,24 @@ func (g *generator) ref(fullyQualifiedName string) (qualifiedName string, err er pkgPath := nameToImport[:i] localName := nameToImport[i+1:] - alias, ok := g.imports[pkgPath] - if !ok { - if g.importsLocked { - return "", errorf(nil, - `genqlient internal error: imports locked but package "%v" has not been imported`, pkgPath) + + var out strings.Builder + out.WriteString(prefix) + + if pkgPath != g.Config.packagePath { + alias, ok := g.imports[pkgPath] + if !ok { + if g.importsLocked { + return "", errorf(nil, + `genqlient internal error: imports locked but package "%v" has not been imported`, pkgPath) + } + alias = g.addImportFor(pkgPath) } - alias = g.addImportFor(pkgPath) + out.WriteString(alias + ".") } - return prefix + alias + "." + localName, nil + out.WriteString(localName) + + return out.String(), nil } // Returns the import-clause to use in the generated code. diff --git a/generate/testdata/queries/mypkg/generated.go b/generate/testdata/queries/mypkg/generated.go new file mode 100644 index 00000000..21aa0788 --- /dev/null +++ b/generate/testdata/queries/mypkg/generated.go @@ -0,0 +1,67 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package mypkg + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// SimpleQueryResponse is returned by SimpleQuery on success. +type SimpleQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User SimpleQueryUser `json:"user"` +} + +// GetUser returns SimpleQueryResponse.User, and is useful for accessing the field via an interface. +func (v *SimpleQueryResponse) GetUser() SimpleQueryUser { return v.User } + +// SimpleQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type SimpleQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() ID { return v.Id } + +// The query or mutation executed by SimpleQuery. +const SimpleQuery_Operation = ` +query SimpleQuery { + user { + id + } +} +` + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: SimpleQuery_Operation, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/queries/mypkg/types.go b/generate/testdata/queries/mypkg/types.go new file mode 100644 index 00000000..2e84a054 --- /dev/null +++ b/generate/testdata/queries/mypkg/types.go @@ -0,0 +1,3 @@ +package mypkg + +type ID string diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocal-testdata-queries-mypkg-myfile.go b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocal-testdata-queries-mypkg-myfile.go new file mode 100644 index 00000000..21aa0788 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocal-testdata-queries-mypkg-myfile.go @@ -0,0 +1,67 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package mypkg + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// SimpleQueryResponse is returned by SimpleQuery on success. +type SimpleQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User SimpleQueryUser `json:"user"` +} + +// GetUser returns SimpleQueryResponse.User, and is useful for accessing the field via an interface. +func (v *SimpleQueryResponse) GetUser() SimpleQueryUser { return v.User } + +// SimpleQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type SimpleQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() ID { return v.Id } + +// The query or mutation executed by SimpleQuery. +const SimpleQuery_Operation = ` +query SimpleQuery { + user { + id + } +} +` + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: SimpleQuery_Operation, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalShorthand-testdata-queries-mypkg-myfile.go b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalShorthand-testdata-queries-mypkg-myfile.go new file mode 100644 index 00000000..21aa0788 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalShorthand-testdata-queries-mypkg-myfile.go @@ -0,0 +1,67 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package mypkg + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// SimpleQueryResponse is returned by SimpleQuery on success. +type SimpleQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User SimpleQueryUser `json:"user"` +} + +// GetUser returns SimpleQueryResponse.User, and is useful for accessing the field via an interface. +func (v *SimpleQueryResponse) GetUser() SimpleQueryUser { return v.User } + +// SimpleQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type SimpleQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() ID { return v.Id } + +// The query or mutation executed by SimpleQuery. +const SimpleQuery_Operation = ` +query SimpleQuery { + user { + id + } +} +` + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: SimpleQuery_Operation, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalWithAlreadyGeneratedFiles-testdata-queries-mypkg-generated.go b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalWithAlreadyGeneratedFiles-testdata-queries-mypkg-generated.go new file mode 100644 index 00000000..21aa0788 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindingsLocalWithAlreadyGeneratedFiles-testdata-queries-mypkg-generated.go @@ -0,0 +1,67 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package mypkg + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// SimpleQueryResponse is returned by SimpleQuery on success. +type SimpleQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User SimpleQueryUser `json:"user"` +} + +// GetUser returns SimpleQueryResponse.User, and is useful for accessing the field via an interface. +func (v *SimpleQueryResponse) GetUser() SimpleQueryUser { return v.User } + +// SimpleQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type SimpleQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() ID { return v.Id } + +// The query or mutation executed by SimpleQuery. +const SimpleQuery_Operation = ` +query SimpleQuery { + user { + id + } +} +` + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: SimpleQuery_Operation, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-TypeBindingsLocal-testdata-queries-mypkg-myfile.go b/generate/testdata/snapshots/TestGenerateWithConfig-TypeBindingsLocal-testdata-queries-mypkg-myfile.go new file mode 100644 index 00000000..21aa0788 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-TypeBindingsLocal-testdata-queries-mypkg-myfile.go @@ -0,0 +1,67 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package mypkg + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// SimpleQueryResponse is returned by SimpleQuery on success. +type SimpleQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User SimpleQueryUser `json:"user"` +} + +// GetUser returns SimpleQueryResponse.User, and is useful for accessing the field via an interface. +func (v *SimpleQueryResponse) GetUser() SimpleQueryUser { return v.User } + +// SimpleQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type SimpleQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() ID { return v.Id } + +// The query or mutation executed by SimpleQuery. +const SimpleQuery_Operation = ` +query SimpleQuery { + user { + id + } +} +` + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: SimpleQuery_Operation, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/snapshots/TestValidConfigs-Lists.yaml b/generate/testdata/snapshots/TestValidConfigs-Lists.yaml index 7ff70462..d0c74c43 100644 --- a/generate/testdata/snapshots/TestValidConfigs-Lists.yaml +++ b/generate/testdata/snapshots/TestValidConfigs-Lists.yaml @@ -23,5 +23,6 @@ StructReferences: (bool) false, Extensions: (bool) false, AllowBrokenFeatures: (bool) false, - baseDir: (string) (len=21) "testdata/valid-config" + baseDir: (string) (len=21) "testdata/valid-config", + packagePath: (string) (len=1) "." }) diff --git a/generate/testdata/snapshots/TestValidConfigs-Simple.yaml b/generate/testdata/snapshots/TestValidConfigs-Simple.yaml index 77cc1b32..3d5ee845 100644 --- a/generate/testdata/snapshots/TestValidConfigs-Simple.yaml +++ b/generate/testdata/snapshots/TestValidConfigs-Simple.yaml @@ -17,5 +17,6 @@ StructReferences: (bool) false, Extensions: (bool) false, AllowBrokenFeatures: (bool) false, - baseDir: (string) (len=21) "testdata/valid-config" + baseDir: (string) (len=21) "testdata/valid-config", + packagePath: (string) (len=1) "." }) diff --git a/generate/testdata/snapshots/TestValidConfigs-Strings.yaml b/generate/testdata/snapshots/TestValidConfigs-Strings.yaml index 1638e8da..fa0f176d 100644 --- a/generate/testdata/snapshots/TestValidConfigs-Strings.yaml +++ b/generate/testdata/snapshots/TestValidConfigs-Strings.yaml @@ -21,5 +21,6 @@ StructReferences: (bool) false, Extensions: (bool) false, AllowBrokenFeatures: (bool) false, - baseDir: (string) (len=21) "testdata/valid-config" + baseDir: (string) (len=21) "testdata/valid-config", + packagePath: (string) (len=1) "." })