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  }