Files
genqlient/generate/convert.go
T
Ben Kraft 2eba9a2c30 Reorganize documentation to make room to grow (#84)
## Summary:
In this commit I reorganize much of our documentation into a new `docs`
directory, where there will hopefully be more room to grow and to
organize things in a user-friendly way.  There's almost no net-new
documentation, although of course it's a great time to review it anyway.

In particular:
- I moved the documentation for the `genqlient.yaml` config file into an
  example file instead of GoDoc (which now just points to the example
  file); I think this will be a lot clearer for casual users.
- I moved the documentation for the `@genqlient` directive out of GoDoc
  and into a GraphQL schema file (since while it's a comment it's all
  real syntax), likewise, and made the `GenqlientDirective` type private
  (since there's now nothing useful to do with it).
- I moved `DESIGN.md` and the logo into `docs/` (just to keep the
  toplevel a bit cleaner), and separated the Contributing section of the
  README into `docs/CONTRIBUTING.md` (which github will automatically
  link on various issue and PR pages).

This leaves it so that:
- README.md is the only documentation at the toplevel (and will become
  just the high-level introduction as I add more user docs to `docs/`)
- GoDoc is only documentation for if you want to call genqlient
  programmatically (which is fairly limited as the API surface is quite
  small: it's now just Main, Generate, and Config, plus a constructor, a
  single method, and a bunch of fields on the latter)

In future commits, I'll add some more new documentation to the `docs`
directory.

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

## Test plan:
make check (and read the docs)


Author: benjaminjkraft

Reviewers: jvoll, benjaminjkraft, aberkan, dnerdy, MiguelCastillo, mahtabsabet

Required Reviewers: 

Approved By: jvoll

Checks:  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Lint

Pull Request URL: https://github.com/Khan/genqlient/pull/84
2021-09-10 16:03:30 -07:00

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 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
}