673840e495
* Picking some nits that my IDE complained about Signed-off-by: Steve Coffman <steve@khanacademy.org> * Update graphql/util.go Co-authored-by: Ben Kraft <benkraft@khanacademy.org> * revert to original for comment Signed-off-by: Steve Coffman <steve@khanacademy.org> Co-authored-by: Ben Kraft <benkraft@khanacademy.org>
631 lines
21 KiB
Go
631 lines
21 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"
|
|
|
|
"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)
|
|
}
|
|
|
|
// Instead of calling out to convertType/convertDefinition, we do our own
|
|
// thing, because we want to do a few things differently, and because we
|
|
// know we have an object type, so we can include only that case.
|
|
fields, err := g.convertSelectionSet(
|
|
newPrefixList(operation.Name), operation.SelectionSet, baseType, queryOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
goType := &goStructType{
|
|
GoName: name,
|
|
descriptionInfo: descriptionInfo{
|
|
CommentOverride: fmt.Sprintf(
|
|
"%v is returned by %v on success.", name, operation.Name),
|
|
GraphQLName: baseType.Name,
|
|
// omit the GraphQL description for baseType; it's uninteresting.
|
|
},
|
|
Fields: fields,
|
|
}
|
|
g.typeMap[name] = goType
|
|
|
|
return goType, nil
|
|
}
|
|
|
|
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",
|
|
}
|
|
|
|
// convertInputType decides the Go type we will generate corresponding to an
|
|
// argument to a GraphQL operation.
|
|
func (g *generator) convertInputType(
|
|
typ *ast.Type,
|
|
options, queryOptions *genqlientDirective,
|
|
) (goType, error) {
|
|
// note prefix is ignored here (see generator.typeName), as is selectionSet
|
|
// (for input types we use the whole thing).
|
|
return g.convertType(nil, 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(
|
|
namePrefix *prefixList,
|
|
typ *ast.Type,
|
|
selectionSet ast.SelectionSet,
|
|
options, queryOptions *genqlientDirective,
|
|
) (goType, error) {
|
|
// We check for local bindings here, so that you can bind, say, a
|
|
// `[String!]` to a struct instead of a slice. Global bindings can only
|
|
// bind GraphQL named types, at least for now.
|
|
localBinding := options.Bind
|
|
if localBinding != "" && localBinding != "-" {
|
|
goRef, err := g.addRef(localBinding)
|
|
return &goOpaqueType{goRef}, err
|
|
}
|
|
|
|
if typ.Elem != nil {
|
|
// Type is a list.
|
|
elem, err := g.convertType(
|
|
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(
|
|
namePrefix, def, typ.Position, selectionSet, options, 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(
|
|
namePrefix *prefixList,
|
|
def *ast.Definition,
|
|
pos *ast.Position,
|
|
selectionSet ast.SelectionSet,
|
|
options, queryOptions *genqlientDirective,
|
|
) (goType, error) {
|
|
// Check if we should use an existing type. (This is usually true for
|
|
// GraphQL scalars, but we allow you to bind non-scalar types too, if you
|
|
// want, subject to the caveats described in Config.Bindings.) Local
|
|
// bindings are checked in the caller (convertType) and never get here,
|
|
// unless the binding is "-" which means "ignore the global binding".
|
|
globalBinding, ok := g.Config.Bindings[def.Name]
|
|
if ok && options.Bind != "-" {
|
|
if def.Kind == ast.Object || def.Kind == ast.Interface || def.Kind == ast.Union {
|
|
err := g.validateBindingSelection(
|
|
def.Name, globalBinding, pos, selectionSet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
goRef, err := g.addRef(globalBinding.Type)
|
|
return &goOpaqueType{goRef}, err
|
|
}
|
|
goBuiltinName, ok := builtinTypes[def.Name]
|
|
if ok {
|
|
return &goOpaqueType{goBuiltinName}, nil
|
|
}
|
|
|
|
desc := descriptionInfo{
|
|
// TODO(benkraft): Copy any comment above this selection-set?
|
|
GraphQLDescription: def.Description,
|
|
GraphQLName: def.Name,
|
|
}
|
|
|
|
switch def.Kind {
|
|
case ast.Object:
|
|
name := makeTypeName(namePrefix, def.Name)
|
|
|
|
fields, err := g.convertSelectionSet(
|
|
namePrefix, selectionSet, def, queryOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
goType := &goStructType{
|
|
GoName: name,
|
|
Fields: fields,
|
|
descriptionInfo: desc,
|
|
}
|
|
g.typeMap[name] = goType
|
|
return goType, nil
|
|
|
|
case ast.InputObject:
|
|
// If we're 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.
|
|
name := upperFirst(def.Name)
|
|
|
|
goType := &goStructType{
|
|
GoName: name,
|
|
Fields: make([]*goStructField, len(def.Fields)),
|
|
descriptionInfo: desc,
|
|
IsInput: true,
|
|
}
|
|
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(
|
|
namePrefix, 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:
|
|
name := makeTypeName(namePrefix, def.Name)
|
|
|
|
sharedFields, err := g.convertSelectionSet(
|
|
namePrefix, selectionSet, def, queryOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
implementationTypes := g.schema.GetPossibleTypes(def)
|
|
goType := &goInterfaceType{
|
|
GoName: name,
|
|
SharedFields: sharedFields,
|
|
Implementations: make([]*goStructType, len(implementationTypes)),
|
|
descriptionInfo: desc,
|
|
}
|
|
g.typeMap[name] = goType
|
|
|
|
for i, implDef := range implementationTypes {
|
|
// 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(
|
|
namePrefix, implDef, pos, selectionSet, options, 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:
|
|
// Like with InputObject, there's only one type we will ever generate
|
|
// for an enum.
|
|
name := upperFirst(def.Name)
|
|
|
|
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:
|
|
// (If you had an entry in bindings, we would have returned it above.)
|
|
return nil, errorf(
|
|
pos, `unknown scalar %v: please add it to "bindings" in genqlient.yaml`, def.Name)
|
|
default:
|
|
return nil, errorf(pos, "unexpected kind: %v", def.Kind)
|
|
}
|
|
}
|
|
|
|
// convertSelectionSet converts a GraphQL selection-set into a list of
|
|
// corresponding Go struct-fields (and their Go types)
|
|
//
|
|
// A selection-set is a list of fields within braces like `{ myField }`, as
|
|
// appears at the toplevel of a query, in a field's sub-selections, or within
|
|
// an inline or named fragment.
|
|
//
|
|
// containingTypedef is the type-def whose fields we are selecting, and may be
|
|
// an object type or an interface type. In the case of interfaces, we'll call
|
|
// convertSelectionSet once for the interface, and once for each
|
|
// implementation.
|
|
func (g *generator) convertSelectionSet(
|
|
namePrefix *prefixList,
|
|
selectionSet ast.SelectionSet,
|
|
containingTypedef *ast.Definition,
|
|
queryOptions *genqlientDirective,
|
|
) ([]*goStructField, error) {
|
|
fields := make([]*goStructField, 0, len(selectionSet))
|
|
for _, 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:
|
|
field, err := g.convertField(
|
|
namePrefix, selection, selectionOptions, queryOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fields = append(fields, field)
|
|
case *ast.FragmentSpread:
|
|
maybeField, err := g.convertFragmentSpread(selection, containingTypedef)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if maybeField != nil {
|
|
fields = append(fields, maybeField)
|
|
}
|
|
case *ast.InlineFragment:
|
|
// (Note this will return nil, nil if the fragment doesn't apply to
|
|
// this type.)
|
|
fragmentFields, err := g.convertInlineFragment(
|
|
namePrefix, selection, containingTypedef, queryOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fields = append(fields, fragmentFields...)
|
|
default:
|
|
return nil, errorf(nil, "invalid selection type: %T", selection)
|
|
}
|
|
}
|
|
|
|
// We need to deduplicate, if you asked for
|
|
// { id, id, id, ... on SubType { id } }
|
|
// (which, yes, is legal) we'll treat that as just { id }.
|
|
uniqFields := make([]*goStructField, 0, len(selectionSet))
|
|
fragmentNames := make(map[string]bool, len(selectionSet))
|
|
fieldNames := make(map[string]bool, len(selectionSet))
|
|
for _, field := range fields {
|
|
// If you embed a field twice via a named fragment, we keep both, even
|
|
// if there are complicated overlaps, since they are separate types to
|
|
// us. (See also the special handling for IsEmbedded in
|
|
// unmarshal.go.tmpl.)
|
|
//
|
|
// But if you spread the samenamed fragment twice, e.g.
|
|
// { ...MyFragment, ... on SubType { ...MyFragment } }
|
|
// we'll still deduplicate that.
|
|
if field.JSONName == "" {
|
|
name := field.GoType.Reference()
|
|
if fragmentNames[name] {
|
|
continue
|
|
}
|
|
uniqFields = append(uniqFields, field)
|
|
fragmentNames[name] = true
|
|
continue
|
|
}
|
|
|
|
// GraphQL (and, effectively, JSON) requires that all fields with the
|
|
// same alias (JSON-name) must be the same (i.e. refer to the same
|
|
// field), so that's how we deduplicate.
|
|
if fieldNames[field.JSONName] {
|
|
// GraphQL (and, effectively, JSON) forbids you from having two
|
|
// fields with the same alias (JSON-name) that refer to different
|
|
// GraphQL fields. But it does allow you to have the same field
|
|
// with different selections (subject to some additional rules).
|
|
// We say: that's too complicated! and allow duplicate fields
|
|
// only if they're "leaf" types (enum or scalar).
|
|
switch field.GoType.Unwrap().(type) {
|
|
case *goOpaqueType, *goEnumType:
|
|
// Leaf field; we can just deduplicate.
|
|
// Note GraphQL already guarantees that the conflicting field
|
|
// has scalar/enum type iff this field does:
|
|
// https://spec.graphql.org/draft/#SameResponseShape()
|
|
continue
|
|
case *goStructType, *goInterfaceType:
|
|
// TODO(benkraft): Keep track of the position of each
|
|
// selection, so we can put this error on the right line.
|
|
return nil, errorf(nil,
|
|
"genqlient doesn't allow duplicate fields with different selections "+
|
|
"(see https://github.com/Khan/genqlient/issues/64); "+
|
|
"duplicate field: %s.%s", containingTypedef.Name, field.JSONName)
|
|
default:
|
|
return nil, errorf(nil, "unexpected field-type: %T", field.GoType.Unwrap())
|
|
}
|
|
}
|
|
uniqFields = append(uniqFields, field)
|
|
fieldNames[field.JSONName] = true
|
|
}
|
|
return uniqFields, nil
|
|
}
|
|
|
|
// fragmentMatches returns true if the given fragment is "active" when applied
|
|
// to the given type.
|
|
//
|
|
// "Active" here means "the fragment's fields will be returned on all objects
|
|
// of the given type", which is true when the given type is or implements
|
|
// the fragment's type. This is distinct from the rules for when a fragment
|
|
// spread is legal, which is true when the fragment would be active for *any*
|
|
// of the concrete types the spread-context could have (see
|
|
// https://spec.graphql.org/draft/#sec-Fragment-Spreads or docs/DESIGN.md).
|
|
//
|
|
// containingTypedef is as described in convertInlineFragment, below.
|
|
// fragmentTypedef is the definition of the fragment's type-condition, i.e. the
|
|
// definition of MyType in a fragment `on MyType`.
|
|
func fragmentMatches(containingTypedef, fragmentTypedef *ast.Definition) bool {
|
|
if containingTypedef.Name == fragmentTypedef.Name {
|
|
return true
|
|
}
|
|
for _, iface := range containingTypedef.Interfaces {
|
|
// Note we don't need to recurse into the interfaces here, because in
|
|
// GraphQL types must list all the interfaces they implement, including
|
|
// all types those interfaces implement [1]. Actually, at present
|
|
// gqlparser doesn't even support interfaces implementing other
|
|
// interfaces, but our code would handle that too.
|
|
// [1] https://spec.graphql.org/draft/#sec-Interfaces.Interfaces-Implementing-Interfaces
|
|
if iface == fragmentTypedef.Name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// convertInlineFragment converts a single GraphQL inline fragment
|
|
// (`... on MyType { myField }`) into Go struct-fields.
|
|
//
|
|
// containingTypedef is the type-def corresponding to the type into which we
|
|
// are spreading; it may be either an interface type (when spreading into one)
|
|
// or an object type (when writing the implementations of such an interface, or
|
|
// when using an inline fragment in an object type which is rare). If the
|
|
// given fragment does not apply to that type, this function returns nil, nil.
|
|
//
|
|
// In general, we treat such fragments' fields as if they were fields of the
|
|
// parent selection-set (except of course they are only included in types the
|
|
// fragment matches); see docs/DESIGN.md for more.
|
|
func (g *generator) convertInlineFragment(
|
|
namePrefix *prefixList,
|
|
fragment *ast.InlineFragment,
|
|
containingTypedef *ast.Definition,
|
|
queryOptions *genqlientDirective,
|
|
) ([]*goStructField, error) {
|
|
// You might think fragmentTypedef would be a fragment.ObjectDefinition, but
|
|
// actually that's the type into which the fragment is spread.
|
|
fragmentTypedef := g.schema.Types[fragment.TypeCondition]
|
|
if !fragmentMatches(containingTypedef, fragmentTypedef) {
|
|
return nil, nil
|
|
}
|
|
return g.convertSelectionSet(namePrefix, fragment.SelectionSet,
|
|
containingTypedef, queryOptions)
|
|
}
|
|
|
|
// convertFragmentSpread converts a single GraphQL fragment-spread
|
|
// (`...MyFragment`) into a Go struct-field. If the fragment does not apply to
|
|
// this type, returns nil.
|
|
//
|
|
// containingTypedef is as described in convertInlineFragment, above.
|
|
func (g *generator) convertFragmentSpread(
|
|
fragmentSpread *ast.FragmentSpread,
|
|
containingTypedef *ast.Definition,
|
|
) (*goStructField, error) {
|
|
if !fragmentMatches(containingTypedef, fragmentSpread.Definition.Definition) {
|
|
return nil, nil
|
|
}
|
|
|
|
typ, ok := g.typeMap[fragmentSpread.Name]
|
|
if !ok {
|
|
// If we haven't yet, convert the fragment itself. Note that fragments
|
|
// aren't allowed to have cycles, so this won't recurse forever.
|
|
var err error
|
|
typ, err = g.convertNamedFragment(fragmentSpread.Definition)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
iface, ok := typ.(*goInterfaceType)
|
|
if ok && containingTypedef.Kind == ast.Object {
|
|
// If the containing type is concrete, and the fragment spread is
|
|
// abstract, refer directly to the appropriate implementation, to save
|
|
// the caller having to do type-assertions that will always succeed.
|
|
//
|
|
// That is, if you do
|
|
// fragment F on I { ... }
|
|
// query Q { a { ...F } }
|
|
// for the fragment we generate
|
|
// type F interface { ... }
|
|
// type FA struct { ... }
|
|
// // (other implementations)
|
|
// when you spread F into a context of type A, we embed FA, not F.
|
|
for _, impl := range iface.Implementations {
|
|
if impl.GraphQLName == containingTypedef.Name {
|
|
typ = impl
|
|
}
|
|
}
|
|
}
|
|
|
|
return &goStructField{GoName: "" /* i.e. embedded */, GoType: typ}, nil
|
|
}
|
|
|
|
// convertNamedFragment converts a single GraphQL named fragment-definition
|
|
// (`fragment MyFragment on MyType { ... }`) into a Go struct.
|
|
func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goType, error) {
|
|
typ := g.schema.Types[fragment.TypeCondition]
|
|
|
|
comment, directive, err := g.parsePrecedingComment(fragment, fragment.Position)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
desc := descriptionInfo{
|
|
CommentOverride: comment,
|
|
GraphQLName: typ.Name,
|
|
GraphQLDescription: typ.Description,
|
|
FragmentName: fragment.Name,
|
|
}
|
|
|
|
// The rest basically follows how we convert a definition, except that
|
|
// things like type-names are a bit different.
|
|
|
|
fields, err := g.convertSelectionSet(
|
|
newPrefixList(fragment.Name), fragment.SelectionSet, typ, directive)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch typ.Kind {
|
|
case ast.Object:
|
|
goType := &goStructType{
|
|
GoName: fragment.Name,
|
|
Fields: fields,
|
|
descriptionInfo: desc,
|
|
}
|
|
g.typeMap[fragment.Name] = goType
|
|
return goType, nil
|
|
case ast.Interface, ast.Union:
|
|
implementationTypes := g.schema.GetPossibleTypes(typ)
|
|
goType := &goInterfaceType{
|
|
GoName: fragment.Name,
|
|
SharedFields: fields,
|
|
Implementations: make([]*goStructType, len(implementationTypes)),
|
|
descriptionInfo: desc,
|
|
}
|
|
g.typeMap[fragment.Name] = goType
|
|
|
|
for i, implDef := range implementationTypes {
|
|
implFields, err := g.convertSelectionSet(
|
|
newPrefixList(fragment.Name), fragment.SelectionSet, implDef, directive)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
implDesc := desc
|
|
implDesc.GraphQLName = implDef.Name
|
|
|
|
implTyp := &goStructType{
|
|
GoName: fragment.Name + upperFirst(implDef.Name),
|
|
Fields: implFields,
|
|
descriptionInfo: implDesc,
|
|
}
|
|
goType.Implementations[i] = implTyp
|
|
g.typeMap[implTyp.GoName] = implTyp
|
|
}
|
|
|
|
return goType, nil
|
|
default:
|
|
return nil, errorf(fragment.Position, "invalid type for fragment: %v is a %v",
|
|
fragment.TypeCondition, typ.Kind)
|
|
}
|
|
}
|
|
|
|
// convertField converts a single GraphQL operation-field into a Go
|
|
// struct-field (and its 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 *prefixList,
|
|
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)
|
|
}
|
|
|
|
goName := upperFirst(field.Alias)
|
|
namePrefix = nextPrefix(namePrefix, field)
|
|
|
|
fieldGoType, err := g.convertType(
|
|
namePrefix, field.Definition.Type, 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
|
|
}
|