Files
genqlient/generate/genqlient_directive.go
T
Ben KraftandGitHub 8815d0991c Big refactor to separate operation-traversal from code-generation (#51)
## Summary:
I've felt for a while that types.go is way too confusing, and as I
started to implement some of the more complex cases of generating
interface-types, the cracks were really starting to show.  Luckily, I
also finally realized how to fix it: we need to separate the process of
traversing the GraphQL operation and schema to decide what types to
generate from the process of actually generating those types.  This
requires an extra set of intermediate data structures, but I think it
makes things quite a lot easier to understand -- and, importantly, it
means that the code-generation doesn't need to go in the order we
traverse the query/schema.

So in this commit, I did that huge refactor.  It's probably best to just
review types.go and traverse.go as if they were new; the old code was
quite hard to understand and the new code will hopefully make a lot more
sense.  (And to that end, review comments about what could be organized
better or needs more documentation are very much in order, even for code
that is mostly unchanged.)

This does introduce one bug, sort of, which is that rather than
generating broken code for list-of-interface fields, we generate no code
at all.  (A TODO in unmarshal.go describes why.)  I'll fix this when I
add support for those fields.  (It's all behind the AllowBrokenFeatures
flag, anyway.)  Otherwise, the only changes to generated code are that a
few methods are ordered differently, because we now generate the
implements-interface methods with the interface, rather than the
implementations, as it's much simpler that way.  (In GraphQL, unlike Go,
we know the list of all possible implementations of each interface, so
this is possible.)

## Test plan:
golangci-lint run ./... && go test ./...

Author: benjaminjkraft

Reviewers: dnerdy, benjaminjkraft, aberkan, csilvers, MiguelCastillo

Required Reviewers: 

Approved by: dnerdy

Checks:  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint

Pull request URL: https://github.com/Khan/genqlient/pull/51
2021-08-25 11:49:30 -07:00

189 lines
5.9 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, or is an empty slice.
//
// 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 (dir *GenqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
func (dir *GenqlientDirective) GetPointer() bool { return dir.Pointer != nil && *dir.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
}