github.com/codykaup/genqlient@v0.6.2/generate/parse.go (about)

     1  package generate
     2  
     3  import (
     4  	"fmt"
     5  	goAst "go/ast"
     6  	goParser "go/parser"
     7  	goToken "go/token"
     8  	"os"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/vektah/gqlparser/v2/ast"
    14  	"github.com/vektah/gqlparser/v2/parser"
    15  	"github.com/vektah/gqlparser/v2/validator"
    16  	_ "github.com/vektah/gqlparser/v2/validator/rules"
    17  )
    18  
    19  func getSchema(globs StringList) (*ast.Schema, error) {
    20  	filenames, err := expandFilenames(globs)
    21  	if err != nil {
    22  		return nil, err
    23  	}
    24  
    25  	sources := make([]*ast.Source, len(filenames))
    26  	for i, filename := range filenames {
    27  		text, err := os.ReadFile(filename)
    28  		if err != nil {
    29  			return nil, errorf(nil, "unreadable schema file %v: %v", filename, err)
    30  		}
    31  		sources[i] = &ast.Source{Name: filename, Input: string(text)}
    32  	}
    33  
    34  	// Ideally here we'd just call gqlparser.LoadSchema. But the schema we are
    35  	// given may or may not contain the builtin types String, Int, etc. (The
    36  	// spec says it shouldn't, but introspection will return those types, and
    37  	// some introspection-to-SDL tools aren't smart enough to remove them.) So
    38  	// we inline LoadSchema and insert some checks.
    39  	document, graphqlError := parser.ParseSchemas(sources...)
    40  	if graphqlError != nil {
    41  		// Schema doesn't even parse.
    42  		return nil, errorf(nil, "invalid schema: %v", graphqlError)
    43  	}
    44  
    45  	// Check if we have a builtin type. (String is an arbitrary choice.)
    46  	hasBuiltins := false
    47  	for _, def := range document.Definitions {
    48  		if def.Name == "String" {
    49  			hasBuiltins = true
    50  			break
    51  		}
    52  	}
    53  
    54  	if !hasBuiltins {
    55  		// modified from parser.ParseSchemas
    56  		var preludeAST *ast.SchemaDocument
    57  		preludeAST, graphqlError = parser.ParseSchema(validator.Prelude)
    58  		if graphqlError != nil {
    59  			return nil, errorf(nil, "invalid prelude (probably a gqlparser bug): %v", graphqlError)
    60  		}
    61  		document.Merge(preludeAST)
    62  	}
    63  
    64  	schema, graphqlError := validator.ValidateSchemaDocument(document)
    65  	if graphqlError != nil {
    66  		return nil, errorf(nil, "invalid schema: %v", graphqlError)
    67  	}
    68  
    69  	return schema, nil
    70  }
    71  
    72  func getAndValidateQueries(basedir string, filenames StringList, schema *ast.Schema) (*ast.QueryDocument, error) {
    73  	queryDoc, err := getQueries(basedir, filenames)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	// Cf. gqlparser.LoadQuery
    79  	graphqlErrors := validator.Validate(schema, queryDoc)
    80  	if graphqlErrors != nil {
    81  		return nil, errorf(nil, "query-spec does not match schema: %v", graphqlErrors)
    82  	}
    83  
    84  	return queryDoc, nil
    85  }
    86  
    87  func expandFilenames(globs []string) ([]string, error) {
    88  	uniqFilenames := make(map[string]bool, len(globs))
    89  	for _, glob := range globs {
    90  		matches, err := filepath.Glob(glob)
    91  		if err != nil {
    92  			return nil, errorf(nil, "can't expand file-glob %v: %v", glob, err)
    93  		}
    94  		if len(matches) == 0 {
    95  			return nil, errorf(nil, "%v did not match any files", glob)
    96  		}
    97  		for _, match := range matches {
    98  			uniqFilenames[match] = true
    99  		}
   100  	}
   101  	filenames := make([]string, 0, len(uniqFilenames))
   102  	for filename := range uniqFilenames {
   103  		filenames = append(filenames, filename)
   104  	}
   105  	return filenames, nil
   106  }
   107  
   108  func getQueries(basedir string, globs StringList) (*ast.QueryDocument, error) {
   109  	// We merge all the queries into a single query-document, since operations
   110  	// in one might reference fragments in another.
   111  	//
   112  	// TODO(benkraft): It might be better to merge just within a filename, so
   113  	// that fragment-names don't need to be unique across files.  (Although
   114  	// then we may have other problems; and query-names still need to be.)
   115  	mergedQueryDoc := new(ast.QueryDocument)
   116  	addQueryDoc := func(queryDoc *ast.QueryDocument) {
   117  		mergedQueryDoc.Operations = append(mergedQueryDoc.Operations, queryDoc.Operations...)
   118  		mergedQueryDoc.Fragments = append(mergedQueryDoc.Fragments, queryDoc.Fragments...)
   119  	}
   120  
   121  	filenames, err := expandFilenames(globs)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	for _, filename := range filenames {
   127  		text, err := os.ReadFile(filename)
   128  		if err != nil {
   129  			return nil, errorf(nil, "unreadable query-spec file %v: %v", filename, err)
   130  		}
   131  
   132  		switch filepath.Ext(filename) {
   133  		case ".graphql":
   134  			queryDoc, err := getQueriesFromString(string(text), basedir, filename)
   135  			if err != nil {
   136  				return nil, err
   137  			}
   138  
   139  			addQueryDoc(queryDoc)
   140  
   141  		case ".go":
   142  			queryDocs, err := getQueriesFromGo(string(text), basedir, filename)
   143  			if err != nil {
   144  				return nil, err
   145  			}
   146  
   147  			for _, queryDoc := range queryDocs {
   148  				addQueryDoc(queryDoc)
   149  			}
   150  
   151  		default:
   152  			return nil, errorf(nil, "unknown file type: %v", filename)
   153  		}
   154  	}
   155  
   156  	return mergedQueryDoc, nil
   157  }
   158  
   159  func getQueriesFromString(text string, basedir, filename string) (*ast.QueryDocument, error) {
   160  	// make path relative to the config-directory
   161  	relname, err := filepath.Rel(basedir, filename)
   162  	if err == nil {
   163  		filename = relname
   164  	}
   165  
   166  	// Cf. gqlparser.LoadQuery
   167  	document, graphqlError := parser.ParseQuery(
   168  		&ast.Source{Name: filename, Input: text})
   169  	if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
   170  		return nil, errorf(nil, "invalid query-spec file %v: %v", filename, graphqlError)
   171  	}
   172  
   173  	return document, nil
   174  }
   175  
   176  func getQueriesFromGo(text string, basedir, filename string) ([]*ast.QueryDocument, error) {
   177  	fset := goToken.NewFileSet()
   178  	f, err := goParser.ParseFile(fset, filename, text, 0)
   179  	if err != nil {
   180  		return nil, errorf(nil, "invalid Go file %v: %v", filename, err)
   181  	}
   182  
   183  	var retval []*ast.QueryDocument
   184  	goAst.Inspect(f, func(node goAst.Node) bool {
   185  		if err != nil {
   186  			return false // don't bother to recurse if something already failed
   187  		}
   188  
   189  		basicLit, ok := node.(*goAst.BasicLit)
   190  		if !ok || basicLit.Kind != goToken.STRING {
   191  			return true // recurse
   192  		}
   193  
   194  		var value string
   195  		value, err = strconv.Unquote(basicLit.Value)
   196  		if err != nil {
   197  			return false
   198  		}
   199  
   200  		if !strings.HasPrefix(strings.TrimSpace(value), "# @genqlient") {
   201  			return true
   202  		}
   203  
   204  		// We put the filename as <real filename>:<line>, which errors.go knows
   205  		// how to parse back out (since it's what gqlparser will give to us in
   206  		// our errors).
   207  		pos := fset.Position(basicLit.Pos())
   208  		fakeFilename := fmt.Sprintf("%v:%v", pos.Filename, pos.Line)
   209  		var query *ast.QueryDocument
   210  		query, err = getQueriesFromString(value, basedir, fakeFilename)
   211  		if err != nil {
   212  			return false
   213  		}
   214  		retval = append(retval, query)
   215  
   216  		return true
   217  	})
   218  
   219  	return retval, err
   220  }