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
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
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
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
236
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 }
287
View as plain text