diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1598619..110b14f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -40,6 +40,7 @@ Version 0.5.0 adds several new configuration options and convenience features. N - genqlient can now run as a portable binary (i.e. without a local checkout of the repository or `go run`). - You can now enable `use_extensions` in the configuration file, to receive extensions returned by the GraphQL API server. Generated functions will return extensions as `map[string]interface{}`, if enabled. - You can now use `graphql.NewClientUsingGet` to create a client that uses query parameters to pass the query to the GraphQL API server. +- You can now bind all types from a package in `genqlient.yaml` using the new `package_bindings` option. - In config files, `schema`, `operations`, and `generated` can now be absolute paths. - You can now configure how nullable types are mapped to Go types in the configuration file. Specifically, you can set `optional: pointer` to have all nullable GraphQL arguments, input fields, and output fields map to pointers. diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index 37bb8e9..11fb187 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -97,7 +97,6 @@ use_struct_references: boolean # Defaults to false. use_extensions: boolean - # Customize how optional fields are handled. optional: # Customize how models are generated for optional fields. This can currently @@ -114,7 +113,6 @@ optional: # map to Go nil- and empty-slice. output: value - # A map from GraphQL type name to Go fully-qualified type name to override # the Go type genqlient will use for this GraphQL type. # @@ -210,3 +208,13 @@ bindings: # certain fields but others are optional. expect_exact_fields: "{ id name }" # unmarshaler and marshaler are also valid here, see above for details. + +# A list of packages for which genqlient should automatically generate +# bindings. This is equivalent to adding a entry +# TypeName: +# type: github.com/you/yourpkg/models.TypeName +# 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. +package_bindings: +- package: github.com/you/yourpkg/models diff --git a/generate/config.go b/generate/config.go index d6f057d..d37b5ce 100644 --- a/generate/config.go +++ b/generate/config.go @@ -2,10 +2,12 @@ package generate import ( _ "embed" + "fmt" "go/token" "os" "path/filepath" + "golang.org/x/tools/go/packages" "gopkg.in/yaml.v2" ) @@ -26,6 +28,7 @@ type Config struct { ContextType string `yaml:"context_type"` ClientGetter string `yaml:"client_getter"` Bindings map[string]*TypeBinding `yaml:"bindings"` + PackageBindings []*PackageBinding `yaml:"package_bindings"` Optional string `yaml:"optional"` StructReferences bool `yaml:"use_struct_references"` Extensions bool `yaml:"use_extensions"` @@ -52,6 +55,13 @@ type TypeBinding struct { Unmarshaler string `yaml:"unmarshaler"` } +// A PackageBinding represents a Go package for which genqlient will +// automatically generate TypeBindings, and is documented further at: +// https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml +type PackageBinding struct { + Package string `yaml:"package"` +} + // pathJoin is like filepath.Join but 1) it only takes two argsuments, // and b) if the second argument is an absolute path the first argument // is ignored (similar to how python's os.path.join() works). @@ -98,6 +108,42 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error { c.Package = base } + if len(c.PackageBindings) > 0 { + for _, binding := range c.PackageBindings { + mode := packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes + pkgs, err := packages.Load(&packages.Config{ + Mode: mode, + }, binding.Package) + if err != nil { + return err + } + + if c.Bindings == nil { + c.Bindings = map[string]*TypeBinding{} + } + + for _, pkg := range pkgs { + p := pkg.Types + if p == nil || p.Scope() == nil { + return errorf(nil, "unable to bind package %s: no types found", binding.Package) + } + + for _, typ := range p.Scope().Names() { + if token.IsExported(typ) { + // Check if type is manual bindings + _, exist := c.Bindings[typ] + if !exist { + pathType := fmt.Sprintf("%s.%s", p.Path(), typ) + c.Bindings[typ] = &TypeBinding{ + Type: pathType, + } + } + } + } + } + } + } + return nil } diff --git a/generate/generate_test.go b/generate/generate_test.go index 21320fe..be5088e 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -175,6 +175,11 @@ func TestGenerateWithConfig(t *testing.T) { StructReferences: true, Generated: "generated-structrefs.go", }}, + {"PackageBindings", "", nil, &Config{ + PackageBindings: []*PackageBinding{ + {Package: "github.com/Khan/genqlient/internal/testutil"}, + }, + }}, {"NoContext", "", nil, &Config{ Generated: "generated.go", ContextType: "-", diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindings-testdata-queries b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindings-testdata-queries new file mode 100644 index 0000000..aa42491 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-PackageBindings-testdata-queries @@ -0,0 +1,65 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package testdata + +import ( + "context" + + "github.com/Khan/genqlient/graphql" + "github.com/Khan/genqlient/internal/testutil" +) + +// 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 testutil.ID `json:"id"` +} + +// GetId returns SimpleQueryUser.Id, and is useful for accessing the field via an interface. +func (v *SimpleQueryUser) GetId() testutil.ID { return v.Id } + +func SimpleQuery( + ctx context.Context, + client graphql.Client, +) (*SimpleQueryResponse, error) { + req := &graphql.Request{ + OpName: "SimpleQuery", + Query: ` +query SimpleQuery { + user { + id + } +} +`, + } + var err error + + var data SimpleQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} +