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