...

Source file src/github.com/99designs/gqlgen/plugin/resolvergen/resolver.go

Documentation: github.com/99designs/gqlgen/plugin/resolvergen

     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  }
   287  

View as plain text