Files
genqlient/generate/comments.go
T

187 lines
5.8 KiB
Go

package generate
import (
"fmt"
"strings"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/parser"
)
// 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 {
pos *ast.Position
// 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
// If set, this argument or field will use a pointer type in Go. Response
// types always use pointers, but otherwise we typically do not.
//
// This can be useful if it's a type you'll need to pass around (and want a
// pointer to save copies) or if you wish to distinguish between the Go
// zero value and null (for nullable fields).
Pointer *bool
}
func (g *GenqlientDirective) GetOmitempty() bool { return g.Omitempty != nil && *g.Omitempty }
func (g *GenqlientDirective) GetPointer() bool { return g.Pointer != nil && *g.Pointer }
func setBool(dst **bool, v *ast.Value) error {
ei, err := v.Value(nil) // no vars allowed
// TODO: here and below, put positions on these errors
if err != nil {
return errorf(v.Position, "invalid boolean value %v: %v", v, err)
}
if b, ok := ei.(bool); ok {
*dst = &b
return nil
}
return errorf(v.Position, "expected boolean, got non-boolean value %T(%v)", ei, ei)
}
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, errorf(dir.Position, "the only valid comment-directive is @genqlient, got %v", dir.Name)
}
var retval GenqlientDirective
retval.pos = dir.Position
var err error
for _, arg := range dir.Arguments {
switch arg.Name {
// TODO: reflect and struct tags?
case "omitempty":
err = setBool(&retval.Omitempty, arg.Value)
case "pointer":
err = setBool(&retval.Pointer, arg.Value)
default:
return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
}
if err != nil {
return nil, err
}
}
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 != nil && node.Type.NonNull {
return errorf(dir.pos, "omitempty may only be used on optional arguments")
}
return nil
case *ast.Field:
if dir.Omitempty != nil {
return errorf(dir.pos, "omitempty is not appilcable to fields")
}
return nil
default:
return errorf(dir.pos, "invalid directive location: %T", node)
}
}
func (dir *GenqlientDirective) merge(other *GenqlientDirective) *GenqlientDirective {
retval := *dir
if other.Omitempty != nil {
retval.Omitempty = other.Omitempty
}
if other.Pointer != nil {
retval.Pointer = other.Pointer
}
return &retval
}
func (g *generator) parsePrecedingComment(
node interface{},
pos *ast.Position,
) (comment string, directive *GenqlientDirective, err error) {
directive = new(GenqlientDirective)
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") {
graphQLDirective, err := parseDirective(trimmed, pos)
if err != nil {
return "", nil, err
}
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 {
break
}
}
reverse(commentLines)
return strings.TrimSpace(strings.Join(commentLines, "\n")), directive, nil
}
func parseDirective(line string, pos *ast.Position) (*ast.Directive, error) {
// HACK: parse the "directive" by making a fake query containing it.
fakeQuery := fmt.Sprintf("query %v { field }", line)
doc, err := parser.ParseQuery(&ast.Source{Input: fakeQuery})
if err != nil {
return nil, errorf(pos, "invalid genqlient directive: %v", err)
}
return doc.Operations[0].Directives[0], nil
}