github.com/mstephano/gqlgen-schemagen@v0.0.0-20230113041936-dd2cd4ea46aa/plugin/resolvergen/resolver.go (about) 1 package resolvergen 2 3 import ( 4 _ "embed" 5 "errors" 6 "fmt" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "golang.org/x/text/cases" 13 "golang.org/x/text/language" 14 15 "github.com/mstephano/gqlgen-schemagen/codegen" 16 "github.com/mstephano/gqlgen-schemagen/codegen/config" 17 "github.com/mstephano/gqlgen-schemagen/codegen/templates" 18 "github.com/mstephano/gqlgen-schemagen/graphql" 19 "github.com/mstephano/gqlgen-schemagen/internal/rewrite" 20 "github.com/mstephano/gqlgen-schemagen/plugin" 21 ) 22 23 //go:embed resolver.gotpl 24 var resolverTemplate string 25 26 func New() plugin.Plugin { 27 return &Plugin{} 28 } 29 30 type Plugin struct{} 31 32 var _ plugin.CodeGenerator = &Plugin{} 33 34 func (m *Plugin) Name() string { 35 return "resolvergen" 36 } 37 38 func (m *Plugin) GenerateCode(data *codegen.Data) error { 39 if !data.Config.Resolver.IsDefined() { 40 return nil 41 } 42 43 switch data.Config.Resolver.Layout { 44 case config.LayoutSingleFile: 45 return m.generateSingleFile(data) 46 case config.LayoutFollowSchema: 47 return m.generatePerSchema(data) 48 } 49 50 return nil 51 } 52 53 func (m *Plugin) generateSingleFile(data *codegen.Data) error { 54 file := File{} 55 56 if _, err := os.Stat(data.Config.Resolver.Filename); err == nil { 57 // file already exists and we do not support updating resolvers with layout = single so just return 58 return nil 59 } 60 61 for _, o := range data.Objects { 62 if o.HasResolvers() { 63 file.Objects = append(file.Objects, o) 64 } 65 for _, f := range o.Fields { 66 if !f.IsResolver { 67 continue 68 } 69 70 resolver := Resolver{o, f, "// foo", `panic("not implemented")`} 71 file.Resolvers = append(file.Resolvers, &resolver) 72 } 73 } 74 75 resolverBuild := &ResolverBuild{ 76 File: &file, 77 PackageName: data.Config.Resolver.Package, 78 ResolverType: data.Config.Resolver.Type, 79 HasRoot: true, 80 } 81 82 return templates.Render(templates.Options{ 83 PackageName: data.Config.Resolver.Package, 84 FileNotice: `// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.`, 85 Filename: data.Config.Resolver.Filename, 86 Data: resolverBuild, 87 Packages: data.Config.Packages, 88 Template: resolverTemplate, 89 }) 90 } 91 92 func (m *Plugin) generatePerSchema(data *codegen.Data) error { 93 rewriter, err := rewrite.New(data.Config.Resolver.Dir()) 94 if err != nil { 95 return err 96 } 97 98 files := map[string]*File{} 99 100 objects := make(codegen.Objects, len(data.Objects)+len(data.Inputs)) 101 copy(objects, data.Objects) 102 copy(objects[len(data.Objects):], data.Inputs) 103 104 for _, o := range objects { 105 if o.HasResolvers() { 106 fn := gqlToResolverName(data.Config.Resolver.Dir(), o.Position.Src.Name, data.Config.Resolver.FilenameTemplate) 107 if files[fn] == nil { 108 files[fn] = &File{} 109 } 110 111 caser := cases.Title(language.English, cases.NoLower) 112 rewriter.MarkStructCopied(templates.LcFirst(o.Name) + templates.UcFirst(data.Config.Resolver.Type)) 113 rewriter.GetMethodBody(data.Config.Resolver.Type, caser.String(o.Name)) 114 files[fn].Objects = append(files[fn].Objects, o) 115 } 116 for _, f := range o.Fields { 117 if !f.IsResolver { 118 continue 119 } 120 121 structName := templates.LcFirst(o.Name) + templates.UcFirst(data.Config.Resolver.Type) 122 implementation := strings.TrimSpace(rewriter.GetMethodBody(structName, f.GoFieldName)) 123 comment := strings.TrimSpace(strings.TrimLeft(rewriter.GetMethodComment(structName, f.GoFieldName), `\`)) 124 if implementation == "" { 125 implementation = fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v - %v\"))", f.GoFieldName, f.Name) 126 } 127 if comment == "" { 128 comment = fmt.Sprintf("%v is the resolver for the %v field.", f.GoFieldName, f.Name) 129 } 130 131 resolver := Resolver{o, f, comment, implementation} 132 fn := gqlToResolverName(data.Config.Resolver.Dir(), f.Position.Src.Name, data.Config.Resolver.FilenameTemplate) 133 if files[fn] == nil { 134 files[fn] = &File{} 135 } 136 137 files[fn].Resolvers = append(files[fn].Resolvers, &resolver) 138 } 139 } 140 141 for filename, file := range files { 142 file.imports = rewriter.ExistingImports(filename) 143 file.RemainingSource = rewriter.RemainingSource(filename) 144 } 145 146 for filename, file := range files { 147 resolverBuild := &ResolverBuild{ 148 File: file, 149 PackageName: data.Config.Resolver.Package, 150 ResolverType: data.Config.Resolver.Type, 151 } 152 153 err := templates.Render(templates.Options{ 154 PackageName: data.Config.Resolver.Package, 155 FileNotice: ` 156 // This file will be automatically regenerated based on the schema, any resolver implementations 157 // will be copied through when generating and any unknown code will be moved to the end. 158 // Code generated by github.com/mstephano/gqlgen-schemagen version ` + graphql.Version, 159 Filename: filename, 160 Data: resolverBuild, 161 Packages: data.Config.Packages, 162 Template: resolverTemplate, 163 }) 164 if err != nil { 165 return err 166 } 167 } 168 169 if _, err := os.Stat(data.Config.Resolver.Filename); errors.Is(err, fs.ErrNotExist) { 170 err := templates.Render(templates.Options{ 171 PackageName: data.Config.Resolver.Package, 172 FileNotice: ` 173 // This file will not be regenerated automatically. 174 // 175 // It serves as dependency injection for your app, add any dependencies you require here.`, 176 Template: `type {{.}} struct {}`, 177 Filename: data.Config.Resolver.Filename, 178 Data: data.Config.Resolver.Type, 179 Packages: data.Config.Packages, 180 }) 181 if err != nil { 182 return err 183 } 184 } 185 return nil 186 } 187 188 type ResolverBuild struct { 189 *File 190 HasRoot bool 191 PackageName string 192 ResolverType string 193 } 194 195 type File struct { 196 // These are separated because the type definition of the resolver object may live in a different file from the 197 // resolver method implementations, for example when extending a type in a different graphql schema file 198 Objects []*codegen.Object 199 Resolvers []*Resolver 200 imports []rewrite.Import 201 RemainingSource string 202 } 203 204 func (f *File) Imports() string { 205 for _, imp := range f.imports { 206 if imp.Alias == "" { 207 _, _ = templates.CurrentImports.Reserve(imp.ImportPath) 208 } else { 209 _, _ = templates.CurrentImports.Reserve(imp.ImportPath, imp.Alias) 210 } 211 } 212 return "" 213 } 214 215 type Resolver struct { 216 Object *codegen.Object 217 Field *codegen.Field 218 Comment string 219 Implementation string 220 } 221 222 func gqlToResolverName(base string, gqlname, filenameTmpl string) string { 223 gqlname = filepath.Base(gqlname) 224 ext := filepath.Ext(gqlname) 225 if filenameTmpl == "" { 226 filenameTmpl = "{name}.resolvers.go" 227 } 228 filename := strings.ReplaceAll(filenameTmpl, "{name}", strings.TrimSuffix(gqlname, ext)) 229 return filepath.Join(base, filename) 230 }