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