Skip to content

Commit

Permalink
feat: support binding to generated package
Browse files Browse the repository at this point in the history
This allows one to generate code in an existing package without causing
import loops.
  • Loading branch information
costela committed Aug 24, 2023
1 parent 0581754 commit 18cb77e
Show file tree
Hide file tree
Showing 14 changed files with 455 additions and 37 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docs/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions generate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
Expand Down Expand Up @@ -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 {
Expand Down
96 changes: 69 additions & 27 deletions generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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{
Expand All @@ -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",
Expand All @@ -192,54 +210,80 @@ 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",
"SimpleQueryWithPointerFalseOverride.graphql",
"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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"},
Expand Down
23 changes: 16 additions & 7 deletions generate/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions generate/testdata/queries/mypkg/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions generate/testdata/queries/mypkg/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package mypkg

type ID string
Loading

0 comments on commit 18cb77e

Please sign in to comment.