## Summary: We had this setting called "scalars", which said: bind this GraphQL type to this Go type, rather than the one you would normally use. It's called that because it's most useful for custom scalars, where "the one you would normally use" is "error: unknown scalar". But nothing ever stopped you from using it for a non-scalar type. I was planning on removing this functionality, because it's sort of a rough edge, but a discussion with Craig found some good use cases, so instead, in this commit, I document it better and add some slightly nicer ways to specify it. Specifically, here are a few potential non-scalar use cases: - bind a GraphQL enum to a nonstandard type (or even `string`) - bind an input type to some type that has exactly the fields you want; this acts as a sort of workaround for issues #14 and #44 - bind an object type to your own struct, so as to add methods to it (this is the use case Craig raised) - bind an object type to your own struct, so as to share it between multiple queries (I believe named fragments will address this case better, but it doesn't hurt to have options) - bind a GraphQL list type to a non-slice type in Go (presumably one with an UnmarshalJSON method), or any other different structure The latter three cases still have the sharp edge I was originally worried about, which is that nothing guarantees that the fields you request in the query are the ones the type expects to get. But I think it's worth having the option, with appropriate disclaimers. The main change to help support that better is that you can now specify the type inline in the query, as an alternative to specifying it in the config file; this means you might map a given object to a given struct, but only in some cases, and when you do you have a chance to look at the list of fields you're requesting. Additionally, I renamed the config field from "scalars" to "bindings" (but mentioned it in a few places where you might go looking for how to map scalars, most importantly the error message you get for an unknown (custom) scalar). While I was making a breaking change, I also changed it to be a `map[string]<struct>` instead of a `map[string]string`, because I expect to add more fields soon, e.g. to handle issue #38. Finally, since the feature is now intended/documented, I added some tests, although it's honestly quite simple on the genqlient side. ## Test plan: make tesc Author: benjaminjkraft Reviewers: csilvers, aberkan, dnerdy, MiguelCastillo Required Reviewers: Approved by: csilvers 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/69
318 lines
11 KiB
Go
318 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
|
|
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 // i.e. the field's alias in this query
|
|
GraphQLName string // i.e. the field's name in its type-def
|
|
Description string
|
|
}
|
|
|
|
func isAbstract(typ goType) bool {
|
|
_, ok := typ.Unwrap().(*goInterfaceType)
|
|
return ok
|
|
}
|
|
|
|
func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
|
|
description := typ.Description
|
|
if typ.Incomplete {
|
|
// 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.",
|
|
typ.GoName, typ.GraphQLName)
|
|
if description != "" {
|
|
description = fmt.Sprintf(
|
|
"%v\nThe GraphQL type's documentation follows.\n\n%v",
|
|
prefix, description)
|
|
} else {
|
|
description = prefix
|
|
}
|
|
}
|
|
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 isAbstract(field.GoType) {
|
|
// 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 {
|
|
if isAbstract(field.GoType) {
|
|
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
|
|
// Fields shared by all the interface's implementations;
|
|
// we'll generate getter methods for each.
|
|
SharedFields []*goStructField
|
|
Implementations []*goStructType
|
|
}
|
|
|
|
func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error {
|
|
goTypeNames := make([]string, len(typ.Implementations))
|
|
for i, impl := range typ.Implementations {
|
|
goTypeNames[i] = impl.Reference()
|
|
}
|
|
|
|
description := fmt.Sprintf(
|
|
"%v includes the requested fields of the GraphQL interface %v.\n\n"+
|
|
"%v is implemented by the following types:\n\t%v",
|
|
typ.GoName, typ.GraphQLName, typ.GoName, strings.Join(goTypeNames, "\n\t"))
|
|
if description != "" {
|
|
description = fmt.Sprintf(
|
|
"%v\n\nThe GraphQL type's documentation follows.\n\n%v",
|
|
description, typ.Description)
|
|
}
|
|
writeDescription(w, description)
|
|
|
|
// 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 {
|
|
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 {
|
|
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"))
|
|
}
|
|
}
|
|
}
|