Files
genqlient/generate/types.go
T
Ben Kraft e88305ecbd Add support for abstract-typed named fragments (#79)
## Summary:
In previous commits I added support to genqlient for interfaces,
inline fragments, and, most recently, named fragments of concrete
(object) type.  This leaves only named fragments of interface type!
Like other named fragments, these are useful for code-sharing,
especially if you want some code that can handle the same fields of
several different types.

As seems to be inevitable with genqlient, this was mostly pretty
straightforward, although there turned out to be surprisingly many
places we needed to add some handling; almost anywhere that touches
interfaces *or* named fragments needed some updates.  But it's all
hopefully fairly clear code.

As a part of this change I made three semi-related improvements:
1. I refactored the handling of descriptions (i.e. GoDoc), because it
   was getting more and more confusing and duplicative.  I'm still not
   sure how much of it it makes sense to inline vs. separate, but I
   think this is better than it was.  This resulted in some minor
   changes to descriptions, generally in the direction of making things
   more consistent.
2. I bumped the minimum Go version to 1.14 so we can guarantee support
   for duplicate interface methods.  These are useful for
   abstract-in-absstract spreads; we generate an interface for the
   fragment, and (if the fragment-type implements the scope-type) we
   embed it into the interface we generate for its spread-context, and
   if the two have a duplicated field we thus duplicate the method.  It
   wouldn't be impossible to support this on 1.13 (maybe just by
   omitting said embed) but it didn't seem worth it.  This also removes
   a few special-cases in tests.
3. I added a bunch of code to better format syntax errors in the
   generated code (which we see from `gofmt`).  This is mostly just an
   internal improvement; I wrote it because I got annoyed while hunting
   down a few such errors..

Fixes, at last, #8.

Issue: https://github.com/Khan/genqlient/issues/8

## Test plan:
make check


Author: benjaminjkraft

Reviewers: dnerdy, benjaminjkraft, aberkan, MiguelCastillo

Required Reviewers: 

Approved By: dnerdy

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

Pull Request URL: https://github.com/Khan/genqlient/pull/79
2021-09-09 09:48:18 -07:00

305 lines
11 KiB
Go

package generate
// 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"
)
// 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
// Remove slice/pointer wrappers, and return the underlying (named (or
// builtin)) type. For example, given []*MyStruct, return MyStruct.
Unwrap() goType
// Count the number of times Unwrap() will unwrap a slice type. For
// example, given [][][]*MyStruct (or []**[][]*MyStruct, but we never
// currently generate that), return 3.
SliceDepth() int
// True if Unwrap() will unwrap a pointer at least once.
IsPointer() bool
}
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, often used to
// represent a GraphQL scalar. (See Config.Bindings for more context.)
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)
}
fmt.Fprintf(w, ")\n")
return nil
}
func (typ *goEnumType) Reference() string { return typ.GoName }
// goStructType represents a Go struct type used to represent a GraphQL object
// or input-object type.
type goStructType struct {
GoName string
Fields []*goStructField
IsInput bool
descriptionInfo
}
type goStructField struct {
GoName string
GoType goType
JSONName string // i.e. the field's alias in this query
GraphQLName string // i.e. the field's name in its type-def
Description string
}
// IsAbstract returns true if this field is of abstract type (i.e. GraphQL
// union or interface; equivalently, represented by an interface in Go).
func (field *goStructField) IsAbstract() bool {
_, ok := field.GoType.Unwrap().(*goInterfaceType)
return ok
}
// IsEmbedded returns true if this field is embedded (a.k.a. anonymous), which
// is in practice true if it corresponds to a named fragment spread in GraphQL.
func (field *goStructField) IsEmbedded() bool {
return field.GoName == ""
}
func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
writeDescription(w, structDescription(typ))
needUnmarshaler := false
fmt.Fprintf(w, "type %s struct {\n", typ.GoName)
for _, field := range typ.Fields {
writeDescription(w, field.Description)
jsonName := field.JSONName
if field.IsAbstract() {
// abstract types are handled in our UnmarshalJSON (see below)
needUnmarshaler = true
jsonName = "-"
}
if field.IsEmbedded() {
// embedded fields also need UnmarshalJSON handling (see below)
needUnmarshaler = true
fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference())
} else {
fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n",
field.GoName, field.GoType.Reference(), jsonName)
}
}
fmt.Fprintf(w, "}\n")
// Now, if needed, write the unmarshaler. We need one if we have any
// interface-typed fields, or any embedded fields.
//
// For interface-typed fields, ideally we'd write an UnmarshalJSON method
// on the field, but you can't add a method to an interface. So we write a
// per-interface-type helper, but we have to call it (with a little
// boilerplate) everywhere the type is referenced.
//
// For embedded fields (from fragments), mostly the JSON library would just
// do what we want, but there are two problems. First, if the embedded
// type has its own UnmarshalJSON, naively that would be promoted to
// become our UnmarshalJSON, which is no good. But we don't want to just
// hide that method and inline its fields, either; we need to call its
// UnmarshalJSON (on the same object we unmarshal into this struct).
// Second, if the embedded type duplicates any fields of the embedding type
// -- maybe both the fragment and the selection into which it's spread
// select the same field, or several fragments select the same field -- the
// JSON library will only fill one of those (the least-nested one); we want
// to fill them all.
if !needUnmarshaler {
return nil
}
// TODO(benkraft): Avoid having to enumerate these in advance; just let the
// template add them directly.
_, err := g.addRef("encoding/json.Unmarshal")
if err != nil {
return err
}
return g.execute("unmarshal.go.tmpl", 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
// Fields shared by all the interface's implementations;
// we'll generate getter methods for each.
SharedFields []*goStructField
Implementations []*goStructType
descriptionInfo
}
func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error {
writeDescription(w, interfaceDescription(typ))
// Write the interface.
fmt.Fprintf(w, "type %s interface {\n", typ.GoName)
implementsMethodName := fmt.Sprintf("implementsGraphQLInterface%v", typ.GoName)
fmt.Fprintf(w, "\t%s()\n", implementsMethodName)
for _, sharedField := range typ.SharedFields {
if sharedField.GoName == "" { // embedded type
fmt.Fprintf(w, "\t%s\n", sharedField.GoType.Reference())
continue
}
methodName := "Get" + sharedField.GoName
description := ""
if sharedField.GraphQLName == "__typename" {
description = fmt.Sprintf(
"%s returns the receiver's concrete GraphQL type-name "+
"(see interface doc for possible values).", methodName)
} else {
description = fmt.Sprintf(
`%s returns the interface-field "%s" from its implementation.`,
methodName, sharedField.GraphQLName)
if sharedField.Description != "" {
description = fmt.Sprintf(
"%s\nThe GraphQL interface field's documentation follows.\n\n%s",
description, sharedField.Description)
}
}
writeDescription(w, description)
fmt.Fprintf(w, "\t%s() %s\n", methodName, sharedField.GoType.Reference())
}
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)
for _, sharedField := range typ.SharedFields {
if sharedField.GoName == "" { // embedded
continue // no method needed
}
description := fmt.Sprintf(
"Get%s is a part of, and documented with, the interface %s.",
sharedField.GoName, typ.GoName)
writeDescription(w, description)
// In principle we should find the corresponding field of the
// implementation and use its name in `v.<name>`. In practice,
// they're always the same.
fmt.Fprintf(w, "func (v *%s) Get%s() %s { return v.%s }\n",
impl.Reference(), sharedField.GoName,
sharedField.GoType.Reference(), sharedField.GoName)
}
fmt.Fprintf(w, "\n") // blank line between each type's implementations
}
// Finally, write the unmarshal-helper, which will be called by struct
// fields referencing this type (see goStructType.WriteDefinition).
//
// TODO(benkraft): Avoid having to enumerate these refs in advance; just
// let the template add them directly.
_, err := g.addRef("encoding/json.Unmarshal")
if err != nil {
return err
}
_, err = g.addRef("fmt.Errorf")
if err != nil {
return err
}
return g.execute("unmarshal_helper.go.tmpl", w, typ)
}
func (typ *goInterfaceType) Reference() string { return typ.GoName }
func (typ *goOpaqueType) Unwrap() goType { return typ }
func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goEnumType) Unwrap() goType { return typ }
func (typ *goStructType) Unwrap() goType { return typ }
func (typ *goInterfaceType) Unwrap() goType { return typ }
func (typ *goOpaqueType) SliceDepth() int { return 0 }
func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 }
func (typ *goPointerType) SliceDepth() int { return 0 }
func (typ *goEnumType) SliceDepth() int { return 0 }
func (typ *goStructType) SliceDepth() int { return 0 }
func (typ *goInterfaceType) SliceDepth() int { return 0 }
func (typ *goOpaqueType) IsPointer() bool { return false }
func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() }
func (typ *goPointerType) IsPointer() bool { return true }
func (typ *goEnumType) IsPointer() bool { return false }
func (typ *goStructType) IsPointer() bool { return false }
func (typ *goInterfaceType) IsPointer() bool { return false }
func writeDescription(w io.Writer, desc string) {
if desc != "" {
for _, line := range strings.Split(desc, "\n") {
fmt.Fprintf(w, "// %s\n", strings.TrimLeft(line, " \t"))
}
}
}