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 }