github.com/opiuman/genqlient@v1.0.0/generate/generate.go (about) 1 package generate 2 3 // This file implements the main entrypoint and framework for the genqlient 4 // code-generation process. See comments in Generate for the high-level 5 // overview. 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "go/format" 11 "io" 12 "sort" 13 "strings" 14 "text/template" 15 16 "github.com/vektah/gqlparser/v2/ast" 17 "github.com/vektah/gqlparser/v2/formatter" 18 "github.com/vektah/gqlparser/v2/validator" 19 "golang.org/x/tools/imports" 20 ) 21 22 // generator is the context for the codegen process (and ends up getting passed 23 // to the template). 24 type generator struct { 25 // The config for which we are generating code. 26 Config *Config 27 // The list of operations for which to generate code. 28 Operations []*operation 29 // The types needed for these operations. 30 typeMap map[string]goType 31 // Imports needed for these operations, path -> alias and alias -> true 32 imports map[string]string 33 usedAliases map[string]bool 34 // True if we've already written out the imports (in which case they can't 35 // be modified). 36 importsLocked bool 37 // Cache of loaded templates. 38 templateCache map[string]*template.Template 39 // Schema we are generating code against 40 schema *ast.Schema 41 // Named fragments (map by name), so we can look them up from spreads. 42 // TODO(benkraft): In theory we shouldn't need this, we can just use 43 // ast.FragmentSpread.Definition, but for some reason it doesn't seem to be 44 // set consistently, even post-validation. 45 fragments map[string]*ast.FragmentDefinition 46 } 47 48 // JSON tags in operation are for ExportOperations (see Config for details). 49 type operation struct { 50 // The type of the operation (query, mutation, or subscription). 51 Type ast.Operation `json:"-"` 52 // The name of the operation, from GraphQL. 53 Name string `json:"operationName"` 54 // The documentation for the operation, from GraphQL. 55 Doc string `json:"-"` 56 // The body of the operation to send. 57 Body string `json:"query"` 58 // The type of the argument to the operation, which we use both internally 59 // and to construct the arguments. We do it this way so we can use the 60 // machinery we have for handling (and, specifically, json-marshaling) 61 // types. 62 Input *goStructType `json:"-"` 63 // The type-name for the operation's response type. 64 ResponseName string `json:"-"` 65 // The original filename from which we got this query. 66 SourceFilename string `json:"sourceLocation"` 67 // The config within which we are generating code. 68 Config *Config `json:"-"` 69 } 70 71 type exportedOperations struct { 72 Operations []*operation `json:"operations"` 73 } 74 75 func newGenerator( 76 config *Config, 77 schema *ast.Schema, 78 fragments ast.FragmentDefinitionList, 79 ) *generator { 80 g := generator{ 81 Config: config, 82 typeMap: map[string]goType{}, 83 imports: map[string]string{}, 84 usedAliases: map[string]bool{}, 85 templateCache: map[string]*template.Template{}, 86 schema: schema, 87 fragments: make(map[string]*ast.FragmentDefinition, len(fragments)), 88 } 89 90 for _, fragment := range fragments { 91 g.fragments[fragment.Name] = fragment 92 } 93 94 return &g 95 } 96 97 func (g *generator) WriteTypes(w io.Writer) error { 98 names := make([]string, 0, len(g.typeMap)) 99 for name := range g.typeMap { 100 names = append(names, name) 101 } 102 // Sort alphabetically by type-name. Sorting somehow deterministically is 103 // important to ensure generated code is deterministic. Alphabetical is 104 // nice because it's easy, and in the current naming scheme, it's even 105 // vaguely aligned to the structure of the queries. 106 sort.Strings(names) 107 108 for _, name := range names { 109 err := g.typeMap[name].WriteDefinition(w, g) 110 if err != nil { 111 return err 112 } 113 // Make sure we have blank lines between types (and between the last 114 // type and the first operation) 115 _, err = io.WriteString(w, "\n\n") 116 if err != nil { 117 return err 118 } 119 } 120 return nil 121 } 122 123 // usedFragmentNames returns the named-fragments used by (i.e. spread into) 124 // this operation. 125 func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList { 126 var retval, queue ast.FragmentDefinitionList 127 seen := map[string]bool{} 128 129 var observers validator.Events 130 // Fragment-spreads are easy to find; just ask for them! 131 observers.OnFragmentSpread(func(_ *validator.Walker, fragmentSpread *ast.FragmentSpread) { 132 if seen[fragmentSpread.Name] { 133 return 134 } 135 def := g.fragments[fragmentSpread.Name] 136 seen[fragmentSpread.Name] = true 137 retval = append(retval, def) 138 queue = append(queue, def) 139 }) 140 141 doc := ast.QueryDocument{Operations: ast.OperationList{op}} 142 validator.Walk(g.schema, &doc, &observers) 143 // Well, easy-ish: we also have to look recursively. 144 // Note GraphQL guarantees there are no cycles among fragments: 145 // https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles 146 for len(queue) > 0 { 147 doc = ast.QueryDocument{Fragments: ast.FragmentDefinitionList{queue[0]}} 148 validator.Walk(g.schema, &doc, &observers) // traversal is the same 149 queue = queue[1:] 150 } 151 152 return retval 153 } 154 155 // Preprocess each query to make any changes that genqlient needs. 156 // 157 // At present, the only change is that we add __typename, if not already 158 // requested, to each field of interface type, so we can use the right types 159 // when unmarshaling. 160 func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) { 161 var observers validator.Events 162 // We want to ensure that everywhere you ask for some list of fields (a 163 // selection-set) from an interface (or union) type, you ask for its 164 // __typename field. There are four places we might find a selection-set: 165 // at the toplevel of a query, on a field, or in an inline or named 166 // fragment. The toplevel of a query must be an object type, so we don't 167 // need to consider that. And fragments must (if used at all) be spread 168 // into some parent selection-set, so we'll add __typename there (if 169 // needed). Note this does mean abstract-typed fragments spread into 170 // object-typed scope will *not* have access to `__typename`, but they 171 // indeed don't need it, since we do know the type in that context. 172 // TODO(benkraft): We should omit __typename if you asked for 173 // `# @genqlient(struct: true)`. 174 observers.OnField(func(_ *validator.Walker, field *ast.Field) { 175 // We are interested in a field from the query like 176 // field { subField ... } 177 // where the schema looks like 178 // type ... { # or interface/union 179 // field: FieldType # or [FieldType!]! etc. 180 // } 181 // interface FieldType { # or union 182 // subField: ... 183 // } 184 // If FieldType is an interface/union, and none of the subFields is 185 // __typename, we want to change the query to 186 // field { __typename subField ... } 187 188 fieldType := g.schema.Types[field.Definition.Type.Name()] 189 if fieldType.Kind != ast.Interface && fieldType.Kind != ast.Union { 190 return // a concrete type 191 } 192 193 hasTypename := false 194 for _, selection := range field.SelectionSet { 195 // Check if we already selected __typename. We ignore fragments, 196 // because we want __typename as a toplevel field. 197 subField, ok := selection.(*ast.Field) 198 if ok && subField.Name == "__typename" { 199 hasTypename = true 200 } 201 } 202 if !hasTypename { 203 // Ok, we need to add the field! 204 field.SelectionSet = append(ast.SelectionSet{ 205 &ast.Field{ 206 Alias: "__typename", Name: "__typename", 207 // Fake definition for the magic field __typename cribbed 208 // from gqlparser's validator/walk.go, equivalent to 209 // __typename: String 210 // TODO(benkraft): This should in principle be 211 // __typename: String! 212 // But genqlient doesn't care, so we just match gqlparser. 213 Definition: &ast.FieldDefinition{ 214 Name: "__typename", 215 Type: ast.NamedType("String", nil /* pos */), 216 }, 217 // Definition of the object that contains this field, i.e. 218 // FieldType. 219 ObjectDefinition: fieldType, 220 }, 221 }, field.SelectionSet...) 222 } 223 }) 224 validator.Walk(g.schema, doc, &observers) 225 } 226 227 // validateOperation checks for a few classes of operations that gqlparser 228 // considers valid but we don't allow, and returns an error if this operation 229 // is invalid for genqlient's purposes. 230 func (g *generator) validateOperation(op *ast.OperationDefinition) error { 231 opType, err := g.baseTypeForOperation(op.Operation) 232 switch { 233 case err != nil: 234 // (e.g. operation has subscriptions, which we don't support) 235 return err 236 case opType == nil: 237 // gqlparser should err here, but doesn't [1], so we err to prevent 238 // panics later. 239 // TODO(benkraft): Remove once gqlparser is fixed. 240 // [1] https://github.com/vektah/gqlparser/issues/221 241 return errorf(op.Position, "schema has no %v type", op.Operation) 242 } 243 244 if op.Name == "" { 245 return errorf(op.Position, "operations must have operation-names") 246 } else if goKeywords[op.Name] { 247 return errorf(op.Position, "operation name must not be a go keyword") 248 } 249 250 return nil 251 } 252 253 // addOperation adds to g.Operations the information needed to generate a 254 // genqlient entrypoint function for the given operation. It also adds to 255 // g.typeMap any types referenced by the operation, except for types belonging 256 // to named fragments, which are added separately by Generate via 257 // convertFragment. 258 func (g *generator) addOperation(op *ast.OperationDefinition) error { 259 if err := g.validateOperation(op); err != nil { 260 return err 261 } 262 263 queryDoc := &ast.QueryDocument{ 264 Operations: ast.OperationList{op}, 265 Fragments: g.usedFragments(op), 266 } 267 g.preprocessQueryDocument(queryDoc) 268 269 var builder strings.Builder 270 f := formatter.NewFormatter(&builder) 271 f.FormatQueryDocument(queryDoc) 272 273 commentLines, directive, err := g.parsePrecedingComment(op, nil, op.Position, nil) 274 if err != nil { 275 return err 276 } 277 278 inputType, err := g.convertArguments(op, directive) 279 if err != nil { 280 return err 281 } 282 283 responseType, err := g.convertOperation(op, directive) 284 if err != nil { 285 return err 286 } 287 288 var docComment string 289 if len(commentLines) > 0 { 290 docComment = "// " + strings.ReplaceAll(commentLines, "\n", "\n// ") 291 } 292 293 // If the filename is a pseudo-filename filename.go:startline, just 294 // put the filename in the export; we don't figure out the line offset 295 // anyway, and if you want to check those exports in they will change a 296 // lot if they have line numbers. 297 // TODO: refactor to use the errorPos machinery for this 298 sourceFilename := op.Position.Src.Name 299 if i := strings.LastIndex(sourceFilename, ":"); i != -1 { 300 sourceFilename = sourceFilename[:i] 301 } 302 303 g.Operations = append(g.Operations, &operation{ 304 Type: op.Operation, 305 Name: op.Name, 306 Doc: docComment, 307 // The newline just makes it format a little nicer. We add it here 308 // rather than in the template so exported operations will match 309 // *exactly* what we send to the server. 310 Body: "\n" + builder.String(), 311 Input: inputType, 312 ResponseName: responseType.Reference(), 313 SourceFilename: sourceFilename, 314 Config: g.Config, // for the convenience of the template 315 }) 316 317 return nil 318 } 319 320 // Generate is the main programmatic entrypoint to genqlient, and generates and 321 // returns Go source code based on the given configuration. 322 // 323 // See Config for more on creating a configuration. The return value is a map 324 // from filename to the generated file-content (e.g. Go source). Callers who 325 // don't want to manage reading and writing the files should call Main. 326 func Generate(config *Config) (map[string][]byte, error) { 327 // Step 1: Read in the schema and operations from the files defined by the 328 // config (and validate the operations against the schema). This is all 329 // defined in parse.go. 330 schema, err := getSchema(config.Schema) 331 if err != nil { 332 return nil, err 333 } 334 335 document, err := getAndValidateQueries(config.baseDir, config.Operations, schema) 336 if err != nil { 337 return nil, err 338 } 339 340 // TODO(benkraft): we could also allow this, and generate an empty file 341 // with just the package-name, if it turns out to be more convenient that 342 // way. (As-is, we generate a broken file, with just (unused) imports.) 343 if len(document.Operations) == 0 { 344 // Hard to have a position when there are no operations :( 345 return nil, errorf(nil, 346 "no queries found, looked in: %v (configure this in genqlient.yaml)", 347 strings.Join(config.Operations, ", ")) 348 } 349 350 // Step 2: For each operation and fragment, convert it into data structures 351 // representing Go types (defined in types.go). The bulk of this logic is 352 // in convert.go, and it additionally updates g.typeMap to include all the 353 // types it needs. 354 g := newGenerator(config, schema, document.Fragments) 355 for _, op := range document.Operations { 356 if err = g.addOperation(op); err != nil { 357 return nil, err 358 } 359 } 360 361 // Step 3: Glue it all together! 362 // 363 // First, write the types (from g.typeMap) and operations to a temporary 364 // buffer, since they affect what imports we'll put in the header. 365 var bodyBuf bytes.Buffer 366 err = g.WriteTypes(&bodyBuf) 367 if err != nil { 368 return nil, err 369 } 370 371 // Sort operations to guarantee a stable order 372 sort.Slice(g.Operations, func(i, j int) bool { 373 return g.Operations[i].Name < g.Operations[j].Name 374 }) 375 376 for _, operation := range g.Operations { 377 err = g.render("operation.go.tmpl", &bodyBuf, operation) 378 if err != nil { 379 return nil, err 380 } 381 } 382 383 // The header also needs to reference some context types, which it does 384 // after it writes the imports, so we need to preregister those imports. 385 if g.Config.ContextType != "-" { 386 _, err = g.ref("context.Context") 387 if err != nil { 388 return nil, err 389 } 390 if g.Config.ContextType != "context.Context" { 391 _, err = g.ref(g.Config.ContextType) 392 if err != nil { 393 return nil, err 394 } 395 } 396 } 397 398 // Now really glue it all together, and format. 399 var buf bytes.Buffer 400 err = g.render("header.go.tmpl", &buf, g) 401 if err != nil { 402 return nil, err 403 } 404 _, err = io.Copy(&buf, &bodyBuf) 405 if err != nil { 406 return nil, err 407 } 408 409 unformatted := buf.Bytes() 410 formatted, err := format.Source(unformatted) 411 if err != nil { 412 return nil, goSourceError("gofmt", unformatted, err) 413 } 414 importsed, err := imports.Process(config.Generated, formatted, nil) 415 if err != nil { 416 return nil, goSourceError("goimports", formatted, err) 417 } 418 419 retval := map[string][]byte{ 420 config.Generated: importsed, 421 } 422 423 if config.ExportOperations != "" { 424 // We use MarshalIndent so that the file is human-readable and 425 // slightly more likely to be git-mergeable (if you check it in). In 426 // general it's never going to be used anywhere where space is an 427 // issue -- it doesn't go in your binary or anything. 428 retval[config.ExportOperations], err = json.MarshalIndent( 429 exportedOperations{Operations: g.Operations}, "", " ") 430 if err != nil { 431 return nil, errorf(nil, "unable to export queries: %v", err) 432 } 433 } 434 435 return retval, nil 436 }