Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor argument-handling to use a struct #103

Merged
merged 5 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ 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.
- 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:

### 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)

## v0.1.0
Expand Down
5 changes: 3 additions & 2 deletions docs/genqlient_directive.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 9 additions & 5 deletions example/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 71 additions & 13 deletions generate/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
49 changes: 9 additions & 40 deletions generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -69,14 +72,6 @@ type exportedOperations struct {
Operations []*operation `json:"operations"`
}

type argument struct {
GoName string
GoType string
GraphQLName string
IsSlice bool
Options *genqlientDirective
}

func newGenerator(
config *Config,
schema *ast.Schema,
Expand Down Expand Up @@ -125,29 +120,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 {
Expand Down Expand Up @@ -277,12 +249,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)
Expand Down Expand Up @@ -313,7 +282,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
Expand Down
3 changes: 0 additions & 3 deletions generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,6 @@ func TestGenerate(t *testing.T) {
t.Run("Build", func(t *testing.T) {
if testing.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])
Expand Down
37 changes: 14 additions & 23 deletions generate/operation.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,23 @@ 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 here is 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 -}}
{{/* We need to avoid conflicting with any of the function's argument names
which are derived from the GraphQL argument names; notably `input` is
a common one. So we use a name that's not legal in GraphQL, namely
one starting with a double-underscore. */ -}}
__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
Expand All @@ -48,7 +39,7 @@ func {{.Name}}(
"{{.Name}}",
`{{.Body}}`,
&retval,
{{if .Args}}variables{{else}}nil{{end}},
{{if .Input}}&__input{{else}}nil{{end}},
)
return &retval, err
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading