Files
genqlient/generate/types.go
T
Ben KraftandGitHub 4c38cb7759 Add support for interfaces, part 1: the simplest cases (#52)
## Summary:
In this commit I begin the journey to add the long-awaited support for
interfaces (part of #8).  Well, it's not the beginning: I already had
some half-written broken code around.  But it's the first fully
functional support, and especially, the first *tested* support; it's
probably best to review the nontrivially-changed code as if it were new.

Conceptually, the code so far is pretty simple: we generate an interface
type, and the implementations.  (That code is in fact mostly unchanged.)
The complexity comes in because encoding/json doesn't know how to
unmarshal that.  So we have to add an UnmarshalJSON method, which
actually has to be on the types with interface-type fields, that knows
how.  I factored it into two methods, such that that UnmarshalJSON
method is just glue, and then there's a separate function, corresponding
to each interface-type, that actually does all the work.  (If only one
could just write it as an actual method!)  The method uses the same
trick suggested to me by a few others in another context to deserialize
all but one field, then handle that field specially, which is discussed
in the code.

This still has some limitations, which will be lifted in future commits:
- it doesn't allow for list-of-interface fields
- it requires that you manually ask for `__typename`
- it doesn't support fragments, i.e. you can only query for interface
  fields, not concrete-type-specific ones
But it works, even in integration tests, which is progress!

As a part of this, I added a proper config option for the "allow broken
features" flag, since I need to be able to set it from the integration
tests which are in a separate package (and actually shell out via `go
generate`).  I also renamed what was to be the first case
(InterfaceNoFragments), and replaced it with a further-simplified
version (avoiding list-of-interface fields.

[1] https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md

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

## Test plan:
make tesc

Author: benjaminjkraft

Reviewers: dnerdy, benjaminjkraft, aberkan, csilvers, MiguelCastillo

Required Reviewers: 

Approved by: dnerdy

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint,  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/52
2021-08-25 11:51:26 -07:00

237 lines
7.6 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
}
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)
}
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
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
}
type goStructField struct {
GoName string
GoType goType
JSONName string
Description 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")
// Now, if needed, write the unmarshaler.
//
// Specifically, in order to unmarshal interface values, we need to add an
// UnmarshalJSON method to each type which has an interface-typed *field*
// (not the interface type itself -- we can't add methods to that).
// But we put most of the logic in a per-interface-type helper function,
// written along with the interface type; the UnmarshalJSON method is just
// the boilerplate.
if len(typ.AbstractFields()) == 0 {
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 }
// AbstractFields returns all the fields which are abstract types (i.e. GraphQL
// unions and interfaces; equivalently, types represented by interfaces in Go).
func (typ *goStructType) AbstractFields() []*goStructField {
var ret []*goStructField
for _, field := range typ.Fields {
// TODO(benkraft): To handle list-of-interface fields, we should really
// be "unwrapping" any goSliceType/goPointerType wrappers to find the
// goInterfaceType.
if _, ok := field.GoType.(*goInterfaceType); ok {
ret = append(ret, field)
}
}
return ret
}
// 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)
}
// 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 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") {
fmt.Fprintf(w, "// %s\n", strings.TrimLeft(line, " \t"))
}
}
}