187 lines
5.8 KiB
Go
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
|
|
}
|