Repository: Khan/genqlient
Pull request #169 (2022-02-07 14:50:42)

Add configuration AutoBindings method
This proposal is same idea config file: `gqlgen AutoBind`.

My personal issue is,

>  I server generate by `gqlgen`, and model is created by generator. 

Without `auto_bindings`, `genqlient` make duplicated structure. 

And i 300 types, i can't manual binds.

This change: 

- Add in config file `auto_bindings` string
- Add in exemple file.


benjaminjkraft: Ah, one more thing: assuming we do go forward with it this will need some tests! I think the easiest thing is probably to add it to `TestGenerateWithConfig` in `generate/generate_test.go` -- perhaps just point it to the gqlgen models in `internal/integration/server`.
benjaminjkraft: Thanks for writing back!  If I understand correctly, I guess what you're doing is to have your server act as a proxy:
```
end-user --> your server --> dgraph
```
so your server wants to be both a client and a server of the (mostly-?)same GraphQL schema.  It's hard to say if genqlient will be the best client for you, without knowing more, but this is at least helpful to understand, and I can see why you'd want this.  It sounds in some ways similar to building a gateway, so I want to cc @dnerdy to see if he has any thoughts on gotchas or whether this is a sensible thing, but my inclination is since it's not too invasive to implement it's fine to err on the side of adding it.  (Or we could decide it's time for plugins, but that's a more complicated question and probably it's not.)

I agree it's not important to support all possible things people might want out of such a feature in the first version.  The main thing I'm concerned about is just avoiding breaking changes later; thus the structure of config I suggested.  It's fine if for now we can only allow one package, although I suspect if we do it fairly naively it's not hard to allow several (just loop over them).
benjaminjkraft: Thanks for the updates! I've pushed a few updates mostly to the documentation; the only thing remaining is if you could sign the [Khan Academy CLA](https://www.khanacademy.org/r/cla), then we can merge this.
NuVivo314: Hello, 

I resolve minor merge conflit. 

Ready for merge ?

Thx. 

Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 692d7dd..110b14f 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -22,6 +22,16 @@ When releasing a new version:
 
 ### Breaking changes:
 
+### New features:
+
+### Bug fixes:
+
+## v0.5.0
+
+Version 0.5.0 adds several new configuration options and convenience features. Note that genqlient now requires Go 1.16 or higher, and is tested through Go 1.18.
+
+### Breaking changes:
+
 - genqlient now requires Go 1.16 or higher.
 - The [`graphql.Client`](https://pkg.go.dev/github.com/Khan/genqlient/graphql#Client) interface now accepts two structs for the request and response, to allow future expansion, rather than several individual arguments.  Clients implementing the interface themselves will need to change the signature; clients who simply call `graphql.NewClient` are unaffected.
 
@@ -30,6 +40,7 @@ When releasing a new version:
 - 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.go b/generate/generate.go
index 6af6cf1..cb5acf4 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -228,17 +228,10 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
 // considers valid but we don't allow, and returns an error if this operation
 // is invalid for genqlient's purposes.
 func (g *generator) validateOperation(op *ast.OperationDefinition) error {
-	opType, err := g.baseTypeForOperation(op.Operation)
-	switch {
-	case err != nil:
+	_, err := g.baseTypeForOperation(op.Operation)
+	if err != nil {
 		// (e.g. operation has subscriptions, which we don't support)
 		return err
-	case opType == nil:
-		// gqlparser should err here, but doesn't [1], so we err to prevent
-		// panics later.
-		// TODO(benkraft): Remove once gqlparser is fixed.
-		// [1] https://github.com/vektah/gqlparser/issues/221
-		return errorf(op.Position, "schema has no %v type", op.Operation)
 	}
 
 	if op.Name == "" {
diff --git a/go.mod b/go.mod
index 14462c8..1531ea4 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/alexflint/go-arg v1.4.2
 	github.com/bradleyjkemp/cupaloy/v2 v2.6.0
 	github.com/stretchr/testify v1.7.0
-	github.com/vektah/gqlparser/v2 v2.4.1
+	github.com/vektah/gqlparser/v2 v2.4.5
 	golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect
 	golang.org/x/tools v0.1.10
 	gopkg.in/yaml.v2 v2.4.0
diff --git a/go.sum b/go.sum
index 6676044..e519639 100644
--- a/go.sum
+++ b/go.sum
@@ -55,8 +55,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
-github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
-github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
+github.com/vektah/gqlparser/v2 v2.4.5 h1:C02NsyEsL4TXJB7ndonqTfuQOL4XPIu0aAWugdmTgmc=
+github.com/vektah/gqlparser/v2 v2.4.5/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=


Test patch:
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/TestGenerateErrors-NoMutationType-graphql b/generate/testdata/snapshots/TestGenerateErrors-NoMutationType-graphql
index 2c40a67..573149a 100644
--- a/generate/testdata/snapshots/TestGenerateErrors-NoMutationType-graphql
+++ b/generate/testdata/snapshots/TestGenerateErrors-NoMutationType-graphql
@@ -1,1 +1,1 @@
-testdata/errors/NoMutationType.graphql:1: schema has no mutation type
+testdata/errors/NoMutationType.graphql:1: query-spec does not match schema: Schema does not support operation type "mutation"
diff --git a/generate/testdata/snapshots/TestGenerateErrors-NoQueryType-graphql b/generate/testdata/snapshots/TestGenerateErrors-NoQueryType-graphql
index aa00a56..779db1b 100644
--- a/generate/testdata/snapshots/TestGenerateErrors-NoQueryType-graphql
+++ b/generate/testdata/snapshots/TestGenerateErrors-NoQueryType-graphql
@@ -1,1 +1,1 @@
-testdata/errors/NoQueryType.graphql:1: schema has no query type
+testdata/errors/NoQueryType.graphql:1: query-spec does not match schema: Schema does not support operation type "query"
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
+}
+