Refactor argument-handling to use a struct (#103)

## Summary:
In this commit I refactor the argument-generation logic to move most of
the code out of the template and into the type-generator.  This logic
predates #51, and I didn't think to update it there, but I think it
benefits from similar treatment, for similar reasons.

Specifically, the main change is to treat variables as another struct
type we can generate, rather than handling them inline as a
`map[string]interface{}`.  Users still pass them the same way, but
instead of putting them into a `map[string]interface{}` and JSONifying
that, we generate a struct and put them there.

This turns out to simplify things quite a lot, because we already have a
lot of code to generate types.  Notably, the omitempty code goes from a
dozen lines to basically two, and fixes a bug (#43) in the process,
because now that we have a struct, `json.Marshal` will do our work for
us! (And, once we have syntax for it (#14), we'll be able to handle
field-level omitempty basically for free.)  More importantly, it will
simplify custom marshalers (#38, forthcoming) significantly, since we do
all that logic at the containing-struct level, but will need to apply it
to arguments.

It does require two breaking changes:

1. For folks implementing the `graphql.Client` API (rather than just
   calling `NewClient`): we now pass them variables as an `interface{}`
   rather than a `map[string]interface{}`.  For most callers, including
   Khan/webapp, this is basically a one-line change to the signature of
   their `MakeRequest`, and it should be a lot more future-proof.
2. genqlient's handling of the `omitempty` option has changed to match
   that of `encoding/json`, in particular it now never considers structs
   "empty".  The difference was never intentional (I just didn't realize
   that behavior of `encoding/json`); arguably our behavior was more
   useful but I think that's outweighed by the value of consistency with
   `encoding/json` as well as the simpler and more correct
   implementation (fixing #43 is actually quite nontrivial otherwise).
   Once we have custom unmarshaler support (#38), users will be able to
   map a zero value to JSON null if they wish, which is mostly if not
   entirely equivalent for GraphQL's purposes.

Issue: https://github.com/Khan/genqlient/issues/38
Issue: https://github.com/Khan/genqlient/issues/43

## Test plan:
make check

Author: benjaminjkraft

Reviewers: StevenACoffman, dnerdy, aberkan, jvoll, mahtabsabet, MiguelCastillo

Required Reviewers: 

Approved By: StevenACoffman, dnerdy

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint,  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14)

Pull Request URL: https://github.com/Khan/genqlient/pull/103
This commit is contained in:
Ben Kraft
2021-09-22 17:16:36 -07:00
committed by GitHub
parent ab1aaed845
commit 5995653583
26 changed files with 395 additions and 206 deletions
+71 -13
View File
@@ -7,7 +7,7 @@ package generate
// into code, in types.go.
//
// The entrypoints are convertOperation, which builds the response-type for a
// query, and convertInputType, which builds the argument-types.
// query, and convertArguments, which builds the argument-types.
import (
"fmt"
@@ -139,15 +139,66 @@ var builtinTypes = map[string]string{
"ID": "string",
}
// convertInputType decides the Go type we will generate corresponding to an
// argument to a GraphQL operation.
func (g *generator) convertInputType(
typ *ast.Type,
options, queryOptions *genqlientDirective,
) (goType, error) {
// note prefix is ignored here (see generator.typeName), as is selectionSet
// (for input types we use the whole thing).
return g.convertType(nil, typ, nil, options, queryOptions)
// convertArguments builds the type of the GraphQL arguments to the given
// operation.
//
// This type is not exposed to the user; it's just used internally in the
// unmarshaler; and it's used as a container
func (g *generator) convertArguments(
operation *ast.OperationDefinition,
queryOptions *genqlientDirective,
) (*goStructType, error) {
if len(operation.VariableDefinitions) == 0 {
return nil, nil
}
name := "__" + operation.Name + "Input"
fields := make([]*goStructField, len(operation.VariableDefinitions))
for i, arg := range operation.VariableDefinitions {
_, directive, err := g.parsePrecedingComment(arg, arg.Position)
if err != nil {
return nil, err
}
options := queryOptions.merge(directive)
goName := upperFirst(arg.Variable)
// Some of the arguments don't apply here, namely the name-prefix (see
// names.go) and the selection-set (we use all the input type's fields,
// and so on recursively). See also the `case ast.InputObject` in
// convertDefinition, below.
goTyp, err := g.convertType(nil, arg.Type, nil, options, queryOptions)
if err != nil {
return nil, err
}
fields[i] = &goStructField{
GoName: goName,
GoType: goTyp,
JSONName: arg.Variable,
GraphQLName: arg.Variable,
Omitempty: options.GetOmitempty(),
}
}
goTyp := &goStructType{
GoName: name,
Fields: fields,
Selection: nil,
IsInput: true,
descriptionInfo: descriptionInfo{
CommentOverride: fmt.Sprintf("%s is used internally by genqlient", name),
// fake name, used by addType
GraphQLName: name,
},
}
goTypAgain, err := g.addType(goTyp, goTyp.GoName, operation.Position)
if err != nil {
return nil, err
}
goTyp, ok := goTypAgain.(*goStructType)
if !ok {
return nil, errorf(
operation.Position, "internal error: input type was %T", goTypAgain)
}
return goTyp, nil
}
// convertType decides the Go type we will generate corresponding to a
@@ -305,11 +356,16 @@ func (g *generator) convertDefinition(
for i, field := range def.Fields {
goName := upperFirst(field.Name)
// Several of the arguments don't really make sense here:
// Several of the arguments don't really make sense here
// (note field.Type is necessarily a scalar, input, or enum)
// - no field-specific options can apply, because this is
// a field in the type, not in the query (see also #14).
// - namePrefix is ignored for input types; see note in
// generator.typeName.
// - namePrefix is ignored for input types and enums (see
// names.go) and for scalars (they use client-specified
// names)
// - selectionSet is ignored for input types, because we
// just use all fields of the type; and it's nonexistent
// for scalars and enums, our only other possible types,
// TODO(benkraft): Can we refactor to avoid passing the values that
// will be ignored? We know field.Type is a scalar, enum, or input
// type. But plumbing that is a bit tricky in practice.
@@ -325,6 +381,8 @@ func (g *generator) convertDefinition(
JSONName: field.Name,
GraphQLName: field.Name,
Description: field.Description,
// TODO(benkraft): set Omitempty once we have a way for the
// user to specify it.
}
}
return goType, nil