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  }