It feels like just yesterday, but it's been over four months since our last release! So it's as good a time as any; while there are quite a few changes they're individually mostly small. As usual, this updates the changelog, and I'll tag it with the release once it lands. Test plan: no relevant bug reports lately
430 lines
14 KiB
Go
430 lines
14 KiB
Go
package generate
|
|
|
|
// This file implements the main entrypoint and framework for the genqlient
|
|
// code-generation process. See comments in Generate for the high-level
|
|
// overview.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"go/format"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/vektah/gqlparser/v2/ast"
|
|
"github.com/vektah/gqlparser/v2/formatter"
|
|
"github.com/vektah/gqlparser/v2/validator"
|
|
"golang.org/x/tools/imports"
|
|
)
|
|
|
|
// generator is the context for the codegen process (and ends up getting passed
|
|
// to the template).
|
|
type generator struct {
|
|
// The config for which we are generating code.
|
|
Config *Config
|
|
// The list of operations for which to generate code.
|
|
Operations []*operation
|
|
// The types needed for these operations.
|
|
typeMap map[string]goType
|
|
// Imports needed for these operations, path -> alias and alias -> true
|
|
imports map[string]string
|
|
usedAliases map[string]bool
|
|
// True if we've already written out the imports (in which case they can't
|
|
// be modified).
|
|
importsLocked bool
|
|
// Cache of loaded templates.
|
|
templateCache map[string]*template.Template
|
|
// Schema we are generating code against
|
|
schema *ast.Schema
|
|
// Named fragments (map by name), so we can look them up from spreads.
|
|
// TODO(benkraft): In theory we shouldn't need this, we can just use
|
|
// ast.FragmentSpread.Definition, but for some reason it doesn't seem to be
|
|
// set consistently, even post-validation.
|
|
fragments map[string]*ast.FragmentDefinition
|
|
}
|
|
|
|
// JSON tags in operation are for ExportOperations (see Config for details).
|
|
type operation struct {
|
|
// The type of the operation (query, mutation, or subscription).
|
|
Type ast.Operation `json:"-"`
|
|
// The name of the operation, from GraphQL.
|
|
Name string `json:"operationName"`
|
|
// The documentation for the operation, from GraphQL.
|
|
Doc string `json:"-"`
|
|
// The body of the operation to send.
|
|
Body string `json:"query"`
|
|
// The type of the argument to the operation, which we use both internally
|
|
// and to construct the arguments. We do it this way so we can use the
|
|
// machinery we have for handling (and, specifically, json-marshaling)
|
|
// types.
|
|
Input *goStructType `json:"-"`
|
|
// The type-name for the operation's response type.
|
|
ResponseName string `json:"-"`
|
|
// The original filename from which we got this query.
|
|
SourceFilename string `json:"sourceLocation"`
|
|
// The config within which we are generating code.
|
|
Config *Config `json:"-"`
|
|
}
|
|
|
|
type exportedOperations struct {
|
|
Operations []*operation `json:"operations"`
|
|
}
|
|
|
|
func newGenerator(
|
|
config *Config,
|
|
schema *ast.Schema,
|
|
fragments ast.FragmentDefinitionList,
|
|
) *generator {
|
|
g := generator{
|
|
Config: config,
|
|
typeMap: map[string]goType{},
|
|
imports: map[string]string{},
|
|
usedAliases: map[string]bool{},
|
|
templateCache: map[string]*template.Template{},
|
|
schema: schema,
|
|
fragments: make(map[string]*ast.FragmentDefinition, len(fragments)),
|
|
}
|
|
|
|
for _, fragment := range fragments {
|
|
g.fragments[fragment.Name] = fragment
|
|
}
|
|
|
|
return &g
|
|
}
|
|
|
|
func (g *generator) WriteTypes(w io.Writer) error {
|
|
names := make([]string, 0, len(g.typeMap))
|
|
for name := range g.typeMap {
|
|
names = append(names, name)
|
|
}
|
|
// Sort alphabetically by type-name. Sorting somehow deterministically is
|
|
// important to ensure generated code is deterministic. Alphabetical is
|
|
// nice because it's easy, and in the current naming scheme, it's even
|
|
// vaguely aligned to the structure of the queries.
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
err := g.typeMap[name].WriteDefinition(w, g)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Make sure we have blank lines between types (and between the last
|
|
// type and the first operation)
|
|
_, err = io.WriteString(w, "\n\n")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// usedFragmentNames returns the named-fragments used by (i.e. spread into)
|
|
// this operation.
|
|
func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList {
|
|
var retval, queue ast.FragmentDefinitionList
|
|
seen := map[string]bool{}
|
|
|
|
var observers validator.Events
|
|
// Fragment-spreads are easy to find; just ask for them!
|
|
observers.OnFragmentSpread(func(_ *validator.Walker, fragmentSpread *ast.FragmentSpread) {
|
|
if seen[fragmentSpread.Name] {
|
|
return
|
|
}
|
|
def := g.fragments[fragmentSpread.Name]
|
|
seen[fragmentSpread.Name] = true
|
|
retval = append(retval, def)
|
|
queue = append(queue, def)
|
|
})
|
|
|
|
doc := ast.QueryDocument{Operations: ast.OperationList{op}}
|
|
validator.Walk(g.schema, &doc, &observers)
|
|
// Well, easy-ish: we also have to look recursively.
|
|
// Note GraphQL guarantees there are no cycles among fragments:
|
|
// https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles
|
|
for len(queue) > 0 {
|
|
doc = ast.QueryDocument{Fragments: ast.FragmentDefinitionList{queue[0]}}
|
|
validator.Walk(g.schema, &doc, &observers) // traversal is the same
|
|
queue = queue[1:]
|
|
}
|
|
|
|
return retval
|
|
}
|
|
|
|
// Preprocess each query to make any changes that genqlient needs.
|
|
//
|
|
// At present, the only change is that we add __typename, if not already
|
|
// requested, to each field of interface type, so we can use the right types
|
|
// when unmarshaling.
|
|
func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
|
|
var observers validator.Events
|
|
// We want to ensure that everywhere you ask for some list of fields (a
|
|
// selection-set) from an interface (or union) type, you ask for its
|
|
// __typename field. There are four places we might find a selection-set:
|
|
// at the toplevel of a query, on a field, or in an inline or named
|
|
// fragment. The toplevel of a query must be an object type, so we don't
|
|
// need to consider that. And fragments must (if used at all) be spread
|
|
// into some parent selection-set, so we'll add __typename there (if
|
|
// needed). Note this does mean abstract-typed fragments spread into
|
|
// object-typed scope will *not* have access to `__typename`, but they
|
|
// indeed don't need it, since we do know the type in that context.
|
|
// TODO(benkraft): We should omit __typename if you asked for
|
|
// `# @genqlient(struct: true)`.
|
|
observers.OnField(func(_ *validator.Walker, field *ast.Field) {
|
|
// We are interested in a field from the query like
|
|
// field { subField ... }
|
|
// where the schema looks like
|
|
// type ... { # or interface/union
|
|
// field: FieldType # or [FieldType!]! etc.
|
|
// }
|
|
// interface FieldType { # or union
|
|
// subField: ...
|
|
// }
|
|
// If FieldType is an interface/union, and none of the subFields is
|
|
// __typename, we want to change the query to
|
|
// field { __typename subField ... }
|
|
|
|
fieldType := g.schema.Types[field.Definition.Type.Name()]
|
|
if fieldType.Kind != ast.Interface && fieldType.Kind != ast.Union {
|
|
return // a concrete type
|
|
}
|
|
|
|
hasTypename := false
|
|
for _, selection := range field.SelectionSet {
|
|
// Check if we already selected __typename. We ignore fragments,
|
|
// because we want __typename as a toplevel field.
|
|
subField, ok := selection.(*ast.Field)
|
|
if ok && subField.Name == "__typename" {
|
|
hasTypename = true
|
|
}
|
|
}
|
|
if !hasTypename {
|
|
// Ok, we need to add the field!
|
|
field.SelectionSet = append(ast.SelectionSet{
|
|
&ast.Field{
|
|
Alias: "__typename", Name: "__typename",
|
|
// Fake definition for the magic field __typename cribbed
|
|
// from gqlparser's validator/walk.go, equivalent to
|
|
// __typename: String
|
|
// TODO(benkraft): This should in principle be
|
|
// __typename: String!
|
|
// But genqlient doesn't care, so we just match gqlparser.
|
|
Definition: &ast.FieldDefinition{
|
|
Name: "__typename",
|
|
Type: ast.NamedType("String", nil /* pos */),
|
|
},
|
|
// Definition of the object that contains this field, i.e.
|
|
// FieldType.
|
|
ObjectDefinition: fieldType,
|
|
},
|
|
}, field.SelectionSet...)
|
|
}
|
|
})
|
|
validator.Walk(g.schema, doc, &observers)
|
|
}
|
|
|
|
// validateOperation checks for a few classes of operations that gqlparser
|
|
// considers valid but we don't allow, and returns an error if this operation
|
|
// is invalid for genqlient's purposes.
|
|
func (g *generator) validateOperation(op *ast.OperationDefinition) error {
|
|
_, err := g.baseTypeForOperation(op.Operation)
|
|
if err != nil {
|
|
// (e.g. operation has subscriptions, which we don't support)
|
|
return err
|
|
}
|
|
|
|
if op.Name == "" {
|
|
return errorf(op.Position, "operations must have operation-names")
|
|
} else if goKeywords[op.Name] {
|
|
return errorf(op.Position, "operation name must not be a go keyword")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addOperation adds to g.Operations the information needed to generate a
|
|
// genqlient entrypoint function for the given operation. It also adds to
|
|
// g.typeMap any types referenced by the operation, except for types belonging
|
|
// to named fragments, which are added separately by Generate via
|
|
// convertFragment.
|
|
func (g *generator) addOperation(op *ast.OperationDefinition) error {
|
|
if err := g.validateOperation(op); err != nil {
|
|
return err
|
|
}
|
|
|
|
queryDoc := &ast.QueryDocument{
|
|
Operations: ast.OperationList{op},
|
|
Fragments: g.usedFragments(op),
|
|
}
|
|
g.preprocessQueryDocument(queryDoc)
|
|
|
|
var builder strings.Builder
|
|
f := formatter.NewFormatter(&builder)
|
|
f.FormatQueryDocument(queryDoc)
|
|
|
|
commentLines, directive, err := g.parsePrecedingComment(op, nil, op.Position, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
inputType, err := g.convertArguments(op, directive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
responseType, err := g.convertOperation(op, directive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var docComment string
|
|
if len(commentLines) > 0 {
|
|
docComment = "// " + strings.ReplaceAll(commentLines, "\n", "\n// ")
|
|
}
|
|
|
|
// If the filename is a pseudo-filename filename.go:startline, just
|
|
// put the filename in the export; we don't figure out the line offset
|
|
// anyway, and if you want to check those exports in they will change a
|
|
// lot if they have line numbers.
|
|
// TODO: refactor to use the errorPos machinery for this
|
|
sourceFilename := op.Position.Src.Name
|
|
if i := strings.LastIndex(sourceFilename, ":"); i != -1 {
|
|
sourceFilename = sourceFilename[:i]
|
|
}
|
|
|
|
g.Operations = append(g.Operations, &operation{
|
|
Type: op.Operation,
|
|
Name: op.Name,
|
|
Doc: docComment,
|
|
// The newline just makes it format a little nicer. We add it here
|
|
// rather than in the template so exported operations will match
|
|
// *exactly* what we send to the server.
|
|
Body: "\n" + builder.String(),
|
|
Input: inputType,
|
|
ResponseName: responseType.Reference(),
|
|
SourceFilename: sourceFilename,
|
|
Config: g.Config, // for the convenience of the template
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Generate is the main programmatic entrypoint to genqlient, and generates and
|
|
// returns Go source code based on the given configuration.
|
|
//
|
|
// See Config for more on creating a configuration. The return value is a map
|
|
// from filename to the generated file-content (e.g. Go source). Callers who
|
|
// don't want to manage reading and writing the files should call Main.
|
|
func Generate(config *Config) (map[string][]byte, error) {
|
|
// Step 1: Read in the schema and operations from the files defined by the
|
|
// config (and validate the operations against the schema). This is all
|
|
// defined in parse.go.
|
|
schema, err := getSchema(config.Schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
document, err := getAndValidateQueries(config.baseDir, config.Operations, schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(benkraft): we could also allow this, and generate an empty file
|
|
// with just the package-name, if it turns out to be more convenient that
|
|
// way. (As-is, we generate a broken file, with just (unused) imports.)
|
|
if len(document.Operations) == 0 {
|
|
// Hard to have a position when there are no operations :(
|
|
return nil, errorf(nil,
|
|
"no queries found, looked in: %v (configure this in genqlient.yaml)",
|
|
strings.Join(config.Operations, ", "))
|
|
}
|
|
|
|
// Step 2: For each operation and fragment, convert it into data structures
|
|
// representing Go types (defined in types.go). The bulk of this logic is
|
|
// in convert.go, and it additionally updates g.typeMap to include all the
|
|
// types it needs.
|
|
g := newGenerator(config, schema, document.Fragments)
|
|
for _, op := range document.Operations {
|
|
if err = g.addOperation(op); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Step 3: Glue it all together!
|
|
//
|
|
// First, write the types (from g.typeMap) and operations to a temporary
|
|
// buffer, since they affect what imports we'll put in the header.
|
|
var bodyBuf bytes.Buffer
|
|
err = g.WriteTypes(&bodyBuf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sort operations to guarantee a stable order
|
|
sort.Slice(g.Operations, func(i, j int) bool {
|
|
return g.Operations[i].Name < g.Operations[j].Name
|
|
})
|
|
|
|
for _, operation := range g.Operations {
|
|
err = g.render("operation.go.tmpl", &bodyBuf, operation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// The header also needs to reference some context types, which it does
|
|
// after it writes the imports, so we need to preregister those imports.
|
|
if g.Config.ContextType != "-" {
|
|
_, err = g.ref("context.Context")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if g.Config.ContextType != "context.Context" {
|
|
_, err = g.ref(g.Config.ContextType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now really glue it all together, and format.
|
|
var buf bytes.Buffer
|
|
err = g.render("header.go.tmpl", &buf, g)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = io.Copy(&buf, &bodyBuf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unformatted := buf.Bytes()
|
|
formatted, err := format.Source(unformatted)
|
|
if err != nil {
|
|
return nil, goSourceError("gofmt", unformatted, err)
|
|
}
|
|
importsed, err := imports.Process(config.Generated, formatted, nil)
|
|
if err != nil {
|
|
return nil, goSourceError("goimports", formatted, err)
|
|
}
|
|
|
|
retval := map[string][]byte{
|
|
config.Generated: importsed,
|
|
}
|
|
|
|
if config.ExportOperations != "" {
|
|
// We use MarshalIndent so that the file is human-readable and
|
|
// slightly more likely to be git-mergeable (if you check it in). In
|
|
// general it's never going to be used anywhere where space is an
|
|
// issue -- it doesn't go in your binary or anything.
|
|
retval[config.ExportOperations], err = json.MarshalIndent(
|
|
exportedOperations{Operations: g.Operations}, "", " ")
|
|
if err != nil {
|
|
return nil, errorf(nil, "unable to export queries: %v", err)
|
|
}
|
|
}
|
|
|
|
return retval, nil
|
|
}
|