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