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
+4
View File
@@ -22,10 +22,14 @@ When releasing a new version:
### Breaking changes: ### Breaking changes:
- The [`graphql.Client`](https://pkg.go.dev/github.com/Khan/genqlient/graphql#Client) interface now accepts `variables interface{}` (containing a JSON-marshalable value) rather than `variables map[string]interface{}`. Clients implementing the interface themselves will need to change the signature; clients who simply call `graphql.NewClient` are unaffected.
- genqlient's handling of the `omitempty` option has changed to match that of `encoding/json`, from which it had inadvertently differed. In particular, this means struct-typed arguments with `# @genqlient(omitempty: true)` will no longer be omitted if they are the zero value. (Struct-pointers are still omitted if nil, so adding `pointer: true` will typically work fine.)
### New features: ### New features:
### Bug fixes: ### Bug fixes:
- The `omitempty` option now works correctly for struct- and map-typed variables, matching `encoding/json`, which is to say it never omits structs, and omits empty maps. (#43)
- Generated type-names now abbreviate across multiple components; for example if the path to a type is `(MyOperation, Outer, Outer, Inner, OuterInner)`, it will again be called `MyOperationOuterInner`. (This regressed in a pre-v0.1.0 refactor.) (#109) - Generated type-names now abbreviate across multiple components; for example if the path to a type is `(MyOperation, Outer, Outer, Inner, OuterInner)`, it will again be called `MyOperationOuterInner`. (This regressed in a pre-v0.1.0 refactor.) (#109)
## v0.1.0 ## v0.1.0
+3 -2
View File
@@ -32,8 +32,9 @@
# query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2. # query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2.
directive genqlient( directive genqlient(
# If set, this argument will be omitted if it's equal to its Go zero # If set, this argument will be omitted if it has an empty value, defined
# value, or is an empty slice. # (the same as in encoding/json) as false, 0, a nil pointer, a nil interface
# value, and any empty array, slice, map, or string.
# #
# For example, given the following query: # For example, given the following query:
# # @genqlient(omitempty: true) # # @genqlient(omitempty: true)
+9 -5
View File
@@ -9,6 +9,11 @@ import (
"github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/graphql"
) )
// __getUserInput is used internally by genqlient
type __getUserInput struct {
Login string `json:"Login"`
}
// getUserResponse is returned by getUser on success. // getUserResponse is returned by getUser on success.
type getUserResponse struct { type getUserResponse struct {
// Lookup a user by login. // Lookup a user by login.
@@ -71,12 +76,11 @@ query getViewer {
func getUser( func getUser(
ctx context.Context, ctx context.Context,
client graphql.Client, client graphql.Client,
login string, Login string,
) (*getUserResponse, error) { ) (*getUserResponse, error) {
variables := map[string]interface{}{ __input := __getUserInput{
"Login": login, Login: Login,
} }
var err error var err error
var retval getUserResponse var retval getUserResponse
@@ -92,7 +96,7 @@ query getUser ($Login: String!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
+71 -13
View File
@@ -7,7 +7,7 @@ package generate
// into code, in types.go. // into code, in types.go.
// //
// The entrypoints are convertOperation, which builds the response-type for a // 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 ( import (
"fmt" "fmt"
@@ -139,15 +139,66 @@ var builtinTypes = map[string]string{
"ID": "string", "ID": "string",
} }
// convertInputType decides the Go type we will generate corresponding to an // convertArguments builds the type of the GraphQL arguments to the given
// argument to a GraphQL operation. // operation.
func (g *generator) convertInputType( //
typ *ast.Type, // This type is not exposed to the user; it's just used internally in the
options, queryOptions *genqlientDirective, // unmarshaler; and it's used as a container
) (goType, error) { func (g *generator) convertArguments(
// note prefix is ignored here (see generator.typeName), as is selectionSet operation *ast.OperationDefinition,
// (for input types we use the whole thing). queryOptions *genqlientDirective,
return g.convertType(nil, typ, nil, options, queryOptions) ) (*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 // 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 { for i, field := range def.Fields {
goName := upperFirst(field.Name) 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 // - no field-specific options can apply, because this is
// a field in the type, not in the query (see also #14). // a field in the type, not in the query (see also #14).
// - namePrefix is ignored for input types; see note in // - namePrefix is ignored for input types and enums (see
// generator.typeName. // 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 // TODO(benkraft): Can we refactor to avoid passing the values that
// will be ignored? We know field.Type is a scalar, enum, or input // will be ignored? We know field.Type is a scalar, enum, or input
// type. But plumbing that is a bit tricky in practice. // type. But plumbing that is a bit tricky in practice.
@@ -325,6 +381,8 @@ func (g *generator) convertDefinition(
JSONName: field.Name, JSONName: field.Name,
GraphQLName: field.Name, GraphQLName: field.Name,
Description: field.Description, Description: field.Description,
// TODO(benkraft): set Omitempty once we have a way for the
// user to specify it.
} }
} }
return goType, nil return goType, nil
+9 -40
View File
@@ -55,8 +55,11 @@ type operation struct {
Doc string `json:"-"` Doc string `json:"-"`
// The body of the operation to send. // The body of the operation to send.
Body string `json:"query"` Body string `json:"query"`
// The arguments to the operation. // The type of the argument to the operation, which we use both internally
Args []argument `json:"-"` // and to construct the arguments. We do it this way so we can use the
// machinery we have for handling (and, specifically, json-marshaling)
// types.
Input *goStructType `json:"-"`
// The type-name for the operation's response type. // The type-name for the operation's response type.
ResponseName string `json:"-"` ResponseName string `json:"-"`
// The original filename from which we got this query. // The original filename from which we got this query.
@@ -69,14 +72,6 @@ type exportedOperations struct {
Operations []*operation `json:"operations"` Operations []*operation `json:"operations"`
} }
type argument struct {
GoName string
GoType string
GraphQLName string
IsSlice bool
Options *genqlientDirective
}
func newGenerator( func newGenerator(
config *Config, config *Config,
schema *ast.Schema, schema *ast.Schema,
@@ -125,29 +120,6 @@ func (g *generator) WriteTypes(w io.Writer) error {
return nil return nil
} }
func (g *generator) getArgument(
arg *ast.VariableDefinition,
operationDirective *genqlientDirective,
) (argument, error) {
_, directive, err := g.parsePrecedingComment(arg, arg.Position)
if err != nil {
return argument{}, err
}
graphQLName := arg.Variable
goTyp, err := g.convertInputType(arg.Type, directive, operationDirective)
if err != nil {
return argument{}, err
}
return argument{
GraphQLName: graphQLName,
GoName: lowerFirst(graphQLName),
GoType: goTyp.Reference(),
IsSlice: arg.Type.Elem != nil,
Options: operationDirective.merge(directive),
}, nil
}
// usedFragmentNames returns the named-fragments used by (i.e. spread into) // usedFragmentNames returns the named-fragments used by (i.e. spread into)
// this operation. // this operation.
func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList { func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList {
@@ -277,12 +249,9 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
return err return err
} }
args := make([]argument, len(op.VariableDefinitions)) inputType, err := g.convertArguments(op, directive)
for i, arg := range op.VariableDefinitions { if err != nil {
args[i], err = g.getArgument(arg, directive) return err
if err != nil {
return err
}
} }
responseType, err := g.convertOperation(op, directive) responseType, err := g.convertOperation(op, directive)
@@ -313,7 +282,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
// rather than in the template so exported operations will match // rather than in the template so exported operations will match
// *exactly* what we send to the server. // *exactly* what we send to the server.
Body: "\n" + builder.String(), Body: "\n" + builder.String(),
Args: args, Input: inputType,
ResponseName: responseType.Reference(), ResponseName: responseType.Reference(),
SourceFilename: sourceFilename, SourceFilename: sourceFilename,
Config: g.Config, // for the convenience of the template Config: g.Config, // for the convenience of the template
-3
View File
@@ -108,9 +108,6 @@ func TestGenerate(t *testing.T) {
t.Run("Build", func(t *testing.T) { t.Run("Build", func(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping build due to -short") t.Skip("skipping build due to -short")
} else if sourceFilename == "Omitempty.graphql" {
t.Skip("TODO: enable after fixing " +
"https://github.com/Khan/genqlient/issues/43")
} }
err := buildGoFile(sourceFilename, generated[goFilename]) err := buildGoFile(sourceFilename, generated[goFilename])
+14 -23
View File
@@ -6,32 +6,23 @@ func {{.Name}}(
{{- if not .Config.ClientGetter -}} {{- if not .Config.ClientGetter -}}
client {{ref "github.com/Khan/genqlient/graphql.Client"}}, client {{ref "github.com/Khan/genqlient/graphql.Client"}},
{{end}} {{end}}
{{- range .Args -}} {{- if .Input -}}
{{.GoName}} {{.GoType}}, {{- range .Input.Fields -}}
{{/* the GraphQL name here is the user-specified variable-name */ -}}
{{.GraphQLName}} {{.GoType.Reference}},
{{end -}}
{{end -}} {{end -}}
) (*{{.ResponseName}}, error) { ) (*{{.ResponseName}}, error) {
{{- if .Args -}} {{- if .Input -}}
variables := map[string]interface{}{ {{/* We need to avoid conflicting with any of the function's argument names
{{range .Args -}} which are derived from the GraphQL argument names; notably `input` is
{{if not .Options.GetOmitempty -}} a common one. So we use a name that's not legal in GraphQL, namely
"{{.GraphQLName}}": {{.GoName}}, one starting with a double-underscore. */ -}}
__input := {{.Input.GoName}}{
{{range .Input.Fields -}}
{{.GoName}}: {{.GraphQLName}},
{{end -}} {{end -}}
{{end}}
} }
{{range .Args -}}
{{if .Options.GetOmitempty -}}
{{if .IsSlice -}}
if len({{.GoName}}) > 0 {
{{else -}}
{{/* zero_{{.GoType}} would be a better name, but {{.GoType}} would require
munging since it might be, say, `time.Time`. */}}
var zero_{{.GoName}} {{.GoType}}
if {{.GoName}} != zero_{{.GoName}} {
{{end -}}
variables["{{.GraphQLName}}"] = {{.GoName}}
}
{{end}}
{{end}}
{{end -}} {{end -}}
var err error var err error
@@ -48,7 +39,7 @@ func {{.Name}}(
"{{.Name}}", "{{.Name}}",
`{{.Body}}`, `{{.Body}}`,
&retval, &retval,
{{if .Args}}variables{{else}}nil{{end}}, {{if .Input}}&__input{{else}}nil{{end}},
) )
return &retval, err return &retval, err
} }
@@ -8,6 +8,12 @@ import (
"github.com/Khan/genqlient/graphql" "github.com/Khan/genqlient/graphql"
) )
// __convertTimezoneInput is used internally by genqlient
type __convertTimezoneInput struct {
Dt time.Time `json:"dt"`
Tz string `json:"tz"`
}
// convertTimezoneResponse is returned by convertTimezone on success. // convertTimezoneResponse is returned by convertTimezone on success.
type convertTimezoneResponse struct { type convertTimezoneResponse struct {
Convert time.Time `json:"convert"` Convert time.Time `json:"convert"`
@@ -18,11 +24,10 @@ func convertTimezone(
dt time.Time, dt time.Time,
tz string, tz string,
) (*convertTimezoneResponse, error) { ) (*convertTimezoneResponse, error) {
variables := map[string]interface{}{ __input := __convertTimezoneInput{
"dt": dt, Dt: dt,
"tz": tz, Tz: tz,
} }
var err error var err error
var retval convertTimezoneResponse var retval convertTimezoneResponse
@@ -35,7 +40,7 @@ query convertTimezone ($dt: DateTime!, $tz: String) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -38,14 +38,18 @@ const (
RoleTeacher Role = "TEACHER" RoleTeacher Role = "TEACHER"
) )
// __InputEnumQueryInput is used internally by genqlient
type __InputEnumQueryInput struct {
Role Role `json:"role"`
}
func InputEnumQuery( func InputEnumQuery(
client graphql.Client, client graphql.Client,
role Role, role Role,
) (*InputEnumQueryResponse, error) { ) (*InputEnumQueryResponse, error) {
variables := map[string]interface{}{ __input := __InputEnumQueryInput{
"role": role, Role: role,
} }
var err error var err error
var retval InputEnumQueryResponse var retval InputEnumQueryResponse
@@ -60,7 +64,7 @@ query InputEnumQuery ($role: Role!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -56,14 +56,18 @@ type UserQueryInput struct {
HasPokemon testutil.Pokemon `json:"hasPokemon"` HasPokemon testutil.Pokemon `json:"hasPokemon"`
} }
// __InputObjectQueryInput is used internally by genqlient
type __InputObjectQueryInput struct {
Query UserQueryInput `json:"query"`
}
func InputObjectQuery( func InputObjectQuery(
client graphql.Client, client graphql.Client,
query UserQueryInput, query UserQueryInput,
) (*InputObjectQueryResponse, error) { ) (*InputObjectQueryResponse, error) {
variables := map[string]interface{}{ __input := __InputObjectQueryInput{
"query": query, Query: query,
} }
var err error var err error
var retval InputObjectQueryResponse var retval InputObjectQueryResponse
@@ -78,7 +82,7 @@ query InputObjectQuery ($query: UserQueryInput) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -27,14 +27,18 @@ type ListInputQueryUser struct {
Id testutil.ID `json:"id"` Id testutil.ID `json:"id"`
} }
// __ListInputQueryInput is used internally by genqlient
type __ListInputQueryInput struct {
Names []string `json:"names"`
}
func ListInputQuery( func ListInputQuery(
client graphql.Client, client graphql.Client,
names []string, names []string,
) (*ListInputQueryResponse, error) { ) (*ListInputQueryResponse, error) {
variables := map[string]interface{}{ __input := __ListInputQueryInput{
"names": names, Names: names,
} }
var err error var err error
var retval ListInputQueryResponse var retval ListInputQueryResponse
@@ -49,7 +53,7 @@ query ListInputQuery ($names: [String]) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -72,6 +72,15 @@ type UserQueryInput struct {
HasPokemon testutil.Pokemon `json:"hasPokemon"` HasPokemon testutil.Pokemon `json:"hasPokemon"`
} }
// __OmitEmptyQueryInput is used internally by genqlient
type __OmitEmptyQueryInput struct {
Query UserQueryInput `json:"query,omitempty"`
Queries []UserQueryInput `json:"queries,omitempty"`
Dt time.Time `json:"dt,omitempty"`
Tz string `json:"tz,omitempty"`
TzNoOmitEmpty string `json:"tzNoOmitEmpty"`
}
func OmitEmptyQuery( func OmitEmptyQuery(
client graphql.Client, client graphql.Client,
query UserQueryInput, query UserQueryInput,
@@ -80,29 +89,13 @@ func OmitEmptyQuery(
tz string, tz string,
tzNoOmitEmpty string, tzNoOmitEmpty string,
) (*OmitEmptyQueryResponse, error) { ) (*OmitEmptyQueryResponse, error) {
variables := map[string]interface{}{ __input := __OmitEmptyQueryInput{
"tzNoOmitEmpty": tzNoOmitEmpty, Query: query,
Queries: queries,
Dt: dt,
Tz: tz,
TzNoOmitEmpty: tzNoOmitEmpty,
} }
var zero_query UserQueryInput
if query != zero_query {
variables["query"] = query
}
if len(queries) > 0 {
variables["queries"] = queries
}
var zero_dt time.Time
if dt != zero_dt {
variables["dt"] = dt
}
var zero_tz string
if tz != zero_tz {
variables["tz"] = tz
}
var err error var err error
var retval OmitEmptyQueryResponse var retval OmitEmptyQueryResponse
@@ -122,7 +115,7 @@ query OmitEmptyQuery ($query: UserQueryInput, $queries: [UserQueryInput], $dt: D
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -79,18 +79,24 @@ type UserQueryInput struct {
HasPokemon *testutil.Pokemon `json:"hasPokemon"` HasPokemon *testutil.Pokemon `json:"hasPokemon"`
} }
// __PointersQueryInput is used internally by genqlient
type __PointersQueryInput struct {
Query *UserQueryInput `json:"query"`
Dt time.Time `json:"dt"`
Tz *string `json:"tz"`
}
func PointersQuery( func PointersQuery(
client graphql.Client, client graphql.Client,
query UserQueryInput, query *UserQueryInput,
dt time.Time, dt time.Time,
tz string, tz *string,
) (*PointersQueryResponse, error) { ) (*PointersQueryResponse, error) {
variables := map[string]interface{}{ __input := __PointersQueryInput{
"query": query, Query: query,
"dt": dt, Dt: dt,
"tz": tz, Tz: tz,
} }
var err error var err error
var retval PointersQueryResponse var retval PointersQueryResponse
@@ -113,7 +119,7 @@ query PointersQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -79,18 +79,24 @@ type UserQueryInput struct {
HasPokemon testutil.Pokemon `json:"hasPokemon"` HasPokemon testutil.Pokemon `json:"hasPokemon"`
} }
// __PointersQueryInput is used internally by genqlient
type __PointersQueryInput struct {
Query *UserQueryInput `json:"query"`
Dt *time.Time `json:"dt"`
Tz string `json:"tz"`
}
func PointersQuery( func PointersQuery(
client graphql.Client, client graphql.Client,
query *UserQueryInput, query *UserQueryInput,
dt *time.Time, dt *time.Time,
tz string, tz string,
) (*PointersQueryResponse, error) { ) (*PointersQueryResponse, error) {
variables := map[string]interface{}{ __input := __PointersQueryInput{
"query": query, Query: query,
"dt": dt, Dt: dt,
"tz": tz, Tz: tz,
} }
var err error var err error
var retval PointersQueryResponse var retval PointersQueryResponse
@@ -113,7 +119,7 @@ query PointersQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -37,14 +37,18 @@ type GetPokemonSiblingsUserGenqlientPokemon struct {
Level int `json:"level"` Level int `json:"level"`
} }
// __GetPokemonSiblingsInput is used internally by genqlient
type __GetPokemonSiblingsInput struct {
Input testutil.Pokemon `json:"input"`
}
func GetPokemonSiblings( func GetPokemonSiblings(
client graphql.Client, client graphql.Client,
input testutil.Pokemon, input testutil.Pokemon,
) (*GetPokemonSiblingsResponse, error) { ) (*GetPokemonSiblingsResponse, error) {
variables := map[string]interface{}{ __input := __GetPokemonSiblingsInput{
"input": input, Input: input,
} }
var err error var err error
var retval GetPokemonSiblingsResponse var retval GetPokemonSiblingsResponse
@@ -69,7 +73,7 @@ query GetPokemonSiblings ($input: PokemonInput!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -36,14 +36,18 @@ type RecursiveInput struct {
Rec []RecursiveInput `json:"rec"` Rec []RecursiveInput `json:"rec"`
} }
// __RecursionInput is used internally by genqlient
type __RecursionInput struct {
Input RecursiveInput `json:"input"`
}
func Recursion( func Recursion(
client graphql.Client, client graphql.Client,
input RecursiveInput, input RecursiveInput,
) (*RecursionResponse, error) { ) (*RecursionResponse, error) {
variables := map[string]interface{}{ __input := __RecursionInput{
"input": input, Input: input,
} }
var err error var err error
var retval RecursionResponse var retval RecursionResponse
@@ -64,7 +68,7 @@ query Recursion ($input: RecursiveInput!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -27,14 +27,18 @@ type SimpleInputQueryUser struct {
Id testutil.ID `json:"id"` Id testutil.ID `json:"id"`
} }
// __SimpleInputQueryInput is used internally by genqlient
type __SimpleInputQueryInput struct {
Name string `json:"name"`
}
func SimpleInputQuery( func SimpleInputQuery(
client graphql.Client, client graphql.Client,
name string, name string,
) (*SimpleInputQueryResponse, error) { ) (*SimpleInputQueryResponse, error) {
variables := map[string]interface{}{ __input := __SimpleInputQueryInput{
"name": name, Name: name,
} }
var err error var err error
var retval SimpleInputQueryResponse var retval SimpleInputQueryResponse
@@ -49,7 +53,7 @@ query SimpleInputQuery ($name: String!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -24,6 +24,11 @@ type SimpleMutationResponse struct {
CreateUser SimpleMutationCreateUser `json:"createUser"` CreateUser SimpleMutationCreateUser `json:"createUser"`
} }
// __SimpleMutationInput is used internally by genqlient
type __SimpleMutationInput struct {
Name string `json:"name"`
}
// SimpleMutation creates a user. // SimpleMutation creates a user.
// //
// It has a long doc-comment, to test that we handle that correctly. // It has a long doc-comment, to test that we handle that correctly.
@@ -32,10 +37,9 @@ func SimpleMutation(
client graphql.Client, client graphql.Client,
name string, name string,
) (*SimpleMutationResponse, error) { ) (*SimpleMutationResponse, error) {
variables := map[string]interface{}{ __input := __SimpleMutationInput{
"name": name, Name: name,
} }
var err error var err error
var retval SimpleMutationResponse var retval SimpleMutationResponse
@@ -51,7 +55,7 @@ mutation SimpleMutation ($name: String!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -36,6 +36,11 @@ type UserQueryInput struct {
HasPokemon testutil.Pokemon `json:"hasPokemon"` HasPokemon testutil.Pokemon `json:"hasPokemon"`
} }
// __unexportedInput is used internally by genqlient
type __unexportedInput struct {
Query UserQueryInput `json:"query"`
}
// unexportedResponse is returned by unexported on success. // unexportedResponse is returned by unexported on success.
type unexportedResponse struct { type unexportedResponse struct {
// user looks up a user by some stuff. // user looks up a user by some stuff.
@@ -60,10 +65,9 @@ func unexported(
client graphql.Client, client graphql.Client,
query UserQueryInput, query UserQueryInput,
) (*unexportedResponse, error) { ) (*unexportedResponse, error) {
variables := map[string]interface{}{ __input := __unexportedInput{
"query": query, Query: query,
} }
var err error var err error
var retval unexportedResponse var retval unexportedResponse
@@ -78,7 +82,7 @@ query unexported ($query: UserQueryInput) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
+12 -4
View File
@@ -139,6 +139,7 @@ type goStructField struct {
GoType goType GoType goType
JSONName string // i.e. the field's alias in this query JSONName string // i.e. the field's alias in this query
GraphQLName string // i.e. the field's name in its type-def GraphQLName string // i.e. the field's name in its type-def
Omitempty bool // only used on input types
Description string Description string
} }
@@ -162,19 +163,23 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
fmt.Fprintf(w, "type %s struct {\n", typ.GoName) fmt.Fprintf(w, "type %s struct {\n", typ.GoName)
for _, field := range typ.Fields { for _, field := range typ.Fields {
writeDescription(w, field.Description) writeDescription(w, field.Description)
jsonName := field.JSONName jsonTag := `"` + field.JSONName
if field.Omitempty {
jsonTag += ",omitempty"
}
jsonTag += `"`
if field.IsAbstract() { if field.IsAbstract() {
// abstract types are handled in our UnmarshalJSON (see below) // abstract types are handled in our UnmarshalJSON (see below)
needUnmarshaler = true needUnmarshaler = true
jsonName = "-" jsonTag = `"-"`
} }
if field.IsEmbedded() { if field.IsEmbedded() {
// embedded fields also need UnmarshalJSON handling (see below) // embedded fields also need UnmarshalJSON handling (see below)
needUnmarshaler = true needUnmarshaler = true
fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference()) fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference())
} else { } else {
fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n", fmt.Fprintf(w, "\t%s %s `json:%s`\n",
field.GoName, field.GoType.Reference(), jsonName) field.GoName, field.GoType.Reference(), jsonTag)
} }
} }
fmt.Fprintf(w, "}\n") fmt.Fprintf(w, "}\n")
@@ -198,6 +203,9 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
// select the same field, or several fragments select the same field -- the // select the same field, or several fragments select the same field -- the
// JSON library will only fill one of those (the least-nested one); we want // JSON library will only fill one of those (the least-nested one); we want
// to fill them all. // to fill them all.
//
// TODO(benkraft): If/when proposal #5901 is implemented (Go 1.18 at the
// earliest), we may be able to do some of this a simpler way.
if !needUnmarshaler { if !needUnmarshaler {
return nil return nil
} }
+8 -8
View File
@@ -24,9 +24,10 @@ type Client interface {
// context.Background(). // context.Background().
// //
// query is the literal string representing the GraphQL query, e.g. // query is the literal string representing the GraphQL query, e.g.
// `query myQuery { myField }`. variables contains the GraphQL variables // `query myQuery { myField }`. variables contains a JSON-marshalable
// to be sent along with the query, or may be nil if there are none. // value containing the variables to be sent along with the query,
// Typically, GraphQL APIs will accept a JSON payload of the form // or may be nil if there are none. Typically, GraphQL APIs will
// accept a JSON payload of the form
// {"query": "query myQuery { ... }", "variables": {...}}` // {"query": "query myQuery { ... }", "variables": {...}}`
// but MakeRequest may use some other transport, handle extensions, or set // but MakeRequest may use some other transport, handle extensions, or set
// other parameters, if it wishes. // other parameters, if it wishes.
@@ -41,8 +42,7 @@ type Client interface {
ctx context.Context, ctx context.Context,
opName string, opName string,
query string, query string,
retval interface{}, input, retval interface{},
variables map[string]interface{},
) error ) error
} }
@@ -69,8 +69,8 @@ func NewClient(endpoint string, httpClient *http.Client) Client {
} }
type payload struct { type payload struct {
Query string `json:"query"` Query string `json:"query"`
Variables map[string]interface{} `json:"variables,omitempty"` Variables interface{} `json:"variables,omitempty"`
// OpName is only required if there are multiple queries in the document, // OpName is only required if there are multiple queries in the document,
// but we set it unconditionally, because that's easier. // but we set it unconditionally, because that's easier.
OpName string `json:"operationName"` OpName string `json:"operationName"`
@@ -81,7 +81,7 @@ type response struct {
Errors gqlerror.List `json:"errors"` Errors gqlerror.List `json:"errors"`
} }
func (c *client) MakeRequest(ctx context.Context, opName string, query string, retval interface{}, variables map[string]interface{}) error { func (c *client) MakeRequest(ctx context.Context, opName string, query string, retval interface{}, variables interface{}) error {
body, err := json.Marshal(payload{ body, err := json.Marshal(payload{
Query: query, Query: query,
Variables: variables, Variables: variables,
+94 -24
View File
@@ -266,6 +266,41 @@ func (v *UserFields) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// __queryWithFragmentsInput is used internally by genqlient
type __queryWithFragmentsInput struct {
Ids []string `json:"ids"`
}
// __queryWithInterfaceListFieldInput is used internally by genqlient
type __queryWithInterfaceListFieldInput struct {
Ids []string `json:"ids"`
}
// __queryWithInterfaceListPointerFieldInput is used internally by genqlient
type __queryWithInterfaceListPointerFieldInput struct {
Ids []string `json:"ids"`
}
// __queryWithInterfaceNoFragmentsInput is used internally by genqlient
type __queryWithInterfaceNoFragmentsInput struct {
Id string `json:"id"`
}
// __queryWithNamedFragmentsInput is used internally by genqlient
type __queryWithNamedFragmentsInput struct {
Ids []string `json:"ids"`
}
// __queryWithOmitemptyInput is used internally by genqlient
type __queryWithOmitemptyInput struct {
Id string `json:"id,omitempty"`
}
// __queryWithVariablesInput is used internally by genqlient
type __queryWithVariablesInput struct {
Id string `json:"id"`
}
// failingQueryMeUser includes the requested fields of the GraphQL type User. // failingQueryMeUser includes the requested fields of the GraphQL type User.
type failingQueryMeUser struct { type failingQueryMeUser struct {
Id string `json:"id"` Id string `json:"id"`
@@ -1036,6 +1071,18 @@ func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// queryWithOmitemptyResponse is returned by queryWithOmitempty on success.
type queryWithOmitemptyResponse struct {
User queryWithOmitemptyUser `json:"user"`
}
// queryWithOmitemptyUser includes the requested fields of the GraphQL type User.
type queryWithOmitemptyUser struct {
Id string `json:"id"`
Name string `json:"name"`
LuckyNumber int `json:"luckyNumber"`
}
// queryWithVariablesResponse is returned by queryWithVariables on success. // queryWithVariablesResponse is returned by queryWithVariables on success.
type queryWithVariablesResponse struct { type queryWithVariablesResponse struct {
User queryWithVariablesUser `json:"user"` User queryWithVariablesUser `json:"user"`
@@ -1114,10 +1161,9 @@ func queryWithVariables(
client graphql.Client, client graphql.Client,
id string, id string,
) (*queryWithVariablesResponse, error) { ) (*queryWithVariablesResponse, error) {
variables := map[string]interface{}{ __input := __queryWithVariablesInput{
"id": id, Id: id,
} }
var err error var err error
var retval queryWithVariablesResponse var retval queryWithVariablesResponse
@@ -1134,7 +1180,36 @@ query queryWithVariables ($id: ID!) {
} }
`, `,
&retval, &retval,
variables, &__input,
)
return &retval, err
}
func queryWithOmitempty(
ctx context.Context,
client graphql.Client,
id string,
) (*queryWithOmitemptyResponse, error) {
__input := __queryWithOmitemptyInput{
Id: id,
}
var err error
var retval queryWithOmitemptyResponse
err = client.MakeRequest(
ctx,
"queryWithOmitempty",
`
query queryWithOmitempty ($id: ID) {
user(id: $id) {
id
name
luckyNumber
}
}
`,
&retval,
&__input,
) )
return &retval, err return &retval, err
} }
@@ -1144,10 +1219,9 @@ func queryWithInterfaceNoFragments(
client graphql.Client, client graphql.Client,
id string, id string,
) (*queryWithInterfaceNoFragmentsResponse, error) { ) (*queryWithInterfaceNoFragmentsResponse, error) {
variables := map[string]interface{}{ __input := __queryWithInterfaceNoFragmentsInput{
"id": id, Id: id,
} }
var err error var err error
var retval queryWithInterfaceNoFragmentsResponse var retval queryWithInterfaceNoFragmentsResponse
@@ -1168,7 +1242,7 @@ query queryWithInterfaceNoFragments ($id: ID!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -1178,10 +1252,9 @@ func queryWithInterfaceListField(
client graphql.Client, client graphql.Client,
ids []string, ids []string,
) (*queryWithInterfaceListFieldResponse, error) { ) (*queryWithInterfaceListFieldResponse, error) {
variables := map[string]interface{}{ __input := __queryWithInterfaceListFieldInput{
"ids": ids, Ids: ids,
} }
var err error var err error
var retval queryWithInterfaceListFieldResponse var retval queryWithInterfaceListFieldResponse
@@ -1198,7 +1271,7 @@ query queryWithInterfaceListField ($ids: [ID!]!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -1208,10 +1281,9 @@ func queryWithInterfaceListPointerField(
client graphql.Client, client graphql.Client,
ids []string, ids []string,
) (*queryWithInterfaceListPointerFieldResponse, error) { ) (*queryWithInterfaceListPointerFieldResponse, error) {
variables := map[string]interface{}{ __input := __queryWithInterfaceListPointerFieldInput{
"ids": ids, Ids: ids,
} }
var err error var err error
var retval queryWithInterfaceListPointerFieldResponse var retval queryWithInterfaceListPointerFieldResponse
@@ -1228,7 +1300,7 @@ query queryWithInterfaceListPointerField ($ids: [ID!]!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -1238,10 +1310,9 @@ func queryWithFragments(
client graphql.Client, client graphql.Client,
ids []string, ids []string,
) (*queryWithFragmentsResponse, error) { ) (*queryWithFragmentsResponse, error) {
variables := map[string]interface{}{ __input := __queryWithFragmentsInput{
"ids": ids, Ids: ids,
} }
var err error var err error
var retval queryWithFragmentsResponse var retval queryWithFragmentsResponse
@@ -1286,7 +1357,7 @@ query queryWithFragments ($ids: [ID!]!) {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
@@ -1296,10 +1367,9 @@ func queryWithNamedFragments(
client graphql.Client, client graphql.Client,
ids []string, ids []string,
) (*queryWithNamedFragmentsResponse, error) { ) (*queryWithNamedFragmentsResponse, error) {
variables := map[string]interface{}{ __input := __queryWithNamedFragmentsInput{
"ids": ids, Ids: ids,
} }
var err error var err error
var retval queryWithNamedFragmentsResponse var retval queryWithNamedFragmentsResponse
@@ -1344,7 +1414,7 @@ fragment MoreUserFields on User {
} }
`, `,
&retval, &retval,
variables, &__input,
) )
return &retval, err return &retval, err
} }
+27
View File
@@ -89,6 +89,33 @@ func TestVariables(t *testing.T) {
assert.Zero(t, resp.User) assert.Zero(t, resp.User)
} }
func TestOmitempty(t *testing.T) {
_ = `# @genqlient(omitempty: true)
query queryWithOmitempty($id: ID) {
user(id: $id) { id name luckyNumber }
}`
ctx := context.Background()
server := server.RunServer()
defer server.Close()
client := graphql.NewClient(server.URL, http.DefaultClient)
resp, err := queryWithOmitempty(ctx, client, "2")
require.NoError(t, err)
assert.Equal(t, "2", resp.User.Id)
assert.Equal(t, "Raven", resp.User.Name)
assert.Equal(t, -1, resp.User.LuckyNumber)
// should return default user, not the user with ID ""
resp, err = queryWithOmitempty(ctx, client, "")
require.NoError(t, err)
assert.Equal(t, "1", resp.User.Id)
assert.Equal(t, "Yours Truly", resp.User.Name)
assert.Equal(t, 17, resp.User.LuckyNumber)
}
func TestInterfaceNoFragments(t *testing.T) { func TestInterfaceNoFragments(t *testing.T) {
_ = `# @genqlient _ = `# @genqlient
query queryWithInterfaceNoFragments($id: ID!) { query queryWithInterfaceNoFragments($id: ID!) {
+1 -1
View File
@@ -1,6 +1,6 @@
type Query { type Query {
me: User me: User
user(id: ID!): User user(id: ID): User
being(id: ID!): Being being(id: ID!): Being
beings(ids: [ID!]!): [Being]! beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky lotteryWinner(number: Int!): Lucky
+22 -7
View File
@@ -64,7 +64,7 @@ type ComplexityRoot struct {
Fail func(childComplexity int) int Fail func(childComplexity int) int
LotteryWinner func(childComplexity int, number int) int LotteryWinner func(childComplexity int, number int) int
Me func(childComplexity int) int Me func(childComplexity int) int
User func(childComplexity int, id string) int User func(childComplexity int, id *string) int
} }
User struct { User struct {
@@ -77,7 +77,7 @@ type ComplexityRoot struct {
type QueryResolver interface { type QueryResolver interface {
Me(ctx context.Context) (*User, error) Me(ctx context.Context) (*User, error)
User(ctx context.Context, id string) (*User, error) User(ctx context.Context, id *string) (*User, error)
Being(ctx context.Context, id string) (Being, error) Being(ctx context.Context, id string) (Being, error)
Beings(ctx context.Context, ids []string) ([]Being, error) Beings(ctx context.Context, ids []string) ([]Being, error)
LotteryWinner(ctx context.Context, number int) (Lucky, error) LotteryWinner(ctx context.Context, number int) (Lucky, error)
@@ -208,7 +208,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.User(childComplexity, args["id"].(string)), true return e.complexity.Query.User(childComplexity, args["id"].(*string)), true
case "User.hair": case "User.hair":
if e.complexity.User.Hair == nil { if e.complexity.User.Hair == nil {
@@ -290,7 +290,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
var sources = []*ast.Source{ var sources = []*ast.Source{
{Name: "../schema.graphql", Input: `type Query { {Name: "../schema.graphql", Input: `type Query {
me: User me: User
user(id: ID!): User user(id: ID): User
being(id: ID!): Being being(id: ID!): Being
beings(ids: [ID!]!): [Being]! beings(ids: [ID!]!): [Being]!
lotteryWinner(number: Int!): Lucky lotteryWinner(number: Int!): Lucky
@@ -400,10 +400,10 @@ func (ec *executionContext) field_Query_lotteryWinner_args(ctx context.Context,
func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
var arg0 string var arg0 *string
if tmp, ok := rawArgs["id"]; ok { if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNID2string(ctx, tmp) arg0, err = ec.unmarshalOID2ᚖstring(ctx, tmp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -743,7 +743,7 @@ func (ec *executionContext) _Query_user(ctx context.Context, field graphql.Colle
fc.Args = args fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().User(rctx, args["id"].(string)) return ec.resolvers.Query().User(rctx, args["id"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@@ -3131,6 +3131,21 @@ func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋin
return ec._Hair(ctx, sel, v) return ec._Hair(ctx, sel, v)
} }
func (ec *executionContext) unmarshalOID2ᚖstring(ctx context.Context, v interface{}) (*string, error) {
if v == nil {
return nil, nil
}
res, err := graphql.UnmarshalID(v)
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOID2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return graphql.MarshalID(*v)
}
func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) { func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) {
if v == nil { if v == nil {
return nil, nil return nil, nil
+5 -2
View File
@@ -58,8 +58,11 @@ func (r *queryResolver) Me(ctx context.Context) (*User, error) {
return userByID("1"), nil return userByID("1"), nil
} }
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) { func (r *queryResolver) User(ctx context.Context, id *string) (*User, error) {
return userByID(id), nil if id == nil {
return userByID("1"), nil
}
return userByID(*id), nil
} }
func (r *queryResolver) Being(ctx context.Context, id string) (Being, error) { func (r *queryResolver) Being(ctx context.Context, id string) (Being, error) {