diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f4c2e740..1f897506 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,10 +22,13 @@ When releasing a new version: ### 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. + ### New features: ### Bug fixes: +- The `omitempty` option now works correctly for struct- and map-typed variables, which is to say it never considers them empty (matching `encoding/json`). (#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) ## v0.1.0 diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql index 5a845c1a..dc56a2be 100644 --- a/docs/genqlient_directive.graphql +++ b/docs/genqlient_directive.graphql @@ -32,8 +32,9 @@ # query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2. directive genqlient( - # If set, this argument will be omitted if it's equal to its Go zero - # value, or is an empty slice. + # If set, this argument will be omitted if it has an empty value, defined + # (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: # # @genqlient(omitempty: true) diff --git a/example/generated.go b/example/generated.go index 60086c4b..82f320ae 100644 --- a/example/generated.go +++ b/example/generated.go @@ -9,6 +9,11 @@ import ( "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. type getUserResponse struct { // Lookup a user by login. @@ -71,12 +76,11 @@ query getViewer { func getUser( ctx context.Context, client graphql.Client, - login string, + Login string, ) (*getUserResponse, error) { - variables := map[string]interface{}{ - "Login": login, + __input := __getUserInput{ + Login: Login, } - var err error var retval getUserResponse @@ -92,7 +96,7 @@ query getUser ($Login: String!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/convert.go b/generate/convert.go index 06271e8e..103ed361 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -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,63 @@ 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) + // note prefix is ignored here (see generator.typeName), as is + // selectionSet (for input types we use the whole thing). + goType, err := g.convertType(nil, arg.Type, nil, options, queryOptions) + if err != nil { + return nil, err + } + + fields[i] = &goStructField{ + GoName: goName, + GoType: goType, + JSONName: arg.Variable, + GraphQLName: arg.Variable, + Omitempty: options.GetOmitempty(), + } + } + goType := &goStructType{ + GoName: name, + Fields: fields, + Selection: nil, + descriptionInfo: descriptionInfo{ + CommentOverride: fmt.Sprintf("%s is used internally by genqlient", name), + // fake name, used by addType + GraphQLName: name, + }, + } + goTypeAgain, err := g.addType(goType, goType.GoName, operation.Position) + if err != nil { + return nil, err + } + goType, ok := goTypeAgain.(*goStructType) + if !ok { + return nil, errorf( + operation.Position, "internal error: input type was %T", goTypeAgain) + } + return goType, nil } // convertType decides the Go type we will generate corresponding to a @@ -325,6 +373,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 diff --git a/generate/generate.go b/generate/generate.go index 08f2ce5e..d40d244b 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -55,8 +55,11 @@ type operation struct { Doc string `json:"-"` // The body of the operation to send. Body string `json:"query"` - // The arguments to the operation. - Args []argument `json:"-"` + // The type of the argument to the operation, which we use both internally + // 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. ResponseName string `json:"-"` // The original filename from which we got this query. @@ -71,9 +74,8 @@ type exportedOperations struct { type argument struct { GoName string - GoType string + GoType goType GraphQLName string - IsSlice bool Options *genqlientDirective } @@ -125,29 +127,6 @@ func (g *generator) WriteTypes(w io.Writer) error { 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) // this operation. func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList { @@ -277,12 +256,9 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { return err } - args := make([]argument, len(op.VariableDefinitions)) - for i, arg := range op.VariableDefinitions { - args[i], err = g.getArgument(arg, directive) - if err != nil { - return err - } + inputType, err := g.convertArguments(op, directive) + if err != nil { + return err } responseType, err := g.convertOperation(op, directive) @@ -313,7 +289,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error { // rather than in the template so exported operations will match // *exactly* what we send to the server. Body: "\n" + builder.String(), - Args: args, + Input: inputType, ResponseName: responseType.Reference(), SourceFilename: sourceFilename, Config: g.Config, // for the convenience of the template diff --git a/generate/operation.go.tmpl b/generate/operation.go.tmpl index e5cadf68..80b0e43c 100644 --- a/generate/operation.go.tmpl +++ b/generate/operation.go.tmpl @@ -6,32 +6,20 @@ func {{.Name}}( {{- if not .Config.ClientGetter -}} client {{ref "github.com/Khan/genqlient/graphql.Client"}}, {{end}} - {{- range .Args -}} - {{.GoName}} {{.GoType}}, + {{- if .Input -}} + {{- range .Input.Fields -}} + {{/* the GraphQL name is here the user-specified variable-name */ -}} + {{.GraphQLName}} {{.GoType.Reference}}, + {{end -}} {{end -}} ) (*{{.ResponseName}}, error) { - {{- if .Args -}} - variables := map[string]interface{}{ - {{range .Args -}} - {{if not .Options.GetOmitempty -}} - "{{.GraphQLName}}": {{.GoName}}, + {{- if .Input -}} + {{/* __input is not a legal variable name in GraphQL */ -}} + __input := {{.Input.GoName}}{ + {{range .Input.Fields -}} + {{.GoName}}: {{.GraphQLName}}, {{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 -}} var err error @@ -48,7 +36,7 @@ func {{.Name}}( "{{.Name}}", `{{.Body}}`, &retval, - {{if .Args}}variables{{else}}nil{{end}}, + {{if .Input}}&__input{{else}}nil{{end}}, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-DateTime.graphql-DateTime.graphql.go b/generate/testdata/snapshots/TestGenerate-DateTime.graphql-DateTime.graphql.go index 66731735..adfc8399 100644 --- a/generate/testdata/snapshots/TestGenerate-DateTime.graphql-DateTime.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-DateTime.graphql-DateTime.graphql.go @@ -8,6 +8,12 @@ import ( "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. type convertTimezoneResponse struct { Convert time.Time `json:"convert"` @@ -18,11 +24,10 @@ func convertTimezone( dt time.Time, tz string, ) (*convertTimezoneResponse, error) { - variables := map[string]interface{}{ - "dt": dt, - "tz": tz, + __input := __convertTimezoneInput{ + Dt: dt, + Tz: tz, } - var err error var retval convertTimezoneResponse @@ -35,7 +40,7 @@ query convertTimezone ($dt: DateTime!, $tz: String) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-InputEnum.graphql-InputEnum.graphql.go b/generate/testdata/snapshots/TestGenerate-InputEnum.graphql-InputEnum.graphql.go index ba190bcb..32c2af80 100644 --- a/generate/testdata/snapshots/TestGenerate-InputEnum.graphql-InputEnum.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InputEnum.graphql-InputEnum.graphql.go @@ -38,14 +38,18 @@ const ( RoleTeacher Role = "TEACHER" ) +// __InputEnumQueryInput is used internally by genqlient +type __InputEnumQueryInput struct { + Role Role `json:"role"` +} + func InputEnumQuery( client graphql.Client, role Role, ) (*InputEnumQueryResponse, error) { - variables := map[string]interface{}{ - "role": role, + __input := __InputEnumQueryInput{ + Role: role, } - var err error var retval InputEnumQueryResponse @@ -60,7 +64,7 @@ query InputEnumQuery ($role: Role!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go b/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go index c64eed52..443070a5 100644 --- a/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go @@ -56,14 +56,18 @@ type UserQueryInput struct { HasPokemon testutil.Pokemon `json:"hasPokemon"` } +// __InputObjectQueryInput is used internally by genqlient +type __InputObjectQueryInput struct { + Query UserQueryInput `json:"query"` +} + func InputObjectQuery( client graphql.Client, query UserQueryInput, ) (*InputObjectQueryResponse, error) { - variables := map[string]interface{}{ - "query": query, + __input := __InputObjectQueryInput{ + Query: query, } - var err error var retval InputObjectQueryResponse @@ -78,7 +82,7 @@ query InputObjectQuery ($query: UserQueryInput) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-ListInput.graphql-ListInput.graphql.go b/generate/testdata/snapshots/TestGenerate-ListInput.graphql-ListInput.graphql.go index 359293e7..1f1c0cc6 100644 --- a/generate/testdata/snapshots/TestGenerate-ListInput.graphql-ListInput.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-ListInput.graphql-ListInput.graphql.go @@ -27,14 +27,18 @@ type ListInputQueryUser struct { Id testutil.ID `json:"id"` } +// __ListInputQueryInput is used internally by genqlient +type __ListInputQueryInput struct { + Names []string `json:"names"` +} + func ListInputQuery( client graphql.Client, names []string, ) (*ListInputQueryResponse, error) { - variables := map[string]interface{}{ - "names": names, + __input := __ListInputQueryInput{ + Names: names, } - var err error var retval ListInputQueryResponse @@ -49,7 +53,7 @@ query ListInputQuery ($names: [String]) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go index 4a904f0f..3b47ae35 100644 --- a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go @@ -72,6 +72,15 @@ type UserQueryInput struct { 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( client graphql.Client, query UserQueryInput, @@ -80,29 +89,13 @@ func OmitEmptyQuery( tz string, tzNoOmitEmpty string, ) (*OmitEmptyQueryResponse, error) { - variables := map[string]interface{}{ - "tzNoOmitEmpty": tzNoOmitEmpty, - } - - var zero_query UserQueryInput - if query != zero_query { - variables["query"] = query - } - - if len(queries) > 0 { - variables["queries"] = queries + __input := __OmitEmptyQueryInput{ + Query: query, + Queries: queries, + Dt: dt, + Tz: tz, + TzNoOmitEmpty: tzNoOmitEmpty, } - - 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 retval OmitEmptyQueryResponse @@ -122,7 +115,7 @@ query OmitEmptyQuery ($query: UserQueryInput, $queries: [UserQueryInput], $dt: D } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go b/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go index b6bbe8a5..96a2a2d6 100644 --- a/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go @@ -79,18 +79,24 @@ type UserQueryInput struct { 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( client graphql.Client, - query UserQueryInput, + query *UserQueryInput, dt time.Time, - tz string, + tz *string, ) (*PointersQueryResponse, error) { - variables := map[string]interface{}{ - "query": query, - "dt": dt, - "tz": tz, + __input := __PointersQueryInput{ + Query: query, + Dt: dt, + Tz: tz, } - var err error var retval PointersQueryResponse @@ -113,7 +119,7 @@ query PointersQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go b/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go index 67497e4a..8ba2afaf 100644 --- a/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go @@ -79,18 +79,24 @@ type UserQueryInput struct { 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( client graphql.Client, query *UserQueryInput, dt *time.Time, tz string, ) (*PointersQueryResponse, error) { - variables := map[string]interface{}{ - "query": query, - "dt": dt, - "tz": tz, + __input := __PointersQueryInput{ + Query: query, + Dt: dt, + Tz: tz, } - var err error var retval PointersQueryResponse @@ -113,7 +119,7 @@ query PointersQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-Pokemon.graphql-Pokemon.graphql.go b/generate/testdata/snapshots/TestGenerate-Pokemon.graphql-Pokemon.graphql.go index cc7e676e..f5f655a1 100644 --- a/generate/testdata/snapshots/TestGenerate-Pokemon.graphql-Pokemon.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-Pokemon.graphql-Pokemon.graphql.go @@ -37,14 +37,18 @@ type GetPokemonSiblingsUserGenqlientPokemon struct { Level int `json:"level"` } +// __GetPokemonSiblingsInput is used internally by genqlient +type __GetPokemonSiblingsInput struct { + Input testutil.Pokemon `json:"input"` +} + func GetPokemonSiblings( client graphql.Client, input testutil.Pokemon, ) (*GetPokemonSiblingsResponse, error) { - variables := map[string]interface{}{ - "input": input, + __input := __GetPokemonSiblingsInput{ + Input: input, } - var err error var retval GetPokemonSiblingsResponse @@ -69,7 +73,7 @@ query GetPokemonSiblings ($input: PokemonInput!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go index 02e2852f..af22c296 100644 --- a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go @@ -36,14 +36,18 @@ type RecursiveInput struct { Rec []RecursiveInput `json:"rec"` } +// __RecursionInput is used internally by genqlient +type __RecursionInput struct { + Input RecursiveInput `json:"input"` +} + func Recursion( client graphql.Client, input RecursiveInput, ) (*RecursionResponse, error) { - variables := map[string]interface{}{ - "input": input, + __input := __RecursionInput{ + Input: input, } - var err error var retval RecursionResponse @@ -64,7 +68,7 @@ query Recursion ($input: RecursiveInput!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInput.graphql-SimpleInput.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInput.graphql-SimpleInput.graphql.go index d9f3235c..4b400942 100644 --- a/generate/testdata/snapshots/TestGenerate-SimpleInput.graphql-SimpleInput.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-SimpleInput.graphql-SimpleInput.graphql.go @@ -27,14 +27,18 @@ type SimpleInputQueryUser struct { Id testutil.ID `json:"id"` } +// __SimpleInputQueryInput is used internally by genqlient +type __SimpleInputQueryInput struct { + Name string `json:"name"` +} + func SimpleInputQuery( client graphql.Client, name string, ) (*SimpleInputQueryResponse, error) { - variables := map[string]interface{}{ - "name": name, + __input := __SimpleInputQueryInput{ + Name: name, } - var err error var retval SimpleInputQueryResponse @@ -49,7 +53,7 @@ query SimpleInputQuery ($name: String!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-SimpleMutation.graphql-SimpleMutation.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleMutation.graphql-SimpleMutation.graphql.go index b6cfc893..e7095e8f 100644 --- a/generate/testdata/snapshots/TestGenerate-SimpleMutation.graphql-SimpleMutation.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-SimpleMutation.graphql-SimpleMutation.graphql.go @@ -24,6 +24,11 @@ type SimpleMutationResponse struct { CreateUser SimpleMutationCreateUser `json:"createUser"` } +// __SimpleMutationInput is used internally by genqlient +type __SimpleMutationInput struct { + Name string `json:"name"` +} + // SimpleMutation creates a user. // // It has a long doc-comment, to test that we handle that correctly. @@ -32,10 +37,9 @@ func SimpleMutation( client graphql.Client, name string, ) (*SimpleMutationResponse, error) { - variables := map[string]interface{}{ - "name": name, + __input := __SimpleMutationInput{ + Name: name, } - var err error var retval SimpleMutationResponse @@ -51,7 +55,7 @@ mutation SimpleMutation ($name: String!) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go b/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go index 47c0e43d..4994cbb0 100644 --- a/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go +++ b/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go @@ -36,6 +36,11 @@ type UserQueryInput struct { 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. type unexportedResponse struct { // user looks up a user by some stuff. @@ -60,10 +65,9 @@ func unexported( client graphql.Client, query UserQueryInput, ) (*unexportedResponse, error) { - variables := map[string]interface{}{ - "query": query, + __input := __unexportedInput{ + Query: query, } - var err error var retval unexportedResponse @@ -78,7 +82,7 @@ query unexported ($query: UserQueryInput) { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/generate/types.go b/generate/types.go index e29f32a4..f5b58a34 100644 --- a/generate/types.go +++ b/generate/types.go @@ -139,6 +139,7 @@ type goStructField struct { GoType goType JSONName string // i.e. the field's alias in this query GraphQLName string // i.e. the field's name in its type-def + Omitempty bool // only used on input types 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) for _, field := range typ.Fields { writeDescription(w, field.Description) - jsonName := field.JSONName + jsonTag := `"` + field.JSONName + if field.Omitempty { + jsonTag += ",omitempty" + } + jsonTag += `"` if field.IsAbstract() { // abstract types are handled in our UnmarshalJSON (see below) needUnmarshaler = true - jsonName = "-" + jsonTag = `"-"` } if field.IsEmbedded() { // embedded fields also need UnmarshalJSON handling (see below) needUnmarshaler = true fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference()) } else { - fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n", - field.GoName, field.GoType.Reference(), jsonName) + fmt.Fprintf(w, "\t%s %s `json:%s`\n", + field.GoName, field.GoType.Reference(), jsonTag) } } fmt.Fprintf(w, "}\n") diff --git a/graphql/client.go b/graphql/client.go index ce28c8d5..fdd106c0 100644 --- a/graphql/client.go +++ b/graphql/client.go @@ -24,9 +24,10 @@ type Client interface { // context.Background(). // // query is the literal string representing the GraphQL query, e.g. - // `query myQuery { myField }`. variables contains the GraphQL variables - // to be sent along with the query, or may be nil if there are none. - // Typically, GraphQL APIs will accept a JSON payload of the form + // `query myQuery { myField }`. variables contains a JSON-marshalable + // value containing the variables to be sent along with the query, + // or may be nil if there are none. Typically, GraphQL APIs wills + // accept a JSON payload of the form // {"query": "query myQuery { ... }", "variables": {...}}` // but MakeRequest may use some other transport, handle extensions, or set // other parameters, if it wishes. @@ -41,8 +42,7 @@ type Client interface { ctx context.Context, opName string, query string, - retval interface{}, - variables map[string]interface{}, + input, retval interface{}, ) error } @@ -69,8 +69,8 @@ func NewClient(endpoint string, httpClient *http.Client) Client { } type payload struct { - Query string `json:"query"` - Variables map[string]interface{} `json:"variables,omitempty"` + Query string `json:"query"` + Variables interface{} `json:"variables,omitempty"` // OpName is only required if there are multiple queries in the document, // but we set it unconditionally, because that's easier. OpName string `json:"operationName"` @@ -81,7 +81,7 @@ type response struct { 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{ Query: query, Variables: variables, diff --git a/internal/integration/generated.go b/internal/integration/generated.go index 73dad471..c73aef50 100644 --- a/internal/integration/generated.go +++ b/internal/integration/generated.go @@ -266,6 +266,41 @@ func (v *UserFields) UnmarshalJSON(b []byte) error { 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. type failingQueryMeUser struct { Id string `json:"id"` @@ -1036,6 +1071,18 @@ func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error { 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. type queryWithVariablesResponse struct { User queryWithVariablesUser `json:"user"` @@ -1114,10 +1161,9 @@ func queryWithVariables( client graphql.Client, id string, ) (*queryWithVariablesResponse, error) { - variables := map[string]interface{}{ - "id": id, + __input := __queryWithVariablesInput{ + Id: id, } - var err error var retval queryWithVariablesResponse @@ -1134,7 +1180,36 @@ query queryWithVariables ($id: ID!) { } `, &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 } @@ -1144,10 +1219,9 @@ func queryWithInterfaceNoFragments( client graphql.Client, id string, ) (*queryWithInterfaceNoFragmentsResponse, error) { - variables := map[string]interface{}{ - "id": id, + __input := __queryWithInterfaceNoFragmentsInput{ + Id: id, } - var err error var retval queryWithInterfaceNoFragmentsResponse @@ -1168,7 +1242,7 @@ query queryWithInterfaceNoFragments ($id: ID!) { } `, &retval, - variables, + &__input, ) return &retval, err } @@ -1178,10 +1252,9 @@ func queryWithInterfaceListField( client graphql.Client, ids []string, ) (*queryWithInterfaceListFieldResponse, error) { - variables := map[string]interface{}{ - "ids": ids, + __input := __queryWithInterfaceListFieldInput{ + Ids: ids, } - var err error var retval queryWithInterfaceListFieldResponse @@ -1198,7 +1271,7 @@ query queryWithInterfaceListField ($ids: [ID!]!) { } `, &retval, - variables, + &__input, ) return &retval, err } @@ -1208,10 +1281,9 @@ func queryWithInterfaceListPointerField( client graphql.Client, ids []string, ) (*queryWithInterfaceListPointerFieldResponse, error) { - variables := map[string]interface{}{ - "ids": ids, + __input := __queryWithInterfaceListPointerFieldInput{ + Ids: ids, } - var err error var retval queryWithInterfaceListPointerFieldResponse @@ -1228,7 +1300,7 @@ query queryWithInterfaceListPointerField ($ids: [ID!]!) { } `, &retval, - variables, + &__input, ) return &retval, err } @@ -1238,10 +1310,9 @@ func queryWithFragments( client graphql.Client, ids []string, ) (*queryWithFragmentsResponse, error) { - variables := map[string]interface{}{ - "ids": ids, + __input := __queryWithFragmentsInput{ + Ids: ids, } - var err error var retval queryWithFragmentsResponse @@ -1286,7 +1357,7 @@ query queryWithFragments ($ids: [ID!]!) { } `, &retval, - variables, + &__input, ) return &retval, err } @@ -1296,10 +1367,9 @@ func queryWithNamedFragments( client graphql.Client, ids []string, ) (*queryWithNamedFragmentsResponse, error) { - variables := map[string]interface{}{ - "ids": ids, + __input := __queryWithNamedFragmentsInput{ + Ids: ids, } - var err error var retval queryWithNamedFragmentsResponse @@ -1344,7 +1414,7 @@ fragment MoreUserFields on User { } `, &retval, - variables, + &__input, ) return &retval, err } diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index bee2ef7c..34ff6b16 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -89,6 +89,33 @@ func TestVariables(t *testing.T) { 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) { _ = `# @genqlient query queryWithInterfaceNoFragments($id: ID!) { diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql index 6ea7069e..badf85d8 100644 --- a/internal/integration/schema.graphql +++ b/internal/integration/schema.graphql @@ -1,6 +1,6 @@ type Query { me: User - user(id: ID!): User + user(id: ID): User being(id: ID!): Being beings(ids: [ID!]!): [Being]! lotteryWinner(number: Int!): Lucky diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go index 06635c64..98a2ba5f 100644 --- a/internal/integration/server/gqlgen_exec.go +++ b/internal/integration/server/gqlgen_exec.go @@ -64,7 +64,7 @@ type ComplexityRoot struct { Fail func(childComplexity int) int LotteryWinner func(childComplexity int, number int) int Me func(childComplexity int) int - User func(childComplexity int, id string) int + User func(childComplexity int, id *string) int } User struct { @@ -77,7 +77,7 @@ type ComplexityRoot struct { type QueryResolver interface { 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) Beings(ctx context.Context, ids []string) ([]Being, 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 e.complexity.Query.User(childComplexity, args["id"].(string)), true + return e.complexity.Query.User(childComplexity, args["id"].(*string)), true case "User.hair": if e.complexity.User.Hair == nil { @@ -290,7 +290,7 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er var sources = []*ast.Source{ {Name: "../schema.graphql", Input: `type Query { me: User - user(id: ID!): User + user(id: ID): User being(id: ID!): Being beings(ids: [ID!]!): [Being]! 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) { var err error args := map[string]interface{}{} - var arg0 string + var arg0 *string if tmp, ok := rawArgs["id"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) - arg0, err = ec.unmarshalNID2string(ctx, tmp) + arg0, err = ec.unmarshalOID2ᚖstring(ctx, tmp) if err != nil { return nil, err } @@ -743,7 +743,7 @@ func (ec *executionContext) _Query_user(ctx context.Context, field graphql.Colle fc.Args = args resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { 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 { ec.Error(ctx, err) @@ -3131,6 +3131,21 @@ func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋin 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) { if v == nil { return nil, nil diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go index 3cf362bb..bbf0a293 100644 --- a/internal/integration/server/server.go +++ b/internal/integration/server/server.go @@ -58,8 +58,11 @@ func (r *queryResolver) Me(ctx context.Context) (*User, error) { return userByID("1"), nil } -func (r *queryResolver) User(ctx context.Context, id string) (*User, error) { - return userByID(id), nil +func (r *queryResolver) User(ctx context.Context, id *string) (*User, error) { + if id == nil { + return userByID("1"), nil + } + return userByID(*id), nil } func (r *queryResolver) Being(ctx context.Context, id string) (Being, error) {