From 093054ef32191147a9d9c2df514bdd17e0189da8 Mon Sep 17 00:00:00 2001 From: NuVivo314 Date: Fri, 17 Jun 2022 20:42:49 +0200 Subject: [PATCH] Add new package bindings option (#169) Add new package bindings option This commit adds list of packages for which genqlient should automatically generate bindings; it's equivalent to adding all the exported types in the package to `bindings` explicitly. This can be useful when you're both a client and a server of the same schema and want to share types. We don't recommend doing things that way, but the feature isn't too invasive and may be useful for other purposes. Co-authored-by: Ben Kraft --- docs/CHANGELOG.md | 1 + docs/genqlient.yaml | 12 +++- generate/config.go | 46 +++++++++++++ generate/generate_test.go | 5 ++ ...ithConfig-PackageBindings-testdata-queries | 65 +++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 generate/testdata/snapshots/TestGenerateWithConfig-PackageBindings-testdata-queries 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 +} +