add omitempty

This commit is contained in:
Ben Kraft
2021-04-09 11:51:30 -07:00
parent 25a2b45ccc
commit 16523c1f81
8 changed files with 246 additions and 14 deletions
+3 -2
View File
@@ -70,7 +70,9 @@ For each GraphQL operation (query or mutation), genqlient generates a Go functio
It returns a pointer to a struct representing the query-result, and an `error`. The struct will always be initialized (never nil), even on error. The error may be a `github.com/vektah/gqlparser/v2/gqlerror.List`, if it was a GraphQL-level error (in this case the returned struct may still contain useful data, if the API returns data even on error), or may be another error if, for example, the whole HTTP request failed (in which case the struct is unlikely to contain useful data). If the GraphQL operation has a comment immediately above it, that comment text will be used as the GoDoc for the generated function.
TODO: document generated types further, especially when they become customizable.
The generated code may be customized using a directive-like syntax, `# @genqlient(...)`. For full documentation of options, see [`go doc github.com/Khan/genqlient/generate.GenqlientDirective`](https://pkg.go.dev/github.com/Khan/genqlient/generate#GenqlientDirective).
TODO: consider inlining the direct stuff; and document generated types further.
## Development
@@ -99,7 +101,6 @@ Fancy features:
Generated code customization:
- add flag(s) to make a field use a pointer (for optionality or perf; see DESIGN)
- omitempty-like thing for optional arguments
- collapsing -- should be able to have `mutation { myMutation { error { code } } }` just return `(code string, err error)`
- map a field to a particular Go type (if you want to use a named type for some string, say)
- specify a particular name for a Go type, or for the helper function, or whatever else
+109 -4
View File
@@ -8,18 +8,123 @@ import (
"github.com/vektah/gqlparser/v2/parser"
)
func (g *generator) parsePrecedingComment(pos *ast.Position) (comment string, directives []*ast.Directive, err error) {
// GenqlientDirective represents the @genqlient quasi-directive, used to
// configure genqlient on a query-by-query basis.
//
// The syntax of the directive is just like a GraphQL directive, except it goes
// in a comment on the line immediately preceding the field. (This is because
// GraphQL expects directives in queries to be defined by the server, not by
// the client, so it would reject a real @genqlient directive as nonexistent.)
//
// Directives may be applied to fields, arguments, or the entire query.
// Directives on the line preceding the query apply to all relevant nodes in
// the query; other directives apply to all nodes on the following line. (In
// all cases it's fine for there to be other comments in between the directive
// and the node(s) to which it applies.) For example, in the following query:
// # @genqlient(n: "a")
//
// # @genqlient(n: "b")
// #
// # Comment describing the query
// #
// # @genqlient(n: "c")
// query MyQuery(arg1: String,
// # @genqlient(n: "d")
// arg2: String, arg3: String,
// arg4: String,
// ) {
// # @genqlient(n: "e")
// field1, field2
// field3
// }
// the directive "a" is ignored, "b" and "c" apply to all relevant nodes in the
// query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2.
type GenqlientDirective struct {
// If set, this argument will be omitted if it's equal to its Go zero
// value. For example, given the following query:
// # @genqlient(omitempty: true)
// query MyQuery(arg: String) { ... }
// genqlient will generate a function
// MyQuery(ctx context.Context, client graphql.Client, arg string) ...
// which will pass {"arg": null} to GraphQL if arg is "", and the actual
// value otherwise.
//
// Only applicable to arguments of nullable types.
Omitempty bool
}
func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
if dir.Name != "genqlient" {
// Actually we just won't get here; we only get here if the line starts
// with "# @genqlient", unless there's some sort of bug.
return nil, fmt.Errorf("the only valid comment-directive is @genqlient, got %v", dir.Name)
}
var retval GenqlientDirective
for _, arg := range dir.Arguments {
switch arg.Name {
case "omitempty":
retval.Omitempty = true
default:
return nil, fmt.Errorf("unknown argument %v for @genqlient", arg.Name)
}
}
return &retval, nil
}
func (dir *GenqlientDirective) validate(node interface{}) error {
switch node := node.(type) {
case *ast.OperationDefinition:
// Anything is valid on the entire operation; it will just apply to
// whatever it is relevant to.
return nil
case *ast.VariableDefinition:
if dir.Omitempty && node.Type.NonNull {
return fmt.Errorf("omitempty may only be used on optional arguments")
}
return nil
case *ast.Field:
if dir.Omitempty {
return fmt.Errorf("omitempty is not appilcable to fields")
}
return nil
default:
return fmt.Errorf("invalid directive location: %T", node)
}
}
func (dir *GenqlientDirective) merge(other *GenqlientDirective) *GenqlientDirective {
if dir == nil {
return other
}
var retval GenqlientDirective
retval.Omitempty = dir.Omitempty || other.Omitempty
return &retval
}
func (g *generator) parsePrecedingComment(
node interface{},
pos *ast.Position,
) (comment string, directive *GenqlientDirective, err error) {
var commentLines []string
sourceLines := strings.Split(pos.Src.Input, "\n")
for i := pos.Line - 1; i > 0; i-- {
line := strings.TrimSpace(sourceLines[i-1])
trimmed := strings.TrimSpace(strings.TrimPrefix(line, "#"))
if strings.HasPrefix(line, "# @genqlient") {
directive, err := parseDirective(trimmed, pos)
graphQLDirective, err := parseDirective(trimmed, pos)
if err != nil {
return "", nil, err
}
directives = append(directives, directive)
genqlientDirective, err := fromGraphQL(graphQLDirective)
if err != nil {
return "", nil, err
}
err = genqlientDirective.validate(node)
if err != nil {
return "", nil, err
}
directive = directive.merge(genqlientDirective)
} else if strings.HasPrefix(line, "#") {
commentLines = append(commentLines, trimmed)
} else {
@@ -29,7 +134,7 @@ func (g *generator) parsePrecedingComment(pos *ast.Position) (comment string, di
reverse(commentLines)
return strings.TrimSpace(strings.Join(commentLines, "\n")), directives, nil
return strings.TrimSpace(strings.Join(commentLines, "\n")), directive, nil
}
func parseDirective(line string, pos *ast.Position) (*ast.Directive, error) {
+23 -7
View File
@@ -61,6 +61,7 @@ type argument struct {
GoName string
GoType string
GraphQLName string
Omitempty bool
}
func newGenerator(config *Config, schema *ast.Schema) *generator {
@@ -107,7 +108,21 @@ func (g *generator) Types() string {
return strings.Join(defs, "\n\n")
}
func (g *generator) getArgument(opName string, arg *ast.VariableDefinition) (argument, error) {
func (g *generator) getArgument(
opName string,
arg *ast.VariableDefinition,
operationDirective *GenqlientDirective,
) (argument, error) {
_, directive, err := g.parsePrecedingComment(arg, arg.Position)
if err != nil {
return argument{}, err
}
directive = operationDirective.merge(directive)
omitempty := false
if directive != nil {
omitempty = directive.Omitempty
}
graphQLName := arg.Variable
goType, err := g.getTypeForInputType(opName, arg.Type)
if err != nil {
@@ -117,6 +132,7 @@ func (g *generator) getArgument(opName string, arg *ast.VariableDefinition) (arg
GraphQLName: graphQLName,
GoName: lowerFirst(graphQLName),
GoType: goType,
Omitempty: omitempty,
}, nil
}
@@ -133,10 +149,15 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
// TODO: handle fragments
})
commentLines, directive, err := g.parsePrecedingComment(op, op.Position)
if err != nil {
return err
}
args := make([]argument, len(op.VariableDefinitions))
for i, arg := range op.VariableDefinitions {
var err error
args[i], err = g.getArgument(op.Name, arg)
args[i], err = g.getArgument(op.Name, arg, directive)
if err != nil {
return err
}
@@ -147,11 +168,6 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
return err
}
commentLines, _, err := g.parsePrecedingComment(op.Position)
if err != nil {
return err
}
var docComment string
if len(commentLines) > 0 {
docComment = "// " + strings.ReplaceAll(commentLines, "\n", "\n// ")
+11 -1
View File
@@ -24,9 +24,19 @@ func {{.Name}}(
{{- if .Args -}}
variables := map[string]interface{}{
{{range .Args -}}
"{{.GraphQLName}}": {{.GoName}},
"{{.GraphQLName}}": {{if .Omitempty}}nil{{else}}{{.GoName}}{{end}},
{{end}}
}
{{range .Args -}}
{{if .Omitempty -}}
{{/* 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}} {
variables["{{.GraphQLName}}"] = {{.GoName}}
}
{{end}}
{{end}}
{{end}}
var retval {{.ResponseName}}
err := {{if $.Config.ClientGetter}}{{$.Config.ClientGetter}}{{else}}client{{end}}.MakeRequest(
+11
View File
@@ -0,0 +1,11 @@
# @genqlient(omitempty: true)
query OmitEmptyQuery(
$query: UserQueryInput,
$dt: DateTime,
$tz: String,
) {
user(query: $query) {
id
}
maybeConvert(dt: $dt, tz: $tz)
}
+79
View File
@@ -0,0 +1,79 @@
package test
// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
import (
"time"
"github.com/Khan/genqlient/graphql"
"github.com/me/mypkg"
)
type OmitEmptyQueryResponse struct {
User OmitEmptyQueryUser `json:"user"`
MaybeConvert time.Time `json:"maybeConvert"`
}
type OmitEmptyQueryUser struct {
Id mypkg.ID `json:"id"`
}
type Role string
const (
RoleStudent Role = "STUDENT"
RoleTeacher Role = "TEACHER"
)
type UserQueryInput struct {
Email string `json:"email"`
Name string `json:"name"`
Id mypkg.ID `json:"id"`
Role Role `json:"role"`
Names []string `json:"names"`
}
func OmitEmptyQuery(
client graphql.Client,
query UserQueryInput,
dt time.Time,
tz string,
) (*OmitEmptyQueryResponse, error) {
variables := map[string]interface{}{
"query": nil,
"dt": nil,
"tz": nil,
}
var zero_query UserQueryInput
if query != zero_query {
variables["query"] = query
}
var zero_dt time.Time
if dt != zero_dt {
variables["dt"] = dt
}
var zero_tz string
if tz != zero_tz {
variables["tz"] = tz
}
var retval OmitEmptyQueryResponse
err := client.MakeRequest(
nil,
"OmitEmptyQuery",
`
query OmitEmptyQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) {
user(query: $query) {
id
}
maybeConvert(dt: $dt, tz: $tz)
}
`,
&retval,
variables,
)
return &retval, err
}
+9
View File
@@ -0,0 +1,9 @@
{
"operations": [
{
"operationName": "OmitEmptyQuery",
"query": "\nquery OmitEmptyQuery ($query: UserQueryInput, $dt: DateTime, $tz: String) {\n\tuser(query: $query) {\n\t\tid\n\t}\n\tmaybeConvert(dt: $dt, tz: $tz)\n}\n",
"sourceLocation": "testdata/queries/Omitempty.graphql"
}
]
}
+1
View File
@@ -63,6 +63,7 @@ type Query {
root: Topic!
randomLeaf: LeafContent!
convert(dt: DateTime!, tz: String): DateTime!
maybeConvert(dt: DateTime, tz: String): DateTime
}
type Mutation {