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 }