bf266d874d
🖍 _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
393 lines
13 KiB
Go
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
|
|
}
|