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
+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 {