1package generator 2 3import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "path/filepath" 8 9 "github.com/dave/jennifer/jen" 10 "github.com/graphql-go/graphql/language/ast" 11 "github.com/graphql-go/graphql/language/location" 12) 13 14// GeneratedFileExt describes the default extension used when the resulting Go 15// file is written. 16const GeneratedFileExt = ".gql.go" 17 18// DefaultPackageName refers to default name given to resulting package. 19const DefaultPackageName = "schema" 20 21// invoker is used to when generating comments to inform reader that what 22// package / app / script generated the given code. 23const defaultInvoker = "graphql/generator package" 24 25// Saver represents an item that can write generated types. 26type Saver interface { 27 Save(name string, f *jen.File) error 28} 29 30// DryRun implements Saver interface, omits writing code to disk and logs 31// result of render. 32type DryRun struct { 33 Debug bool 34} 35 36// Save runs render and logs results 37func (d *DryRun) Save(name string, f *jen.File) error { 38 buf := &bytes.Buffer{} 39 if err := f.Render(buf); err != nil { 40 logger.WithError(err).Error("unable to render generated code") 41 return err 42 } 43 logger.WithField("file", name).Info("dry-run successful; rendered without err") 44 if d.Debug { 45 logger.Print(buf) 46 } 47 return nil 48} 49 50// fileSaver writes generated types to disk given path. 51type fileSaver struct { 52 sourceDir string 53} 54 55// Save renders types to disk 56func (s fileSaver) Save(fname string, f *jen.File) error { 57 buf := &bytes.Buffer{} 58 if err := f.Render(buf); err != nil { 59 return err 60 } 61 outpath := makeOutputPath(s.sourceDir, fname, GeneratedFileExt) 62 return ioutil.WriteFile(outpath, buf.Bytes(), 0644) 63} 64 65// File extension .gql.go is used for generated files 66func makeOutputPath(dir, name, newExt string) string { 67 ext := filepath.Ext(name) 68 fpath := name[0 : len(name)-len(ext)] 69 return filepath.Join(dir, fpath+newExt) 70} 71 72// Generator generates Go code for type defnitions found in given source files. 73type Generator struct { 74 // Saver handles rendering and persisting generators output. 75 Saver 76 77 // Invoker field identifies the caller that invoked generated. Name is 78 // included in warning comment at top of generated file. 79 Invoker string 80 81 // PackageName of given to resulting files. Defaults to "schema." 82 PackageName string 83 84 source GraphQLFiles 85} 86 87// New returns new generator given path and name of package resulting file will 88// reside. 89func New(source GraphQLFiles) Generator { 90 return Generator{ 91 Saver: fileSaver{sourceDir: source.Dir()}, 92 Invoker: defaultInvoker, 93 PackageName: DefaultPackageName, 94 source: source, 95 } 96} 97 98// Run generates code and saves 99func (g Generator) Run() error { 100 // Wrap contextual information about current step 101 i := newInfo(g.source) 102 103 // Generate code for each source file 104 outfiles := make(map[string]*jen.File, len(g.source)) 105 for _, s := range g.source { 106 outfile := newFile(g.PackageName, g.Invoker) 107 generateCode(s, i, outfile) 108 outfiles[s.Filename()] = outfile 109 } 110 111 // Do dry run to ensure that the files can be written. 112 for _, outfile := range outfiles { 113 if err := outfile.Render(ioutil.Discard); err != nil { 114 return err 115 } 116 } 117 118 // Write generated code to disk. 119 for name, outfile := range outfiles { 120 if err := g.Save(name, outfile); err != nil { 121 return err 122 } 123 } 124 125 return nil 126} 127 128func generateCode(source *GraphQLFile, i info, file *jen.File) { 129 // Iterate through each definition found in the document and generate 130 // appropriate code. 131 for _, d := range source.Definitions() { 132 // Update contextual information about current iteration 133 i := withUpdatedInfo(i, source, getNodeName(d)) 134 135 // Assuming code was generated for node append to file 136 if code := genTypeDefinition(d, i); code != nil { 137 file.Add(code) 138 } 139 } 140} 141 142func genTypeDefinition(node ast.Node, i info) jen.Code { 143 loc := location.GetLocation(node.GetLoc().Source, node.GetLoc().Start) 144 logger := logger.WithField("type", node.GetKind()).WithField("line", loc.Line) 145 146 switch def := node.(type) { 147 case *ast.EnumDefinition: 148 return genEnum(def) 149 case *ast.InputObjectDefinition: 150 return genInputObject(def, i) 151 case *ast.InterfaceDefinition: 152 return genInterface(def) 153 case *ast.ObjectDefinition: 154 return genObjectType(def, i) 155 case *ast.ScalarDefinition: 156 return genScalar(def) 157 case *ast.SchemaDefinition: 158 return genSchema(def) 159 case *ast.UnionDefinition: 160 return genUnion(def) 161 case *ast.DirectiveDefinition: 162 logger.Warn("unsupported at this time; skipping") 163 case *ast.TypeExtensionDefinition: 164 return genObjectExtension(def, i) 165 default: 166 logger.Fatal("unhandled type encountered") 167 } 168 return nil 169} 170 171// Used when generating code to lookup adjacent definitions, provide contextual 172// information. 173type info struct { 174 files GraphQLFiles 175 definitions map[string]ast.Node 176 currentFile *GraphQLFile 177 currentNode string 178} 179 180func newInfo(files GraphQLFiles) info { 181 return info{ 182 files: files, 183 definitions: files.DefinitionsMap(), 184 } 185} 186 187func withUpdatedInfo(i info, file *GraphQLFile, node string) info { 188 i.currentFile = file 189 i.currentNode = node 190 return i 191} 192 193func newFile(name, invoker string) *jen.File { 194 // New file abstract w/ package name 195 file := jen.NewFile(name) 196 197 // Warning comment 198 file.HeaderComment(fmt.Sprintf("Code generated by %s. DO NOT EDIT.", invoker)) 199 file.Line() 200 201 return file 202} 203