diff --git a/codegen/config/config.go b/codegen/config/config.go index a11eb75c65..dc1e4b11a3 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -2,7 +2,6 @@ package config import ( "fmt" - "go/types" "io/ioutil" "os" "path/filepath" @@ -23,7 +22,7 @@ type Config struct { SchemaFilename StringList `yaml:"schema,omitempty"` Exec PackageConfig `yaml:"exec"` Model PackageConfig `yaml:"model,omitempty"` - Resolver PackageConfig `yaml:"resolver,omitempty"` + Resolver ResolverConfig `yaml:"resolver,omitempty"` AutoBind []string `yaml:"autobind"` Models TypeMap `yaml:"models,omitempty"` StructTag string `yaml:"struct_tag,omitempty"` @@ -138,12 +137,6 @@ func LoadConfig(filename string) (*Config, error) { return config, nil } -type PackageConfig struct { - Filename string `yaml:"filename,omitempty"` - Package string `yaml:"package,omitempty"` - Type string `yaml:"type,omitempty"` -} - type TypeMapEntry struct { Model StringList `yaml:"model"` Fields map[string]TypeMapField `yaml:"fields,omitempty"` @@ -184,90 +177,27 @@ func (a StringList) Has(file string) bool { return false } -func (c *PackageConfig) normalize() error { - if c.Filename == "" { - return errors.New("Filename is required") - } - c.Filename = abs(c.Filename) - // If Package is not set, first attempt to load the package at the output dir. If that fails - // fallback to just the base dir name of the output filename. - if c.Package == "" { - c.Package = code.NameForDir(c.Dir()) - } - - return nil -} - -func (c *PackageConfig) ImportPath() string { - return code.ImportPathForDir(c.Dir()) -} - -func (c *PackageConfig) Dir() string { - return filepath.Dir(c.Filename) -} - -func (c *PackageConfig) Check() error { - if strings.ContainsAny(c.Package, "./\\") { - return fmt.Errorf("package should be the output package name only, do not include the output filename") - } - if c.Filename != "" && !strings.HasSuffix(c.Filename, ".go") { - return fmt.Errorf("filename should be path to a go source file") - } - - return c.normalize() -} - -func (c *PackageConfig) Pkg() *types.Package { - return types.NewPackage(c.ImportPath(), c.Dir()) -} - -func (c *PackageConfig) IsDefined() bool { - return c.Filename != "" -} - func (c *Config) Check() error { + filesMap := make(map[string]bool) + pkgConfigsByDir := make(map[string]*PackageConfig) + if err := c.Models.Check(); err != nil { return errors.Wrap(err, "config.models") } - if err := c.Exec.Check(); err != nil { + if err := c.Exec.Check(filesMap, pkgConfigsByDir); err != nil { return errors.Wrap(err, "config.exec") } if c.Model.IsDefined() { - if err := c.Model.Check(); err != nil { + if err := c.Model.Check(filesMap, pkgConfigsByDir); err != nil { return errors.Wrap(err, "config.model") } } if c.Resolver.IsDefined() { - if err := c.Resolver.Check(); err != nil { + if err := c.Resolver.Check(filesMap, pkgConfigsByDir); err != nil { return errors.Wrap(err, "config.resolver") } } - // check packages names against conflict, if present in the same dir - // and check filenames for uniqueness - packageConfigList := []PackageConfig{} - if c.Model.IsDefined() { - packageConfigList = append(packageConfigList, c.Model) - } - packageConfigList = append(packageConfigList, []PackageConfig{ - c.Exec, - c.Resolver, - }...) - filesMap := make(map[string]bool) - pkgConfigsByDir := make(map[string]PackageConfig) - for _, current := range packageConfigList { - _, fileFound := filesMap[current.Filename] - if fileFound { - return fmt.Errorf("filename %s defined more than once", current.Filename) - } - filesMap[current.Filename] = true - previous, inSameDir := pkgConfigsByDir[current.Dir()] - if inSameDir && current.Package != previous.Package { - return fmt.Errorf("filenames %s and %s are in the same directory but have different package definitions", stripPath(current.Filename), stripPath(previous.Filename)) - } - pkgConfigsByDir[current.Dir()] = current - } - return c.normalize() } diff --git a/codegen/config/package.go b/codegen/config/package.go new file mode 100644 index 0000000000..500b56134e --- /dev/null +++ b/codegen/config/package.go @@ -0,0 +1,69 @@ +package config + +import ( + "fmt" + "go/types" + "path/filepath" + "strings" + + "github.com/99designs/gqlgen/internal/code" +) + +type PackageConfig struct { + Filename string `yaml:"filename,omitempty"` + Package string `yaml:"package,omitempty"` + Type string `yaml:"type,omitempty"` +} + +func (c *PackageConfig) normalize() error { + if c.Filename != "" { + c.Filename = abs(c.Filename) + } + // If Package is not set, first attempt to load the package at the output dir. If that fails + // fallback to just the base dir name of the output filename. + if c.Package == "" { + c.Package = code.NameForDir(c.Dir()) + } + + return nil +} + +func (c *PackageConfig) ImportPath() string { + return code.ImportPathForDir(c.Dir()) +} + +func (c *PackageConfig) Dir() string { + return filepath.Dir(c.Filename) +} + +func (c *PackageConfig) Pkg() *types.Package { + return types.NewPackage(c.ImportPath(), c.Dir()) +} + +func (c *PackageConfig) IsDefined() bool { + return c.Filename != "" +} + +func (c *PackageConfig) Check(filesMap map[string]bool, pkgConfigsByDir map[string]*PackageConfig) error { + if err := c.normalize(); err != nil { + return err + } + if strings.ContainsAny(c.Package, "./\\") { + return fmt.Errorf("package should be the output package name only, do not include the output filename") + } + if c.Filename != "" && !strings.HasSuffix(c.Filename, ".go") { + return fmt.Errorf("filename should be path to a go source file") + } + + _, fileFound := filesMap[c.Filename] + if fileFound { + return fmt.Errorf("filename %s defined more than once", c.Filename) + } + filesMap[c.Filename] = true + previous, inSameDir := pkgConfigsByDir[c.Dir()] + if inSameDir && c.Package != previous.Package { + return fmt.Errorf("filenames %s and %s are in the same directory but have different package definitions (%s vs %s)", stripPath(c.Filename), stripPath(previous.Filename), c.Package, previous.Package) + } + pkgConfigsByDir[c.Dir()] = c + return nil +} diff --git a/codegen/config/resolver.go b/codegen/config/resolver.go new file mode 100644 index 0000000000..3f37ad3dcb --- /dev/null +++ b/codegen/config/resolver.go @@ -0,0 +1,71 @@ +package config + +import ( + "fmt" + "go/types" + "path/filepath" + + "github.com/99designs/gqlgen/internal/code" +) + +type ResolverConfig struct { + PackageConfig `yaml:",inline"` + Layout ResolverLayout `yaml:"layout,omitempty"` + DirName string `yaml:"dir"` +} + +type ResolverLayout string + +var ( + LayoutSingleFile ResolverLayout = "single-file" + LayoutFollowSchema ResolverLayout = "follow-schema" +) + +func (r *ResolverConfig) Check(filesMap map[string]bool, pkgConfigsByDir map[string]*PackageConfig) error { + if r.DirName != "" { + r.DirName = abs(r.DirName) + } + + if r.Layout == "" { + r.Layout = "single-file" + } + if r.Layout != LayoutFollowSchema && r.Layout != LayoutSingleFile { + return fmt.Errorf("invalid layout %s. must be single-file or follow-schema", r.Layout) + } + + if r.Layout == "follow-schema" && r.DirName == "" { + return fmt.Errorf("must specify dir when using laout:follow-schema") + } + + return r.PackageConfig.Check(filesMap, pkgConfigsByDir) +} + +func (r *ResolverConfig) ImportPath() string { + return code.ImportPathForDir(r.Dir()) +} + +func (r *ResolverConfig) Dir() string { + switch r.Layout { + case LayoutSingleFile: + return filepath.Dir(r.Filename) + case LayoutFollowSchema: + return r.DirName + default: + panic("invalid layout " + r.Layout) + } +} + +func (r *ResolverConfig) Pkg() *types.Package { + return types.NewPackage(r.ImportPath(), r.Dir()) +} + +func (r *ResolverConfig) IsDefined() bool { + switch r.Layout { + case LayoutSingleFile, "": + return r.Filename != "" + case LayoutFollowSchema: + return r.DirName != "" + default: + panic(fmt.Errorf("invalid layout %s", r.Layout)) + } +} diff --git a/codegen/templates/templates.go b/codegen/templates/templates.go index 5d5f69bf88..8e126d6d1f 100644 --- a/codegen/templates/templates.go +++ b/codegen/templates/templates.go @@ -40,6 +40,8 @@ type Options struct { Filename string RegionTags bool GeneratedHeader bool + // PackageDoc is documentation written above the package line + PackageDoc string // Data will be passed to the template execution. Data interface{} Funcs template.FuncMap @@ -131,6 +133,9 @@ func Render(cfg Options) error { if cfg.GeneratedHeader { result.WriteString("// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.\n\n") } + if cfg.PackageDoc != "" { + result.WriteString(cfg.PackageDoc + "\n") + } result.WriteString("package ") result.WriteString(cfg.PackageName) result.WriteString("\n\n") diff --git a/plugin/resolvergen/resolver.go b/plugin/resolvergen/resolver.go index 6785c77c45..f5bef24365 100644 --- a/plugin/resolvergen/resolver.go +++ b/plugin/resolvergen/resolver.go @@ -1,13 +1,16 @@ package resolvergen import ( - "log" "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" "github.com/99designs/gqlgen/codegen" + "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/templates" "github.com/99designs/gqlgen/plugin" - "github.com/pkg/errors" ) func New() plugin.Plugin { @@ -19,36 +22,163 @@ type Plugin struct{} var _ plugin.CodeGenerator = &Plugin{} func (m *Plugin) Name() string { - // TODO: typo, should be resolvergen - return "resovlergen" + return "resolvergen" } + func (m *Plugin) GenerateCode(data *codegen.Data) error { if !data.Config.Resolver.IsDefined() { return nil } + switch data.Config.Resolver.Layout { + case config.LayoutSingleFile: + return m.generateSingleFile(data) + case config.LayoutFollowSchema: + return m.generatePerSchema(data) + } + + return nil +} + +func (m *Plugin) generateSingleFile(data *codegen.Data) error { + file := File{} + + if _, err := os.Stat(data.Config.Resolver.Filename); err == nil { + // file already exists and we dont support updating resolvers with layout = single so just return + return nil + } + + for _, o := range data.Objects { + if o.HasResolvers() { + file.Objects = append(file.Objects, o) + } + for _, f := range o.Fields { + if !f.IsResolver { + continue + } + + resolver := Resolver{o, f} + file.Resolvers = append(file.Resolvers, &resolver) + } + } + resolverBuild := &ResolverBuild{ - Data: data, + File: &file, PackageName: data.Config.Resolver.Package, ResolverType: data.Config.Resolver.Type, + HasRoot: true, + } + + return templates.Render(templates.Options{ + PackageName: data.Config.Resolver.Package, + PackageDoc: `// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.`, + Filename: data.Config.Resolver.Filename, + Data: resolverBuild, + }) +} + +func (m *Plugin) generatePerSchema(data *codegen.Data) error { + files := map[string]*File{} + + for _, o := range data.Objects { + if o.HasResolvers() { + fn := gqlToResolverName(data.Config.Resolver.Dir(), o.Position.Src.Name) + if files[fn] == nil { + files[fn] = &File{} + } + + files[fn].Objects = append(files[fn].Objects, o) + } + for _, f := range o.Fields { + if !f.IsResolver { + continue + } + + resolver := Resolver{o, f} + fn := gqlToResolverName(data.Config.Resolver.Dir(), f.Position.Src.Name) + if files[fn] == nil { + files[fn] = &File{} + } + + files[fn].Resolvers = append(files[fn].Resolvers, &resolver) + } } - filename := data.Config.Resolver.Filename - if _, err := os.Stat(filename); os.IsNotExist(errors.Cause(err)) { - return templates.Render(templates.Options{ + for filename, file := range files { + resolverBuild := &ResolverBuild{ + File: file, + PackageName: data.Config.Resolver.Package, + ResolverType: data.Config.Resolver.Type, + } + + err := templates.Render(templates.Options{ PackageName: data.Config.Resolver.Package, - Filename: data.Config.Resolver.Filename, - Data: resolverBuild, + PackageDoc: ` + // This file will be automatically regenerated based on the schema, any resolver implementations + // will be copied through when generating and any unknown code will be moved to the end.`, + Filename: filename, + Data: resolverBuild, }) + if err != nil { + return err + } } - log.Printf("Skipped resolver: %s already exists\n", filename) + if data.Config.Resolver.Layout == config.LayoutFollowSchema { + rootFilename := filepath.Join(data.Config.Resolver.Dir(), "resolver.go") + + if _, err := os.Stat(rootFilename); os.IsNotExist(errors.Cause(err)) { + err := templates.Render(templates.Options{ + PackageName: data.Config.Resolver.Package, + PackageDoc: ` + // This file will not be regenerated automatically. + // + // It serves as dependency injection for your app, add any dependencies you require here.`, + Template: `type {{.}} struct {}`, + Filename: rootFilename, + Data: data.Config.Resolver.Type, + }) + if err != nil { + return err + } + } + } return nil } type ResolverBuild struct { - *codegen.Data - + *File + HasRoot bool PackageName string ResolverType string } + +type File struct { + // These are separated because the type definition of the resolver object may live in a different file from the + //resolver method implementations, for example when extending a type in a different graphql schema file + Objects []*codegen.Object + Resolvers []*Resolver +} + +type Resolver struct { + Object *codegen.Object + Field *codegen.Field +} + +func (r *Resolver) filename(cfg config.ResolverConfig) string { + switch cfg.Layout { + case config.LayoutSingleFile: + return cfg.Filename + case config.LayoutFollowSchema: + return gqlToResolverName(cfg.Dir(), r.Field.Position.Src.Name) + default: + panic("bad config.resolver.layout " + cfg.Layout) + } +} + +func gqlToResolverName(base string, gqlname string) string { + gqlname = filepath.Base(gqlname) + ext := filepath.Ext(gqlname) + + return filepath.Join(base, strings.TrimSuffix(gqlname, ext)+"_resolvers.go") +} diff --git a/plugin/resolvergen/resolver.gotpl b/plugin/resolvergen/resolver.gotpl index ab42005006..af36a0525d 100644 --- a/plugin/resolvergen/resolver.gotpl +++ b/plugin/resolvergen/resolver.gotpl @@ -1,5 +1,3 @@ -// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. - {{ reserveImport "context" }} {{ reserveImport "fmt" }} {{ reserveImport "io" }} @@ -14,26 +12,21 @@ {{ reserveImport "github.com/99designs/gqlgen/graphql" }} {{ reserveImport "github.com/99designs/gqlgen/graphql/introspection" }} -type {{.ResolverType}} struct {} +{{ if .HasRoot }} + type {{.ResolverType}} struct {} +{{ end }} -{{ range $object := .Objects -}} - {{- if $object.HasResolvers -}} - func (r *{{$.ResolverType}}) {{$object.Name}}() {{ $object.ResolverInterface | ref }} { - return &{{lcFirst $object.Name}}{{ucFirst $.ResolverType}}{r} - } - {{ end -}} +{{ range $resolver := .Resolvers -}} + func (r *{{lcFirst $resolver.Object.Name}}{{ucFirst $.ResolverType}}) {{$resolver.Field.GoFieldName}}{{ $resolver.Field.ShortResolverDeclaration }} { + panic("not implemented") + } {{ end }} {{ range $object := .Objects -}} - {{- if $object.HasResolvers -}} - type {{lcFirst $object.Name}}{{ucFirst $.ResolverType}} struct { *{{$.ResolverType}} } + func (r *{{$.ResolverType}}) {{$object.Name}}() {{ $object.ResolverInterface | ref }} { return &{{lcFirst $object.Name}}{{ucFirst $.ResolverType}}{r} } +{{ end }} - {{ range $field := $object.Fields -}} - {{- if $field.IsResolver -}} - func (r *{{lcFirst $object.Name}}{{ucFirst $.ResolverType}}) {{$field.GoFieldName}}{{ $field.ShortResolverDeclaration }} { - panic("not implemented") - } - {{ end -}} - {{ end -}} - {{ end -}} +{{ range $object := .Objects -}} + type {{lcFirst $object.Name}}{{ucFirst $.ResolverType}} struct { *{{$.ResolverType}} } {{ end }} +