Files
genqlient/generate/convert.go
T
Ben Kraft bf266d874d [🔥AUDIT🔥] Mark interfaces as enabled! (#67)
🖍 _This is an audit!_ 🖍

## Summary:
We've completed support for interfaces!  Fragments are still pending,
but interfaces by themselves should be usable.  But I forgot to actually
remove the flag they were behind.  In this commit I do!

Issue: https://khanacademy.slack.com/archives/C01120CNCS0/p1630012808004100

## Test plan:
make check


Author: benjaminjkraft

Auditors: aberkan, csilvers, dnerdy, MiguelCastillo

Required Reviewers: 

Approved by: 

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/67
2021-08-26 14:35:54 -07:00

393 lines
13 KiB
Go

package generate
// This file implements the core type-generation logic of genqlient, whereby we
// traverse an operation-definition (and the schema against which it will be
// executed), and convert that into Go types. It returns data structures
// representing the types to be generated; these are defined, and converted
// into code, in types.go.
//
// The entrypoints are convertOperation, which builds the response-type for a
// query, and convertInputType, which builds the argument-types.
import (
"fmt"
"strings"
"github.com/vektah/gqlparser/v2/ast"
)
// baseTypeForOperation returns the definition of the GraphQL type to which the
// root of the operation corresponds, e.g. the "Query" or "Mutation" type.
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 !g.Config.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)
}
}
// convertOperation builds the response-type into which the given operation's
// result will be unmarshaled.
func (g *generator) convertOperation(
operation *ast.OperationDefinition,
queryOptions *GenqlientDirective,
) (goType, error) {
name := operation.Name + "Response"
if def, ok := g.typeMap[name]; ok {
return nil, errorf(operation.Position, "%s defined twice:\n%s", name, def)
}
baseType, err := g.baseTypeForOperation(operation.Operation)
if err != nil {
return nil, errorf(operation.Position, "%v", err)
}
goTyp, err := g.convertDefinition(
name, operation.Name, baseType, operation.Position,
operation.SelectionSet, queryOptions)
if structType, ok := goTyp.(*goStructType); ok {
// Override the ordinary description; the GraphQL documentation for
// Query/Mutation is unlikely to be of value.
// TODO(benkraft): This is a bit awkward/fragile.
structType.Description =
fmt.Sprintf("%v is returned by %v on success.", name, operation.Name)
structType.Incomplete = false
}
return goTyp, 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",
}
// typeName computes the name, in Go, that we should use for the given
// GraphQL type definition. This is dependent on its location within the query
// (see DESIGN.md for more on why we generate type-names this way), which is
// determined by the prefix argument; the nextPrefix result should be passed to
// calls to typeName on any child types.
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
}
// convertInputType decides the Go type we will generate corresponding to an
// argument to a GraphQL operation.
func (g *generator) convertInputType(
opName string,
typ *ast.Type,
options, queryOptions *GenqlientDirective,
) (goType, error) {
// Sort of a hack: case the input type name to match the op-name.
// TODO(benkraft): this is another thing that breaks the assumption that we
// only need one of an input type, albeit in a relatively safe way.
name := matchFirst(typ.Name(), opName)
// note prefix is ignored here (see generator.typeName), as is selectionSet
// (for input types we use the whole thing)).
return g.convertType(name, "", typ, nil, options, queryOptions)
}
// convertType decides the Go type we will generate corresponding to a
// particular GraphQL type. In this context, "type" represents the type of a
// field, and may be a list or a reference to a named type, with or without the
// "non-null" annotation.
func (g *generator) convertType(
name, namePrefix string,
typ *ast.Type,
selectionSet ast.SelectionSet,
options, queryOptions *GenqlientDirective,
) (goType, error) {
if typ.Elem != nil {
// Type is a list.
elem, err := g.convertType(
name, namePrefix, typ.Elem, selectionSet, options, queryOptions)
return &goSliceType{elem}, err
}
// If this is a builtin type or custom scalar, just refer to it.
def := g.schema.Types[typ.Name()]
goTyp, err := g.convertDefinition(
name, namePrefix, def, typ.Position, selectionSet, queryOptions)
if options.GetPointer() {
// Whatever we get, wrap it in a pointer. (Because of the way the
// options work, recursing here isn't as connvenient.)
// Note this does []*T or [][]*T, not e.g. *[][]T. See #16.
goTyp = &goPointerType{goTyp}
}
return goTyp, err
}
// convertDefinition decides the Go type we will generate corresponding to a
// particular GraphQL named type.
//
// In this context, "definition" (and "named type") refer to an
// *ast.Definition, which represents the definition of a type in the GraphQL
// schema, which may be referenced by a field-type (see convertType).
func (g *generator) convertDefinition(
name, namePrefix string,
def *ast.Definition,
pos *ast.Position,
selectionSet ast.SelectionSet,
queryOptions *GenqlientDirective,
) (goType, error) {
qualifiedGoName, ok := g.Config.Scalars[def.Name]
if ok {
goRef, err := g.addRef(qualifiedGoName)
return &goOpaqueType{goRef}, err
}
goBuiltinName, ok := builtinTypes[def.Name]
if ok {
return &goOpaqueType{goBuiltinName}, nil
}
switch def.Kind {
case ast.Object:
goType := &goStructType{
GoName: name,
Description: def.Description,
GraphQLName: def.Name,
Fields: make([]*goStructField, len(selectionSet)),
Incomplete: true,
}
g.typeMap[name] = goType
for i, selection := range selectionSet {
_, selectionDirective, err := g.parsePrecedingComment(
selection, selection.GetPosition())
if err != nil {
return nil, err
}
selectionOptions := queryOptions.merge(selectionDirective)
switch selection := selection.(type) {
case *ast.Field:
goType.Fields[i], err = g.convertField(
namePrefix, selection, selectionOptions, queryOptions)
if err != nil {
return nil, err
}
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 goType, nil
case ast.InputObject:
goType := &goStructType{
GoName: name,
Description: def.Description,
GraphQLName: def.Name,
Fields: make([]*goStructField, len(def.Fields)),
}
g.typeMap[name] = goType
for i, field := range def.Fields {
goName := upperFirst(field.Name)
// Several of the arguments don't really make sense here:
// - no field-specific options can apply, because this is
// a field in the type, not in the query (see also #14).
// - namePrefix is ignored for input types; see note in
// generator.typeName.
// TODO(benkraft): Can we refactor to avoid passing the values that
// will be ignored? We know field.Type is a scalar, enum, or input
// type. But plumbing that is a bit tricky in practice.
fieldGoType, err := g.convertType(
field.Type.Name(), "", field.Type, nil, queryOptions, queryOptions)
if err != nil {
return nil, err
}
goType.Fields[i] = &goStructField{
GoName: goName,
GoType: fieldGoType,
JSONName: field.Name,
GraphQLName: field.Name,
Description: field.Description,
}
}
return goType, nil
case ast.Interface, ast.Union:
implementationTypes := g.schema.GetPossibleTypes(def)
goType := &goInterfaceType{
GoName: name,
Description: def.Description,
GraphQLName: def.Name,
SharedFields: make([]*goStructField, 0, len(selectionSet)),
Implementations: make([]*goStructType, len(implementationTypes)),
}
g.typeMap[name] = goType
// TODO(benkraft): This sorta-duplicates what we'll do in each
// implementation when it traverses the fields. But they'll differ
// more once we support fragments; at that point we should figure out
// how to refactor.
for _, selection := range selectionSet {
field, ok := selection.(*ast.Field)
if !ok { // fragment/interface, not a shared field
continue
}
_, fieldDirective, err := g.parsePrecedingComment(field, field.GetPosition())
if err != nil {
return nil, err
}
fieldOptions := queryOptions.merge(fieldDirective)
goField, err := g.convertField(namePrefix, field, fieldOptions, queryOptions)
if err != nil {
return nil, err
}
goType.SharedFields = append(goType.SharedFields, goField)
}
for i, implDef := range implementationTypes {
// Note for shared fields we propagate forward the interface's
// name-prefix: that is, the implementations will have fields with
// types like
// MyInterfaceMyFieldMyType
// not
// MyInterfaceMyImplMyFieldMyType
// ^^^^^^
// In particular, this means that the Go type of MyField will be
// the same across all the implementations; this is important so
// that we can write a method GetMyField() that returns it!
implName, _ := g.typeName(namePrefix, implDef)
// TODO(benkraft): In principle we should skip generating a Go
// field for __typename each of these impl-defs if you didn't
// request it (and it was automatically added by
// preprocessQueryDocument). But in practice it doesn't really
// hurt, and would be extra work to avoid, so we just leave it.
implTyp, err := g.convertDefinition(
implName, namePrefix, implDef, pos, selectionSet, queryOptions)
if err != nil {
return nil, err
}
implStructTyp, ok := implTyp.(*goStructType)
if !ok { // (should never happen on a valid schema)
return nil, errorf(
pos, "interface %s had non-object implementation %s",
def.Name, implDef.Name)
}
goType.Implementations[i] = implStructTyp
}
return goType, nil
case ast.Enum:
goType := &goEnumType{
GoName: name,
Description: def.Description,
Values: make([]goEnumValue, len(def.EnumValues)),
}
g.typeMap[name] = goType
for i, val := range def.EnumValues {
goType.Values[i] = goEnumValue{Name: val.Name, Description: val.Description}
}
return goType, nil
case ast.Scalar:
return nil, errorf(
pos, "unknown scalar %v: please add it to genqlient.yaml", def.Name)
default:
return nil, errorf(pos, "unexpected kind: %v", def.Kind)
}
}
// convertField converts a single GraphQL operation-field into a GraphQL type.
//
// Note that input-type fields are handled separately (inline in
// convertDefinition), because they come from the type-definition, not the
// operation.
func (g *generator) convertField(
namePrefix string,
field *ast.Field,
fieldOptions, queryOptions *GenqlientDirective,
) (*goStructField, error) {
if field.Definition == nil {
// Unclear why gqlparser hasn't already rejected this,
// but empirically it might not.
return nil, errorf(
field.Position, "undefined field %v", field.Alias)
}
// Needs to be exported for JSON-marshaling
goName := upperFirst(field.Alias)
typ := field.Definition.Type
fieldTypedef := g.schema.Types[typ.Name()]
// 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 := g.typeName(namePrefix+goName, fieldTypedef)
fieldGoType, err := g.convertType(
name, namePrefix, typ, field.SelectionSet,
fieldOptions, queryOptions)
if err != nil {
return nil, err
}
return &goStructField{
GoName: goName,
GoType: fieldGoType,
JSONName: field.Alias,
GraphQLName: field.Name,
Description: field.Definition.Description,
}, nil
}