github.com/codykaup/genqlient@v0.6.2/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  	_, err := g.baseTypeForOperation(op.Operation)
   232  	if err != nil {
   233  		// (e.g. operation has subscriptions, which we don't support)
   234  		return err
   235  	}
   236  
   237  	if op.Name == "" {
   238  		return errorf(op.Position, "operations must have operation-names")
   239  	} else if goKeywords[op.Name] {
   240  		return errorf(op.Position, "operation name must not be a go keyword")
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  // addOperation adds to g.Operations the information needed to generate a
   247  // genqlient entrypoint function for the given operation.  It also adds to
   248  // g.typeMap any types referenced by the operation, except for types belonging
   249  // to named fragments, which are added separately by Generate via
   250  // convertFragment.
   251  func (g *generator) addOperation(op *ast.OperationDefinition) error {
   252  	if err := g.validateOperation(op); err != nil {
   253  		return err
   254  	}
   255  
   256  	queryDoc := &ast.QueryDocument{
   257  		Operations: ast.OperationList{op},
   258  		Fragments:  g.usedFragments(op),
   259  	}
   260  	g.preprocessQueryDocument(queryDoc)
   261  
   262  	var builder strings.Builder
   263  	f := formatter.NewFormatter(&builder)
   264  	f.FormatQueryDocument(queryDoc)
   265  
   266  	commentLines, directive, err := g.parsePrecedingComment(op, nil, op.Position, nil)
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	inputType, err := g.convertArguments(op, directive)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	responseType, err := g.convertOperation(op, directive)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	var docComment string
   282  	if len(commentLines) > 0 {
   283  		docComment = "// " + strings.ReplaceAll(commentLines, "\n", "\n// ")
   284  	}
   285  
   286  	// If the filename is a pseudo-filename filename.go:startline, just
   287  	// put the filename in the export; we don't figure out the line offset
   288  	// anyway, and if you want to check those exports in they will change a
   289  	// lot if they have line numbers.
   290  	// TODO: refactor to use the errorPos machinery for this
   291  	sourceFilename := op.Position.Src.Name
   292  	if i := strings.LastIndex(sourceFilename, ":"); i != -1 {
   293  		sourceFilename = sourceFilename[:i]
   294  	}
   295  
   296  	g.Operations = append(g.Operations, &operation{
   297  		Type: op.Operation,
   298  		Name: op.Name,
   299  		Doc:  docComment,
   300  		// The newline just makes it format a little nicer.  We add it here
   301  		// rather than in the template so exported operations will match
   302  		// *exactly* what we send to the server.
   303  		Body:           "\n" + builder.String(),
   304  		Input:          inputType,
   305  		ResponseName:   responseType.Reference(),
   306  		SourceFilename: sourceFilename,
   307  		Config:         g.Config, // for the convenience of the template
   308  	})
   309  
   310  	return nil
   311  }
   312  
   313  // Generate is the main programmatic entrypoint to genqlient, and generates and
   314  // returns Go source code based on the given configuration.
   315  //
   316  // See [Config] for more on creating a configuration.  The return value is a
   317  // map from filename to the generated file-content (e.g. Go source).  Callers
   318  // who don't want to manage reading and writing the files should call [Main].
   319  func Generate(config *Config) (map[string][]byte, error) {
   320  	// Step 1: Read in the schema and operations from the files defined by the
   321  	// config (and validate the operations against the schema).  This is all
   322  	// defined in parse.go.
   323  	schema, err := getSchema(config.Schema)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  
   328  	document, err := getAndValidateQueries(config.baseDir, config.Operations, schema)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  
   333  	// TODO(benkraft): we could also allow this, and generate an empty file
   334  	// with just the package-name, if it turns out to be more convenient that
   335  	// way.  (As-is, we generate a broken file, with just (unused) imports.)
   336  	if len(document.Operations) == 0 {
   337  		// Hard to have a position when there are no operations :(
   338  		return nil, errorf(nil,
   339  			"no queries found, looked in: %v (configure this in genqlient.yaml)",
   340  			strings.Join(config.Operations, ", "))
   341  	}
   342  
   343  	// Step 2: For each operation and fragment, convert it into data structures
   344  	// representing Go types (defined in types.go).  The bulk of this logic is
   345  	// in convert.go, and it additionally updates g.typeMap to include all the
   346  	// types it needs.
   347  	g := newGenerator(config, schema, document.Fragments)
   348  	for _, op := range document.Operations {
   349  		if err = g.addOperation(op); err != nil {
   350  			return nil, err
   351  		}
   352  	}
   353  
   354  	// Step 3: Glue it all together!
   355  	//
   356  	// First, write the types (from g.typeMap) and operations to a temporary
   357  	// buffer, since they affect what imports we'll put in the header.
   358  	var bodyBuf bytes.Buffer
   359  	err = g.WriteTypes(&bodyBuf)
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	// Sort operations to guarantee a stable order
   365  	sort.Slice(g.Operations, func(i, j int) bool {
   366  		return g.Operations[i].Name < g.Operations[j].Name
   367  	})
   368  
   369  	for _, operation := range g.Operations {
   370  		err = g.render("operation.go.tmpl", &bodyBuf, operation)
   371  		if err != nil {
   372  			return nil, err
   373  		}
   374  	}
   375  
   376  	// The header also needs to reference some context types, which it does
   377  	// after it writes the imports, so we need to preregister those imports.
   378  	if g.Config.ContextType != "-" {
   379  		_, err = g.ref("context.Context")
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  		if g.Config.ContextType != "context.Context" {
   384  			_, err = g.ref(g.Config.ContextType)
   385  			if err != nil {
   386  				return nil, err
   387  			}
   388  		}
   389  	}
   390  
   391  	// Now really glue it all together, and format.
   392  	var buf bytes.Buffer
   393  	err = g.render("header.go.tmpl", &buf, g)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	_, err = io.Copy(&buf, &bodyBuf)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	unformatted := buf.Bytes()
   403  	formatted, err := format.Source(unformatted)
   404  	if err != nil {
   405  		return nil, goSourceError("gofmt", unformatted, err)
   406  	}
   407  	importsed, err := imports.Process(config.Generated, formatted, nil)
   408  	if err != nil {
   409  		return nil, goSourceError("goimports", formatted, err)
   410  	}
   411  
   412  	retval := map[string][]byte{
   413  		config.Generated: importsed,
   414  	}
   415  
   416  	if config.ExportOperations != "" {
   417  		// We use MarshalIndent so that the file is human-readable and
   418  		// slightly more likely to be git-mergeable (if you check it in).  In
   419  		// general it's never going to be used anywhere where space is an
   420  		// issue -- it doesn't go in your binary or anything.
   421  		retval[config.ExportOperations], err = json.MarshalIndent(
   422  			exportedOperations{Operations: g.Operations}, "", "  ")
   423  		if err != nil {
   424  			return nil, errorf(nil, "unable to export queries: %v", err)
   425  		}
   426  	}
   427  
   428  	return retval, nil
   429  }