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 }