github.com/kerryoscer/gqlgen@v0.17.29/codegen/config/config.go (about) 1 package config 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/kerryoscer/gqlgen/internal/code" 13 "github.com/vektah/gqlparser/v2/ast" 14 ) 15 16 type Config struct { 17 SchemaFilename StringList `yaml:"schema,omitempty"` 18 Exec ExecConfig `yaml:"exec"` 19 Model PackageConfig `yaml:"model,omitempty"` 20 Federation PackageConfig `yaml:"federation,omitempty"` 21 Resolver ResolverConfig `yaml:"resolver,omitempty"` 22 AutoBind []string `yaml:"autobind"` 23 Models TypeMap `yaml:"models,omitempty"` 24 StructTag string `yaml:"struct_tag,omitempty"` 25 Directives map[string]DirectiveConfig `yaml:"directives,omitempty"` 26 OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"` 27 OmitGetters bool `yaml:"omit_getters,omitempty"` 28 OmitComplexity bool `yaml:"omit_complexity,omitempty"` 29 StructFieldsAlwaysPointers bool `yaml:"struct_fields_always_pointers,omitempty"` 30 ReturnPointersInUmarshalInput bool `yaml:"return_pointers_in_unmarshalinput,omitempty"` 31 ResolversAlwaysReturnPointers bool `yaml:"resolvers_always_return_pointers,omitempty"` 32 SkipValidation bool `yaml:"skip_validation,omitempty"` 33 SkipModTidy bool `yaml:"skip_mod_tidy,omitempty"` 34 Sources []*ast.Source `yaml:"-"` 35 Packages *code.Packages `yaml:"-"` 36 Schema *ast.Schema `yaml:"-"` 37 38 // Deprecated: use Federation instead. Will be removed next release 39 Federated bool `yaml:"federated,omitempty"` 40 } 41 42 var cfgFilenames = []string{".gqlgen.yml", "gqlgen.yml", "gqlgen.yaml"} 43 44 // DefaultConfig creates a copy of the default config 45 func DefaultConfig() *Config { 46 return &Config{ 47 SchemaFilename: StringList{"schema.graphql"}, 48 Model: PackageConfig{Filename: "models_gen.go"}, 49 Exec: ExecConfig{Filename: "generated.go"}, 50 Directives: map[string]DirectiveConfig{}, 51 Models: TypeMap{}, 52 StructFieldsAlwaysPointers: true, 53 ReturnPointersInUmarshalInput: false, 54 ResolversAlwaysReturnPointers: true, 55 } 56 } 57 58 // LoadDefaultConfig loads the default config so that it is ready to be used 59 func LoadDefaultConfig() (*Config, error) { 60 config := DefaultConfig() 61 62 for _, filename := range config.SchemaFilename { 63 filename = filepath.ToSlash(filename) 64 var err error 65 var schemaRaw []byte 66 schemaRaw, err = os.ReadFile(filename) 67 if err != nil { 68 return nil, fmt.Errorf("unable to open schema: %w", err) 69 } 70 71 config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) 72 } 73 74 return config, nil 75 } 76 77 // LoadConfigFromDefaultLocations looks for a config file in the current directory, and all parent directories 78 // walking up the tree. The closest config file will be returned. 79 func LoadConfigFromDefaultLocations() (*Config, error) { 80 cfgFile, err := findCfg() 81 if err != nil { 82 return nil, err 83 } 84 85 err = os.Chdir(filepath.Dir(cfgFile)) 86 if err != nil { 87 return nil, fmt.Errorf("unable to enter config dir: %w", err) 88 } 89 return LoadConfig(cfgFile) 90 } 91 92 var path2regex = strings.NewReplacer( 93 `.`, `\.`, 94 `*`, `.+`, 95 `\`, `[\\/]`, 96 `/`, `[\\/]`, 97 ) 98 99 // LoadConfig reads the gqlgen.yml config file 100 func LoadConfig(filename string) (*Config, error) { 101 config := DefaultConfig() 102 103 b, err := os.ReadFile(filename) 104 if err != nil { 105 return nil, fmt.Errorf("unable to read config: %w", err) 106 } 107 108 dec := yaml.NewDecoder(bytes.NewReader(b)) 109 dec.KnownFields(true) 110 111 if err := dec.Decode(config); err != nil { 112 return nil, fmt.Errorf("unable to parse config: %w", err) 113 } 114 115 if err := CompleteConfig(config); err != nil { 116 return nil, err 117 } 118 119 return config, nil 120 } 121 122 // CompleteConfig fills in the schema and other values to a config loaded from 123 // YAML. 124 func CompleteConfig(config *Config) error { 125 defaultDirectives := map[string]DirectiveConfig{ 126 "skip": {SkipRuntime: true}, 127 "include": {SkipRuntime: true}, 128 "deprecated": {SkipRuntime: true}, 129 "specifiedBy": {SkipRuntime: true}, 130 } 131 132 for key, value := range defaultDirectives { 133 if _, defined := config.Directives[key]; !defined { 134 config.Directives[key] = value 135 } 136 } 137 138 preGlobbing := config.SchemaFilename 139 config.SchemaFilename = StringList{} 140 for _, f := range preGlobbing { 141 var matches []string 142 143 // for ** we want to override default globbing patterns and walk all 144 // subdirectories to match schema files. 145 if strings.Contains(f, "**") { 146 pathParts := strings.SplitN(f, "**", 2) 147 rest := strings.TrimPrefix(strings.TrimPrefix(pathParts[1], `\`), `/`) 148 // turn the rest of the glob into a regex, anchored only at the end because ** allows 149 // for any number of dirs in between and walk will let us match against the full path name 150 globRe := regexp.MustCompile(path2regex.Replace(rest) + `$`) 151 152 if err := filepath.Walk(pathParts[0], func(path string, info os.FileInfo, err error) error { 153 if err != nil { 154 return err 155 } 156 157 if globRe.MatchString(strings.TrimPrefix(path, pathParts[0])) { 158 matches = append(matches, path) 159 } 160 161 return nil 162 }); err != nil { 163 return fmt.Errorf("failed to walk schema at root %s: %w", pathParts[0], err) 164 } 165 } else { 166 var err error 167 matches, err = filepath.Glob(f) 168 if err != nil { 169 return fmt.Errorf("failed to glob schema filename %s: %w", f, err) 170 } 171 } 172 173 for _, m := range matches { 174 if config.SchemaFilename.Has(m) { 175 continue 176 } 177 config.SchemaFilename = append(config.SchemaFilename, m) 178 } 179 } 180 181 for _, filename := range config.SchemaFilename { 182 filename = filepath.ToSlash(filename) 183 var err error 184 var schemaRaw []byte 185 schemaRaw, err = os.ReadFile(filename) 186 if err != nil { 187 return fmt.Errorf("unable to open schema: %w", err) 188 } 189 190 config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) 191 } 192 return nil 193 } 194 195 func (c *Config) Init() error { 196 if c.Packages == nil { 197 c.Packages = &code.Packages{} 198 } 199 200 if c.Schema == nil { 201 if err := c.LoadSchema(); err != nil { 202 return err 203 } 204 } 205 206 err := c.injectTypesFromSchema() 207 if err != nil { 208 return err 209 } 210 211 err = c.autobind() 212 if err != nil { 213 return err 214 } 215 216 c.injectBuiltins() 217 // prefetch all packages in one big packages.Load call 218 c.Packages.LoadAll(c.packageList()...) 219 220 // check everything is valid on the way out 221 err = c.check() 222 if err != nil { 223 return err 224 } 225 226 return nil 227 } 228 229 func (c *Config) packageList() []string { 230 pkgs := []string{ 231 "github.com/kerryoscer/gqlgen/graphql", 232 "github.com/kerryoscer/gqlgen/graphql/introspection", 233 } 234 pkgs = append(pkgs, c.Models.ReferencedPackages()...) 235 pkgs = append(pkgs, c.AutoBind...) 236 return pkgs 237 } 238 239 func (c *Config) ReloadAllPackages() { 240 c.Packages.ReloadAll(c.packageList()...) 241 } 242 243 func (c *Config) injectTypesFromSchema() error { 244 c.Directives["goModel"] = DirectiveConfig{ 245 SkipRuntime: true, 246 } 247 248 c.Directives["goField"] = DirectiveConfig{ 249 SkipRuntime: true, 250 } 251 252 c.Directives["goTag"] = DirectiveConfig{ 253 SkipRuntime: true, 254 } 255 256 for _, schemaType := range c.Schema.Types { 257 if schemaType == c.Schema.Query || schemaType == c.Schema.Mutation || schemaType == c.Schema.Subscription { 258 continue 259 } 260 261 if bd := schemaType.Directives.ForName("goModel"); bd != nil { 262 if ma := bd.Arguments.ForName("model"); ma != nil { 263 if mv, err := ma.Value.Value(nil); err == nil { 264 c.Models.Add(schemaType.Name, mv.(string)) 265 } 266 } 267 if ma := bd.Arguments.ForName("models"); ma != nil { 268 if mvs, err := ma.Value.Value(nil); err == nil { 269 for _, mv := range mvs.([]interface{}) { 270 c.Models.Add(schemaType.Name, mv.(string)) 271 } 272 } 273 } 274 } 275 276 if schemaType.Kind == ast.Object || schemaType.Kind == ast.InputObject { 277 for _, field := range schemaType.Fields { 278 if fd := field.Directives.ForName("goField"); fd != nil { 279 forceResolver := c.Models[schemaType.Name].Fields[field.Name].Resolver 280 fieldName := c.Models[schemaType.Name].Fields[field.Name].FieldName 281 282 if ra := fd.Arguments.ForName("forceResolver"); ra != nil { 283 if fr, err := ra.Value.Value(nil); err == nil { 284 forceResolver = fr.(bool) 285 } 286 } 287 288 if na := fd.Arguments.ForName("name"); na != nil { 289 if fr, err := na.Value.Value(nil); err == nil { 290 fieldName = fr.(string) 291 } 292 } 293 294 if c.Models[schemaType.Name].Fields == nil { 295 c.Models[schemaType.Name] = TypeMapEntry{ 296 Model: c.Models[schemaType.Name].Model, 297 Fields: map[string]TypeMapField{}, 298 } 299 } 300 301 c.Models[schemaType.Name].Fields[field.Name] = TypeMapField{ 302 FieldName: fieldName, 303 Resolver: forceResolver, 304 } 305 } 306 } 307 } 308 } 309 310 return nil 311 } 312 313 type TypeMapEntry struct { 314 Model StringList `yaml:"model"` 315 Fields map[string]TypeMapField `yaml:"fields,omitempty"` 316 } 317 318 type TypeMapField struct { 319 Resolver bool `yaml:"resolver"` 320 FieldName string `yaml:"fieldName"` 321 GeneratedMethod string `yaml:"-"` 322 } 323 324 type StringList []string 325 326 func (a *StringList) UnmarshalYAML(unmarshal func(interface{}) error) error { 327 var single string 328 err := unmarshal(&single) 329 if err == nil { 330 *a = []string{single} 331 return nil 332 } 333 334 var multi []string 335 err = unmarshal(&multi) 336 if err != nil { 337 return err 338 } 339 340 *a = multi 341 return nil 342 } 343 344 func (a StringList) Has(file string) bool { 345 for _, existing := range a { 346 if existing == file { 347 return true 348 } 349 } 350 return false 351 } 352 353 func (c *Config) check() error { 354 if c.Models == nil { 355 c.Models = TypeMap{} 356 } 357 358 type FilenamePackage struct { 359 Filename string 360 Package string 361 Declaree string 362 } 363 364 fileList := map[string][]FilenamePackage{} 365 366 if err := c.Models.Check(); err != nil { 367 return fmt.Errorf("config.models: %w", err) 368 } 369 if err := c.Exec.Check(); err != nil { 370 return fmt.Errorf("config.exec: %w", err) 371 } 372 fileList[c.Exec.ImportPath()] = append(fileList[c.Exec.ImportPath()], FilenamePackage{ 373 Filename: c.Exec.Filename, 374 Package: c.Exec.Package, 375 Declaree: "exec", 376 }) 377 378 if c.Model.IsDefined() { 379 if err := c.Model.Check(); err != nil { 380 return fmt.Errorf("config.model: %w", err) 381 } 382 fileList[c.Model.ImportPath()] = append(fileList[c.Model.ImportPath()], FilenamePackage{ 383 Filename: c.Model.Filename, 384 Package: c.Model.Package, 385 Declaree: "model", 386 }) 387 } 388 if c.Resolver.IsDefined() { 389 if err := c.Resolver.Check(); err != nil { 390 return fmt.Errorf("config.resolver: %w", err) 391 } 392 fileList[c.Resolver.ImportPath()] = append(fileList[c.Resolver.ImportPath()], FilenamePackage{ 393 Filename: c.Resolver.Filename, 394 Package: c.Resolver.Package, 395 Declaree: "resolver", 396 }) 397 } 398 if c.Federation.IsDefined() { 399 if err := c.Federation.Check(); err != nil { 400 return fmt.Errorf("config.federation: %w", err) 401 } 402 fileList[c.Federation.ImportPath()] = append(fileList[c.Federation.ImportPath()], FilenamePackage{ 403 Filename: c.Federation.Filename, 404 Package: c.Federation.Package, 405 Declaree: "federation", 406 }) 407 if c.Federation.ImportPath() != c.Exec.ImportPath() { 408 return fmt.Errorf("federation and exec must be in the same package") 409 } 410 } 411 if c.Federated { 412 return fmt.Errorf("federated has been removed, instead use\nfederation:\n filename: path/to/federated.go") 413 } 414 415 for importPath, pkg := range fileList { 416 for _, file1 := range pkg { 417 for _, file2 := range pkg { 418 if file1.Package != file2.Package { 419 return fmt.Errorf("%s and %s define the same import path (%s) with different package names (%s vs %s)", 420 file1.Declaree, 421 file2.Declaree, 422 importPath, 423 file1.Package, 424 file2.Package, 425 ) 426 } 427 } 428 } 429 } 430 431 return nil 432 } 433 434 type TypeMap map[string]TypeMapEntry 435 436 func (tm TypeMap) Exists(typeName string) bool { 437 _, ok := tm[typeName] 438 return ok 439 } 440 441 func (tm TypeMap) UserDefined(typeName string) bool { 442 m, ok := tm[typeName] 443 return ok && len(m.Model) > 0 444 } 445 446 func (tm TypeMap) Check() error { 447 for typeName, entry := range tm { 448 for _, model := range entry.Model { 449 if strings.LastIndex(model, ".") < strings.LastIndex(model, "/") { 450 return fmt.Errorf("model %s: invalid type specifier \"%s\" - you need to specify a struct to map to", typeName, entry.Model) 451 } 452 } 453 } 454 return nil 455 } 456 457 func (tm TypeMap) ReferencedPackages() []string { 458 var pkgs []string 459 460 for _, typ := range tm { 461 for _, model := range typ.Model { 462 if model == "map[string]interface{}" || model == "interface{}" { 463 continue 464 } 465 pkg, _ := code.PkgAndType(model) 466 if pkg == "" || inStrSlice(pkgs, pkg) { 467 continue 468 } 469 pkgs = append(pkgs, code.QualifyPackagePath(pkg)) 470 } 471 } 472 473 sort.Slice(pkgs, func(i, j int) bool { 474 return pkgs[i] > pkgs[j] 475 }) 476 return pkgs 477 } 478 479 func (tm TypeMap) Add(name string, goType string) { 480 modelCfg := tm[name] 481 modelCfg.Model = append(modelCfg.Model, goType) 482 tm[name] = modelCfg 483 } 484 485 type DirectiveConfig struct { 486 SkipRuntime bool `yaml:"skip_runtime"` 487 } 488 489 func inStrSlice(haystack []string, needle string) bool { 490 for _, v := range haystack { 491 if needle == v { 492 return true 493 } 494 } 495 496 return false 497 } 498 499 // findCfg searches for the config file in this directory and all parents up the tree 500 // looking for the closest match 501 func findCfg() (string, error) { 502 dir, err := os.Getwd() 503 if err != nil { 504 return "", fmt.Errorf("unable to get working dir to findCfg: %w", err) 505 } 506 507 cfg := findCfgInDir(dir) 508 509 for cfg == "" && dir != filepath.Dir(dir) { 510 dir = filepath.Dir(dir) 511 cfg = findCfgInDir(dir) 512 } 513 514 if cfg == "" { 515 return "", os.ErrNotExist 516 } 517 518 return cfg, nil 519 } 520 521 func findCfgInDir(dir string) string { 522 for _, cfgName := range cfgFilenames { 523 path := filepath.Join(dir, cfgName) 524 if _, err := os.Stat(path); err == nil { 525 return path 526 } 527 } 528 return "" 529 } 530 531 func (c *Config) autobind() error { 532 if len(c.AutoBind) == 0 { 533 return nil 534 } 535 536 ps := c.Packages.LoadAll(c.AutoBind...) 537 538 for _, t := range c.Schema.Types { 539 if c.Models.UserDefined(t.Name) { 540 continue 541 } 542 543 for i, p := range ps { 544 if p == nil || p.Module == nil { 545 return fmt.Errorf("unable to load %s - make sure you're using an import path to a package that exists", c.AutoBind[i]) 546 } 547 if t := p.Types.Scope().Lookup(t.Name); t != nil { 548 c.Models.Add(t.Name(), t.Pkg().Path()+"."+t.Name()) 549 break 550 } 551 } 552 } 553 554 for i, t := range c.Models { 555 for j, m := range t.Model { 556 pkg, typename := code.PkgAndType(m) 557 558 // skip anything that looks like an import path 559 if strings.Contains(pkg, "/") { 560 continue 561 } 562 563 for _, p := range ps { 564 if p.Name != pkg { 565 continue 566 } 567 if t := p.Types.Scope().Lookup(typename); t != nil { 568 c.Models[i].Model[j] = t.Pkg().Path() + "." + t.Name() 569 break 570 } 571 } 572 } 573 } 574 575 return nil 576 } 577 578 func (c *Config) injectBuiltins() { 579 builtins := TypeMap{ 580 "__Directive": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Directive"}}, 581 "__DirectiveLocation": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}}, 582 "__Type": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Type"}}, 583 "__TypeKind": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}}, 584 "__Field": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Field"}}, 585 "__EnumValue": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.EnumValue"}}, 586 "__InputValue": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.InputValue"}}, 587 "__Schema": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql/introspection.Schema"}}, 588 "Float": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.FloatContext"}}, 589 "String": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.String"}}, 590 "Boolean": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Boolean"}}, 591 "Int": {Model: StringList{ 592 "github.com/kerryoscer/gqlgen/graphql.Int", 593 "github.com/kerryoscer/gqlgen/graphql.Int32", 594 "github.com/kerryoscer/gqlgen/graphql.Int64", 595 }}, 596 "ID": { 597 Model: StringList{ 598 "github.com/kerryoscer/gqlgen/graphql.ID", 599 "github.com/kerryoscer/gqlgen/graphql.IntID", 600 }, 601 }, 602 } 603 604 for typeName, entry := range builtins { 605 if !c.Models.Exists(typeName) { 606 c.Models[typeName] = entry 607 } 608 } 609 610 // These are additional types that are injected if defined in the schema as scalars. 611 extraBuiltins := TypeMap{ 612 "Time": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Time"}}, 613 "Map": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Map"}}, 614 "Upload": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Upload"}}, 615 "Any": {Model: StringList{"github.com/kerryoscer/gqlgen/graphql.Any"}}, 616 } 617 618 for typeName, entry := range extraBuiltins { 619 if t, ok := c.Schema.Types[typeName]; !c.Models.Exists(typeName) && ok && t.Kind == ast.Scalar { 620 c.Models[typeName] = entry 621 } 622 } 623 } 624 625 func (c *Config) LoadSchema() error { 626 if c.Packages != nil { 627 c.Packages = &code.Packages{} 628 } 629 630 if err := c.check(); err != nil { 631 return err 632 } 633 634 schema, err := gqlparser.LoadSchema(c.Sources...) 635 if err != nil { 636 return err 637 } 638 639 if schema.Query == nil { 640 schema.Query = &ast.Definition{ 641 Kind: ast.Object, 642 Name: "Query", 643 } 644 schema.Types["Query"] = schema.Query 645 } 646 647 c.Schema = schema 648 return nil 649 } 650 651 func abs(path string) string { 652 absPath, err := filepath.Abs(path) 653 if err != nil { 654 panic(err) 655 } 656 return filepath.ToSlash(absPath) 657 }