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
This commit is contained in:
Ben Kraft
2021-08-25 11:49:30 -07:00
committed by GitHub
parent 4a06e3f28e
commit 8815d0991c
10 changed files with 711 additions and 711 deletions
+164 -388
View File
@@ -1,412 +1,188 @@
package generate
// This file is the core of genqlient: it's what generates the types into which
// we will unmarshal.
//
// TODO: this file really really needs a file-comment explaining what's going
// on. probably writing that will help me figure out how to make it make more
// sense!
// This file defines the data structures from which genqlient generates types,
// and the code to write them out as actual Go code. The main entrypoint is
// goType, which represents such a type, but convert.go also constructs each
// of the implementing types, by traversing the GraphQL operation and schema.
import (
"fmt"
"io"
"strings"
"github.com/vektah/gqlparser/v2/ast"
)
type typeBuilder struct {
strings.Builder
*generator
// goType represents a type for which we'll generate code.
type goType interface {
// WriteDefinition writes the code for this type into the given io.Writer.
//
// TODO(benkraft): Some of the implementations might now benefit from being
// converted to templates.
WriteDefinition(io.Writer, *generator) error
// Reference returns the Go name of this type, e.g. []*MyStruct, and may be
// used to refer to it in Go code.
Reference() string
}
func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definition, error) {
switch operation {
case ast.Query:
return g.schema.Query, nil
case ast.Mutation:
return g.schema.Mutation, nil
case ast.Subscription:
if !allowBrokenFeatures {
return nil, errorf(nil, "genqlient does not yet support subscriptions")
}
return g.schema.Subscription, nil
default:
return nil, errorf(nil, "unexpected operation: %v", operation)
var (
_ goType = (*goOpaqueType)(nil)
_ goType = (*goSliceType)(nil)
_ goType = (*goPointerType)(nil)
_ goType = (*goEnumType)(nil)
_ goType = (*goStructType)(nil)
_ goType = (*goInterfaceType)(nil)
)
type (
// goOpaqueType represents a user-defined or builtin type, used to
// represent a GraphQL scalar.
goOpaqueType struct{ GoRef string }
// goSliceType represents the Go type []Elem, used to represent GraphQL
// list types.
goSliceType struct{ Elem goType }
// goSliceType represents the Go type *Elem, used when requested by the
// user (perhaps to handle nulls explicitly, or to avoid copying large
// structures).
goPointerType struct{ Elem goType }
)
// Opaque types are defined by the user; pointers and slices need no definition
func (typ *goOpaqueType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goSliceType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goPointerType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goOpaqueType) Reference() string { return typ.GoRef }
func (typ *goSliceType) Reference() string { return "[]" + typ.Elem.Reference() }
func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() }
// goEnumType represents a Go named-string type used to represent a GraphQL
// enum. In this case, we generate both the type (`type T string`) and also a
// list of consts representing the values.
type goEnumType struct {
GoName string
Description string
Values []goEnumValue
}
type goEnumValue struct {
Name string
Description string
}
func (typ *goEnumType) WriteDefinition(w io.Writer, g *generator) error {
// All GraphQL enums have underlying type string (in the Go sense).
writeDescription(w, typ.Description)
fmt.Fprintf(w, "type %s string\n", typ.GoName)
fmt.Fprintf(w, "const (\n")
for _, val := range typ.Values {
writeDescription(w, val.Description)
fmt.Fprintf(w, "%s %s = \"%s\"\n",
typ.GoName+goConstName(val.Name),
typ.GoName, val.Name)
}
}
func (g *generator) getTypeForOperation(operation *ast.OperationDefinition, queryOptions *GenqlientDirective) (name string, err error) {
name = operation.Name + "Response"
if def, ok := g.typeMap[name]; ok {
// TODO: check for and handle conflicts a better way
return "", errorf(operation.Position, "%s defined twice:\n%s", name, def)
}
fields, err := selections(g, operation.SelectionSet, queryOptions)
if err != nil {
return "", err
}
baseType, err := g.baseTypeForOperation(operation.Operation)
if err != nil {
return "", errorf(operation.Position, "%v", err)
}
description := fmt.Sprintf("%v is returned by %v on success.", name, operation.Name)
builder := &typeBuilder{generator: g}
err = builder.writeTypedef(
name, operation.Name, baseType, operation.Position, fields, queryOptions, description)
return name, err
}
var builtinTypes = map[string]string{
// GraphQL guarantees int32 is enough, but using int seems more idiomatic
"Int": "int",
"Float": "float64",
"String": "string",
"Boolean": "bool",
"ID": "string",
}
func (g *generator) typeName(prefix string, typ *ast.Definition) (name, nextPrefix string) {
typeGoName := upperFirst(typ.Name)
if typ.Kind == ast.Enum || typ.Kind == ast.InputObject {
// If we're an enum or an input-object, there is only one type we
// will ever possibly generate for this type, so we don't need any
// of the qualifiers. This is especially helpful because the
// caller is very likely to need to reference these types in their
// code.
return typeGoName, typeGoName
}
name = prefix
if !strings.HasSuffix(prefix, typeGoName) {
// If the field and type names are the same, we can avoid the
// duplication. (We include the field name in case there are
// multiple fields with the same type, and the type name because
// that's the actual name (the rest are really qualifiers); but if
// they are the same then including it once suffices for both
// purposes.)
name += typeGoName
}
if typ.Kind == ast.Interface || typ.Kind == ast.Union {
// for interface/union types, we do not add the type name to the
// name prefix; we want to have QueryFieldType rather than
// QueryFieldInterfaceType. So we just use the input prefix.
return name, prefix
}
// Otherwise, the name will also be the prefix for the next type.
return name, name
}
func (g *generator) getTypeForInputType(opName string, typ *ast.Type, options, queryOptions *GenqlientDirective) (string, error) {
// Sort of a hack: case the input type name to match the op-name.
name := matchFirst(typ.Name(), opName)
builder := &typeBuilder{generator: g}
// note prefix is ignored here (see generator.typeName)
err := builder.writeType(name, "", typ, selectionsForInputType(g, typ, queryOptions), options)
return builder.String(), err
}
type field interface {
Alias() string
Options() (*GenqlientDirective, error)
Description() string
Type() *ast.Type
Pos() *ast.Position
SubFields() ([]field, error)
}
type outputField struct {
*generator
queryOptions *GenqlientDirective
field *ast.Field
}
func (s outputField) Alias() string {
// gqlparser sets Alias even if the field is not aliased, see e.g.
// https://github.com/vektah/gqlparser/v2/blob/c06d8e0d135f285e37e7f1ff397f10e049733eb3/parser/query.go#L150
return s.field.Alias
}
func (s outputField) Options() (*GenqlientDirective, error) {
_, directive, err := s.generator.parsePrecedingComment(s.field, s.field.Position)
if err != nil {
return nil, err
}
return s.queryOptions.merge(directive), nil
}
func (s outputField) Description() string {
if s.field.Definition == nil {
return ""
}
return s.field.Definition.Description
}
func (s outputField) Type() *ast.Type {
if s.field.Definition == nil {
return nil
}
return s.field.Definition.Type
}
func (s outputField) Pos() *ast.Position {
return s.field.Position
}
func (s outputField) SubFields() ([]field, error) {
return selections(s.generator, s.field.SelectionSet, s.queryOptions)
}
func selections(g *generator, selectionSet ast.SelectionSet, options *GenqlientDirective) ([]field, error) {
retval := make([]field, len(selectionSet))
for i, selection := range selectionSet {
switch selection := selection.(type) {
case *ast.Field:
retval[i] = outputField{g, options, selection}
case *ast.FragmentSpread:
return nil, errorf(selection.Position, "not implemented: %T", selection)
case *ast.InlineFragment:
return nil, errorf(selection.Position, "not implemented: %T", selection)
default:
return nil, errorf(nil, "invalid selection type: %T", selection)
}
}
return retval, nil
}
type inputField struct {
*generator
field *ast.FieldDefinition
queryOptions *GenqlientDirective
}
func (s inputField) Alias() string { return s.field.Name }
func (s inputField) Options() (*GenqlientDirective, error) {
_, directive, err := s.generator.parsePrecedingComment(s.field, s.field.Position)
if err != nil {
return nil, err
}
return s.queryOptions.merge(directive), nil
}
func (s inputField) Description() string { return s.field.Description }
func (s inputField) Type() *ast.Type { return s.field.Type }
func (s inputField) Pos() *ast.Position { return s.field.Position }
func (s inputField) SubFields() ([]field, error) {
return selectionsForInputType(s.generator, s.field.Type, s.queryOptions), nil
}
func selectionsForInputType(g *generator, typ *ast.Type, queryOptions *GenqlientDirective) []field {
def := g.schema.Types[typ.Name()]
fields := make([]field, len(def.Fields))
for i, field := range def.Fields {
fields[i] = inputField{g, field, queryOptions}
}
return fields
}
func (builder *typeBuilder) writeField(typeNamePrefix string, field field) error {
jsonName := field.Alias()
// We need an exportable name for JSON-marshaling.
goName := upperFirst(jsonName)
typ := field.Type()
if typ == nil {
// Unclear why gqlparser hasn't already rejected this,
// but empirically it might not.
return errorf(field.Pos(), "undefined field %v", field.Alias())
}
fields, err := field.SubFields()
if err != nil {
return err
}
options, err := field.Options()
if err != nil {
return err
}
typedef := builder.schema.Types[typ.Name()]
builder.writeDescription(field.Description())
builder.WriteString(goName)
builder.WriteRune(' ')
// Note we don't deduplicate suffixes here -- if our prefix is GetUser
// and the field name is User, we do GetUserUser. This is important
// because if you have a field called user on a type called User we
// need `query q { user { user { id } } }` to generate two types, QUser
// and QUserUser.
// Note also this is named based on the GraphQL alias (Go name), not the
// field-name, because if we have `query q { a: f { b }, c: f { d } }` we
// need separate types for a and c, even though they are the same type in
// GraphQL, because they have different fields.
name, namePrefix := builder.typeName(typeNamePrefix+goName, typedef)
err = builder.writeType(name, namePrefix, typ, fields, options)
if err != nil {
return err
}
if typedef.IsAbstractType() {
// abstract types are handled in our UnmarshalJSON
jsonName = "-"
}
fmt.Fprintf(builder, " `json:\"%s\"`\n", jsonName)
fmt.Fprintf(w, ")\n")
return nil
}
func (builder *typeBuilder) writeType(name, namePrefix string, typ *ast.Type, fields []field, options *GenqlientDirective) error {
// gqlgen does slightly different things here, but its implementation may
// be useful to crib from:
// https://github.com/99designs/gqlgen/blob/master/plugin/modelgen/models.go#L113
for typ.Elem != nil {
// Type is a list.
builder.WriteString("[]")
typ = typ.Elem
}
if options.GetPointer() {
// Note this does []*T or [][]*T, not e.g. *[][]T. See #16.
builder.WriteString("*")
}
func (typ *goEnumType) Reference() string { return typ.GoName }
// If this is a builtin type or custom scalar, just refer to it.
def := builder.schema.Types[typ.Name()]
goName, ok := builder.Config.Scalars[def.Name]
if ok {
newName, err := builder.addRef(goName)
builder.WriteString(newName)
return err
}
goName, ok = builtinTypes[def.Name]
if ok {
builder.WriteString(goName)
return nil
}
// Else, write the name, then generate the definition.
builder.WriteString(name)
childBuilder := &typeBuilder{generator: builder.generator}
return childBuilder.writeTypedef(name, namePrefix, def, typ.Position, fields, options, "")
// goStructType represents a Go struct type used to represent a GraphQL object
// or input-object type.
type goStructType struct {
GoName string
Description string
GraphQLName string
Fields []*goStructField
// Incomplete is set if this type contains only certain fields of the
// corresponding GraphQL type (i.e. those selected by the operation) in
// which case we put a note in the doc-comment saying as much.
Incomplete bool
}
func (builder *typeBuilder) writeTypedef(
typeName, typeNamePrefix string,
typedef *ast.Definition,
pos *ast.Position,
fields []field,
options *GenqlientDirective, //nolint:unparam // it is used!
description string, // defaults to typedef.Description
) (err error) {
defer func() {
// Whenever we're done, add the type to the type-map.
// TODO: there's got to be a better way than defer.
if err == nil {
// TODO: this should also check for conflicts (except not for enums
// and input-objects, see above)
builder.typeMap[typeName] = builder.String()
}
}()
if description == "" {
switch typedef.Kind {
case ast.Object, ast.Interface, ast.Union:
// For types where we only have some fields, note that, along with
// the GraphQL documentation (if any). We don't want to just use
// the GraphQL documentation, since it may refer to fields we
// haven't selected, say.
// TODO: When we implement interfaces and unions more completely,
// also mention the concrete types they might be.
description = fmt.Sprintf(
"%v includes the requested fields of the GraphQL type %v.",
typeName, typedef.Name)
if typedef.Description != "" {
description = fmt.Sprintf(
"%v\nThe GraphQL type's documentation follows.\n\n%v",
description, typedef.Description)
}
default:
description = typedef.Description
}
}
builder.writeDescription(description)
fmt.Fprintf(builder, "type %s ", typeName)
switch typedef.Kind {
case ast.Object, ast.InputObject:
builder.WriteString("struct {\n")
for _, field := range fields {
err := builder.writeField(typeNamePrefix, field)
if err != nil {
return err
}
}
builder.WriteString("}")
// If any field is abstract, we need an UnmarshalJSON method to handle
// it.
return builder.maybeWriteUnmarshal(typeName, typeNamePrefix, fields)
case ast.Interface, ast.Union:
if !allowBrokenFeatures {
return errorf(pos, "not implemented: %v", typedef.Kind)
}
// First, write the interface type.
builder.WriteString("interface {\n")
implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", typeName)
// TODO: Also write GetX() accessor methods for fields of the interface
builder.WriteString(implementsMethodName)
builder.WriteString("()\n")
builder.WriteString("}")
// Then, write the implementations.
// TODO(benkraft): Put a doc-comment somewhere with the list.
for _, impldef := range builder.schema.GetPossibleTypes(typedef) {
name, namePrefix := builder.typeName(typeNamePrefix, impldef)
implBuilder := &typeBuilder{generator: builder.generator}
err := implBuilder.writeTypedef(name, namePrefix, impldef, pos, fields, options, "")
if err != nil {
return err
}
builder.typeMap[name] += fmt.Sprintf(
"\nfunc (v %v) %v() {}", name, implementsMethodName)
}
return nil
case ast.Enum:
// All GraphQL enums have underlying type string (in the Go sense).
builder.WriteString("string\n")
builder.WriteString("const (\n")
for _, val := range typedef.EnumValues {
builder.writeDescription(val.Description)
fmt.Fprintf(builder, "%s %s = \"%s\"\n",
typeName+goConstName(val.Name),
typeName, val.Name)
}
builder.WriteString(")\n")
return nil
case ast.Scalar:
return errorf(pos, "unknown scalar %v: please add it to genqlient.yaml", typedef.Name)
default:
return errorf(pos, "unexpected kind: %v", typedef.Kind)
}
type goStructField struct {
GoName string
GoType goType
JSONName string
Description string
}
func (builder *typeBuilder) writeDescription(desc string) {
func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
description := typ.Description
if typ.Incomplete {
description = incompleteTypeDescription(typ.GoName, typ.GraphQLName, typ.Description)
}
writeDescription(w, description)
fmt.Fprintf(w, "type %s struct {\n", typ.GoName)
for _, field := range typ.Fields {
writeDescription(w, field.Description)
jsonName := field.JSONName
if _, ok := field.GoType.(*goInterfaceType); ok {
// abstract types are handled in our UnmarshalJSON
jsonName = "-"
}
fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n",
field.GoName, field.GoType.Reference(), jsonName)
}
fmt.Fprintf(w, "}\n")
return g.maybeWriteUnmarshal(w, typ)
}
func (typ *goStructType) Reference() string { return typ.GoName }
// goInterfaceType represents a Go interface type, used to represent a GraphQL
// interface or union type.
type goInterfaceType struct {
GoName string
Description string
GraphQLName string
Implementations []*goStructType
}
func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error {
// TODO(benkraft): also mention the list of implementations.
description := incompleteTypeDescription(typ.GoName, typ.GraphQLName, typ.Description)
writeDescription(w, description)
// Write the interface.
fmt.Fprintf(w, "type %s interface {\n", typ.GoName)
implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", typ.GoName)
// TODO(benkraft): Also write GetX() accessor methods for fields of the interface
fmt.Fprintf(w, "\t%s()\n", implementsMethodName)
fmt.Fprintf(w, "}\n")
// Now, write out the implementations.
for _, impl := range typ.Implementations {
fmt.Fprintf(w, "func (v %s) %s() {}\n",
impl.Reference(), implementsMethodName)
}
return nil
}
func (typ *goInterfaceType) Reference() string { return typ.GoName }
func incompleteTypeDescription(goName, graphQLName, description string) string {
// For types where we only have some fields, note that, along with
// the GraphQL documentation (if any). We don't want to just use
// the GraphQL documentation, since it may refer to fields we
// haven't selected, say.
prefix := fmt.Sprintf(
"%v includes the requested fields of the GraphQL type %v.",
goName, graphQLName)
if description != "" {
return fmt.Sprintf(
"%v\nThe GraphQL type's documentation follows.\n\n%v",
prefix, description)
}
return prefix
}
func writeDescription(w io.Writer, desc string) {
if desc != "" {
for _, line := range strings.Split(desc, "\n") {
builder.WriteString("// " + strings.TrimLeft(line, " \t") + "\n")
fmt.Fprintf(w, "// %s\n", strings.TrimLeft(line, " \t"))
}
}
}