├── .github
├── fanatics-eng.logo.png
└── toast.logo.png
├── LICENSE
├── README.md
├── cmd
└── toast
│ ├── main.go
│ └── plugins.go
├── collector
├── collector.go
└── primitives.go
├── go.mod
├── go.sum
├── plugin-samples
└── toast-plugin
│ └── main.go
├── plugin
├── init.go
└── util.go
└── test
├── base
└── data.go
└── item.go
/.github/fanatics-eng.logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fanatics/toast/fe5b69e9a377c42a83c1edfff9c49be8d5c49fc5/.github/fanatics-eng.logo.png
--------------------------------------------------------------------------------
/.github/toast.logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Fanatics/toast/fe5b69e9a377c42a83c1edfff9c49be8d5c49fc5/.github/toast.logo.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2018 Fanatics, Inc.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://godoc.org/github.com/Fanatics/toast)
3 |
4 | `toast` is a plugin-driven code generation tool that parses regular [Go](https://golang.org) code, rather than an IDL like Protobufs or other common defintion code.
5 |
6 | ## Why?
7 | Fanatics loves Go, we have a lot of Go code, and after using plenty of other code generation tools, it struck us that Go code as an IDL could be a nice alternative.
8 |
9 | `toast` makes it much easier to generate code from your Go code, since it gives you pre-parsed AST-like data about your packages and files. Why the name "toast", you ask? `toast` is a tool to turn your Go code "to AST" in a helpful way.
10 |
11 | ## Usage
12 |
13 | ```sh
14 | $ toast --input . \
15 | --plugin amdm_gen_db:out=./internal/db \
16 | --plugin "amdm_gen_proto --option1 value1 -o v2:out=./api/proto"
17 | ```
18 |
19 | > See a basic [**example plugin**](https://github.com/Fanatics/toast/blob/master/plugin-samples/toast-plugin/main.go) written in Go.
20 |
21 | ## Installation
22 |
23 | If you have Go installed, run:
24 | ```sh
25 | $ go get github.com/Fanatics/toast/...
26 | ```
27 |
28 | Once the project is at a more stable point, pre-built binaries will be made
29 | available for download across common platforms.
30 |
31 | ## Status
32 | `toast` is still under heavy development. Tests are still being written, APIs
33 | will change without notice. However, feel free to give it a spin, open issues and submit PRs.
34 |
35 | The simplified AST that is sent to a plugin's stdin may (and probably should) change from version to version. It is incomplete, and once tests are ready, we will have a better idea of what else is left to parse and collect.
36 |
37 | **Please open issues if/when you encounter incompleteness or errors!**
38 |
39 |
40 | ## Disclaimer
41 | This is not an official Fanatics product (experimental or otherwise), it is just code that happens to be owned by Fanatics.
42 |
43 |
44 | 
45 | Want to work on projects like this?
46 |
47 | Get in touch.
48 |
49 |
--------------------------------------------------------------------------------
/cmd/toast/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "flag"
7 | "fmt"
8 | "go/ast"
9 | "go/parser"
10 | "go/token"
11 | "log"
12 | "os"
13 | "path/filepath"
14 |
15 | "github.com/Fanatics/toast/collector"
16 | "github.com/tidwall/sjson"
17 | )
18 |
19 | const toastPrefix = "[toast]"
20 |
21 | var plugins *plugin
22 |
23 | func main() {
24 | input := flag.String("input", ".", "input directory from where to parse Go code")
25 | debug := flag.Bool("debug", false, "write data from parsed AST to stdout, skips plugins")
26 | flag.Var(plugins, "plugin", "executable plugin for toast to invoke, and the output base directory for files to be written")
27 | flag.Parse()
28 |
29 | fset := token.NewFileSet()
30 | data := &collector.Data{}
31 |
32 | err := filepath.Walk(*input, func(path string, fi os.FileInfo, err error) error {
33 | if err != nil {
34 | log.Fatal("recursive walk error:", err)
35 | }
36 | // skip over files, only continue into directories for parser to enter
37 | if !fi.IsDir() {
38 | return nil
39 | }
40 |
41 | pkgs, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
42 | if err != nil {
43 | log.Fatalf("parse dir error: %v\n", err)
44 | }
45 | for _, pkg := range pkgs {
46 | p := collector.Package{
47 | Name: pkg.Name,
48 | }
49 | for _, file := range pkg.Files {
50 | c := &collector.FileCollector{}
51 | ast.Walk(c, file)
52 | f := collector.File{
53 | Name: fset.Position(file.Pos()).Filename,
54 | Package: pkg.Name,
55 | Imports: c.Imports,
56 | BuildTags: c.BuildTags,
57 | Comments: c.Comments,
58 | MagicComments: c.MagicComments,
59 | GenerateComments: c.GenerateComments,
60 | Consts: c.Consts,
61 | Vars: c.Vars,
62 | Structs: c.Structs,
63 | TypeDefs: c.TypeDefs,
64 | Interfaces: c.Interfaces,
65 | Funcs: c.Funcs,
66 | }
67 | p.Files = append(p.Files, f)
68 | }
69 | data.Packages = append(data.Packages, p)
70 | }
71 |
72 | return nil
73 | })
74 | if err != nil {
75 | exitWithMessage("filepath walk error", err)
76 | }
77 |
78 | // debug mode enables users to inspect the raw JSON on the command line
79 | if *debug {
80 | b, err := json.MarshalIndent(data, "", " ")
81 | if err != nil {
82 | exitWithMessage("(debug) JSON encode error", err)
83 | }
84 | // write the data to stdout and skip the plugin execution
85 | fmt.Println(string(b))
86 | return
87 | }
88 |
89 | b, err := json.Marshal(data)
90 | if err != nil {
91 | exitWithMessage("JSON encode error", err)
92 | os.Exit(1)
93 | }
94 |
95 | err = plugins.each(func(i int, p *plugin) error {
96 | // replace the output base value for each plugin rather than decoding,
97 | // re-assigning the value, and re-encoding
98 | b, err := sjson.SetBytes(b, "output_base", []byte(p.outputDir))
99 | if err != nil {
100 | return err
101 | }
102 | // set the plugin into a runner and execute it, passing in the data
103 | exe := &runner{
104 | p: p,
105 | data: bytes.NewReader(b),
106 | }
107 | if err := exe.run(); err != nil {
108 | return err
109 | }
110 | return nil
111 | })
112 | if err != nil {
113 | // err is a collection of errors, one per line, from all of the plugins
114 | fmt.Println(toastPrefix, "accumulated plugin errors:")
115 | fmt.Println(err)
116 | os.Exit(1)
117 | }
118 | }
119 |
120 | func exitWithMessage(msg string, err error) {
121 | fmt.Println(toastPrefix, msg, err)
122 | os.Exit(1)
123 | }
124 |
--------------------------------------------------------------------------------
/cmd/toast/plugins.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "os"
8 | "os/exec"
9 | "strings"
10 | )
11 |
12 | type plugin struct {
13 | cmd *exec.Cmd
14 | outputDir string
15 | }
16 |
17 | type runner struct {
18 | p *plugin
19 | data io.Reader
20 | }
21 |
22 | const (
23 | outPrefix = "out="
24 | pluginErrPrefix = "[toast:plugin]"
25 | )
26 |
27 | var pluginList []plugin
28 |
29 | func (p *plugin) String() string {
30 | var all []string
31 | for _, plug := range pluginList {
32 | all = append(all, fmt.Sprintf(
33 | "plugin command: %s, output: [%s]",
34 | plug.cmd.Args, plug.outputDir,
35 | ))
36 | }
37 |
38 | return strings.Join(all, "\n")
39 | }
40 |
41 | func (p *plugin) Set(value string) error {
42 | pluginParts := strings.Split(value, ":")
43 | if len(pluginParts) < 2 {
44 | return fmt.Errorf("invalid plugin flag value: %s", value)
45 | }
46 |
47 | var pluginCmd, pluginOutput string
48 | var pluginOptions []string
49 |
50 | pluginCmdVals := strings.Split(pluginParts[0], " ")
51 | if len(pluginCmdVals) == 0 {
52 | return fmt.Errorf("invalid plugin flag value (bad command): %s", value)
53 | }
54 |
55 | pluginCmd = pluginCmdVals[0]
56 | if strings.HasPrefix(pluginParts[1], outPrefix) {
57 | // plugin was passed no options, and output path is second part
58 | pluginOutput = pluginParts[1]
59 | } else {
60 | return fmt.Errorf("invalid plugin flag value (bad out): %s", value)
61 | }
62 | if len(pluginCmdVals) > 1 {
63 | // plugin was passed options as second part, output is third
64 | pluginOptions = pluginCmdVals[1:]
65 | }
66 |
67 | outputVals := strings.Split(pluginOutput, "=")
68 | if !strings.HasPrefix(pluginOutput, outPrefix) || len(outputVals) < 2 {
69 | return fmt.Errorf("invalid plugin out value: %s", pluginOutput)
70 | }
71 | baseOutputDir := outputVals[1]
72 |
73 | pluginList = append(pluginList, plugin{
74 | cmd: exec.Command(pluginCmd, pluginOptions...),
75 | outputDir: baseOutputDir,
76 | })
77 |
78 | return nil
79 | }
80 |
81 | func (p *plugin) each(fn func(idx int, plug *plugin) error) error {
82 | errChan := make(chan error)
83 | done := make(chan struct{})
84 | var errs []string
85 | go func() {
86 | for {
87 | select {
88 | case <-done:
89 | return
90 |
91 | case err := <-errChan:
92 | errs = append(errs, err.Error())
93 | }
94 | }
95 | }()
96 |
97 | for i := range pluginList {
98 | plug := &pluginList[i]
99 | err := fn(i, plug)
100 | if err != nil {
101 | errChan <- fmt.Errorf(
102 | "%s %s: %v (%s)",
103 | pluginErrPrefix, plug.cmd.Args[0], err, plug.cmd.Path,
104 | )
105 | }
106 | }
107 | done <- struct{}{}
108 | if errs != nil {
109 | return errors.New(strings.Join(errs, "\n"))
110 | }
111 |
112 | return nil
113 | }
114 |
115 | func (r *runner) run() error {
116 | r.p.cmd.Stdin = r.data
117 | r.p.cmd.Stdout = os.Stdout
118 | r.p.cmd.Stderr = os.Stderr
119 |
120 | _, err := exec.LookPath(r.p.cmd.Args[0])
121 | if err != nil {
122 | return err
123 | }
124 |
125 | return r.p.cmd.Run()
126 | }
127 |
--------------------------------------------------------------------------------
/collector/collector.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "fmt"
5 | "go/ast"
6 | "go/printer"
7 | "go/token"
8 | "strings"
9 | )
10 |
11 | const (
12 | slashes = `//`
13 | magicPrefix = slashes + `go:`
14 | gogenPrefix = magicPrefix + `generate`
15 | buildPrefix = slashes + ` +build`
16 | ellipsis = "..."
17 | interfaceLit = "interface{}"
18 | literalLit = "literal"
19 | typeLit = "type"
20 | mapLit = "map"
21 | arrayLit = "array"
22 | sliceLit = "slice"
23 | funcLit = "func"
24 | chanLit = "chan"
25 | )
26 |
27 | type FileCollector struct {
28 | Imports []Import
29 | Consts []Const
30 | Vars []Var
31 | Structs []Struct
32 | TypeDefs []TypeDefinition
33 | Interfaces []Interface
34 | Funcs []Func
35 | Comments []Comment
36 | MagicComments []MagicComment
37 | GenerateComments []GenerateComment
38 | BuildTags []Constraint
39 | }
40 |
41 | func (c *FileCollector) Visit(node ast.Node) ast.Visitor {
42 | if node == nil {
43 | return c
44 | }
45 |
46 | // only collect declarations at file-level
47 | file, ok := node.(*ast.File)
48 | if !ok {
49 | return c
50 | }
51 |
52 | // collect all file-level comments, including doc comments associated with
53 | // other file-level declarations
54 | // additionally, check each comment for special comment prefixes and save
55 | // matches accordingly, e.g. // +build, //go:
56 | c.collectComments(file)
57 |
58 | // collect all file-level imports
59 | c.collectImports(file)
60 |
61 | unresolvedTypes := make(map[string]*TypeDefinition)
62 | structs := make(map[string]*Struct)
63 | types := make(map[string]*TypeDefinition)
64 | interfaces := make([]Interface, 0)
65 | funcs := make([]Func, 0)
66 | vars := make([]Var, 0)
67 | consts := make([]Const, 0)
68 |
69 | // iterate through declarations within the file
70 | for _, decl := range file.Decls {
71 | switch n := decl.(type) {
72 | case *ast.FuncDecl:
73 | magicComments, generateComments := specialComments(n.Doc)
74 |
75 | // find methods on receiver types (will have a Recv prop)
76 | method := Method{
77 | Name: n.Name.Name,
78 | IsExported: isExported(n.Name),
79 | Doc: normalizeComment(n.Doc),
80 | MagicComments: magicComments,
81 | GenerateComments: generateComments,
82 | }
83 | if n.Recv != nil {
84 | var exportedRecv bool
85 | // get the receiver's name and check if it is a pointer
86 | for i := range n.Recv.List {
87 | if recv, ok := n.Recv.List[i].Type.(*ast.Ident); ok {
88 | exportedRecv = isExported(recv)
89 | method.Receiver = recv.Name
90 | break
91 | }
92 | }
93 | for i := range n.Recv.List {
94 | if ptr, ok := n.Recv.List[i].Type.(*ast.StarExpr); ok {
95 | method.Receiver = ptr.X.(*ast.Ident).Name
96 | method.ReceiverIndirect = true
97 | break
98 | }
99 | }
100 |
101 | // if the receiver type has already been encountered
102 | // and stored in our unresolved type map, add this method to it
103 | if t, ok := unresolvedTypes[method.Receiver]; ok {
104 | t.IsExported = exportedRecv
105 | t.Methods = append(t.Methods, method)
106 | } else {
107 | // otherwise, create the type def and insert it into
108 | // the struct map
109 | unresolvedTypes[method.Receiver] = &TypeDefinition{
110 | Name: method.Receiver,
111 | IsExported: exportedRecv,
112 | Methods: []Method{method},
113 | }
114 | }
115 | continue
116 | }
117 |
118 | // if the func has no reciever, collect it as a basic function
119 | funcs = append(funcs, Func{
120 | Name: n.Name.Name,
121 | Doc: normalizeComment(n.Doc),
122 | IsExported: isExported(n.Name),
123 | Params: funcFields(n.Type.Params),
124 | Results: funcFields(n.Type.Results),
125 | MagicComments: magicComments,
126 | GenerateComments: generateComments,
127 | })
128 |
129 | case *ast.GenDecl:
130 | for _, spec := range n.Specs {
131 | switch s := spec.(type) {
132 | case *ast.ValueSpec:
133 | // find and stash values including file-level constants and
134 | // variables
135 | for _, ident := range s.Names {
136 | if ident.Obj != nil {
137 | magic, generate := specialComments(s.Doc)
138 | var valType string
139 | for _, val := range s.Values {
140 | if lit, ok := val.(*ast.BasicLit); ok {
141 | valType = lit.Kind.String()
142 | }
143 | }
144 | switch ident.Obj.Kind {
145 | case ast.Var:
146 | val := value(s)
147 | if val == nil {
148 | continue
149 | }
150 | vars = append(vars, Var{
151 | IsExported: isExported(ident),
152 | Name: ident.Name,
153 | Value: val,
154 | Type: valType,
155 | Doc: normalizeComment(s.Doc),
156 | Comment: normalizeComment(s.Comment),
157 | MagicComments: magic,
158 | GenerateComments: generate,
159 | })
160 |
161 | case ast.Con:
162 | val := value(s)
163 | if val == nil {
164 | continue
165 | }
166 | consts = append(consts, Const{
167 | IsExported: isExported(ident),
168 | Name: ident.Name,
169 | Value: val,
170 | Type: valType,
171 | Doc: normalizeComment(s.Doc),
172 | Comment: normalizeComment(s.Comment),
173 | MagicComments: magic,
174 | GenerateComments: generate,
175 | })
176 | }
177 | }
178 | }
179 |
180 | case *ast.TypeSpec:
181 | // find and stash the structs
182 | if strct, ok := s.Type.(*ast.StructType); ok {
183 | var fields []StructField
184 | for _, field := range strct.Fields.List {
185 | var (
186 | fType interface{}
187 | indirect bool
188 | isArray bool
189 | arrayLen string
190 | isSlice bool
191 | isMap bool
192 | exportedField bool
193 | )
194 |
195 | fName := identName(field.Names)
196 | for _, nm := range field.Names {
197 | if isExported(nm) {
198 | exportedField = true
199 | break
200 | }
201 | }
202 |
203 | switch t := field.Type.(type) {
204 | case *ast.SelectorExpr:
205 | typ := t.Sel.Name
206 | slct := t.X.(*ast.Ident).Name
207 | fType = ValueType{
208 | Kind: typeLit,
209 | Value: slct + "." + typ,
210 | }
211 | case *ast.StarExpr:
212 | indirect = true
213 | // check if we have a selector (within package, etc)
214 | // expression prepending the identifier
215 | if sel, ok := t.X.(*ast.SelectorExpr); ok {
216 | typ := sel.Sel.Name
217 | slct := sel.X.(*ast.Ident).Name
218 | fType = ValueType{
219 | Kind: typeLit,
220 | Value: slct + "." + typ,
221 | }
222 | } else {
223 | switch typ := t.X.(type) {
224 | case *ast.Ident:
225 | fType = ValueType{
226 | Kind: typeLit,
227 | Value: typ.Name,
228 | }
229 |
230 | case *ast.ArrayType:
231 | var kind string
232 | if typ.Len == nil {
233 | kind = sliceLit
234 | } else {
235 | kind = arrayLit
236 | }
237 |
238 | fType = ValueType{
239 | Kind: kind,
240 | Value: typeName(typ.Elt),
241 | }
242 | }
243 | }
244 |
245 | case *ast.Ident:
246 | if star, ok := field.Type.(*ast.StarExpr); ok {
247 | indirect = true
248 | fType = ValueType{
249 | Kind: typeLit,
250 | Value: star.X.(*ast.Ident).Name,
251 | }
252 | } else {
253 | fType = field.Type.(*ast.Ident).Name
254 | }
255 |
256 | case *ast.ChanType:
257 | fType = ValueType{
258 | Kind: chanLit,
259 | Value: channelType(t),
260 | }
261 |
262 | case *ast.MapType:
263 | isMap = true
264 | fType = ValueType{
265 | Kind: mapLit,
266 | Value: mapType(t),
267 | }
268 |
269 | case *ast.ArrayType:
270 | if t.Len == nil {
271 | isSlice = true
272 | } else {
273 | isArray = true
274 | switch l := t.Len.(type) {
275 | case *ast.BasicLit:
276 | arrayLen = l.Value
277 | case *ast.Ident:
278 | arrayLen = l.Name
279 | }
280 | }
281 | switch fieldType := t.Elt.(type) {
282 | case *ast.StarExpr:
283 | indirect = true
284 | fType = ValueType{
285 | Kind: typeLit,
286 | Value: typeName(fieldType.X),
287 | }
288 |
289 | case *ast.InterfaceType:
290 | fType = interfaceLit
291 |
292 | case *ast.Ident:
293 | fType = fieldType.Name
294 |
295 | case *ast.MapType:
296 | fType = ValueType{
297 | Kind: mapLit,
298 | Value: mapType(fieldType),
299 | }
300 |
301 | case *ast.ChanType:
302 | fType = ValueType{
303 | Kind: chanLit,
304 | Value: channelType(fieldType),
305 | }
306 | }
307 | }
308 |
309 | magic, generate := specialComments(field.Doc)
310 | fields = append(fields, StructField{
311 | Name: fName,
312 | Type: fType,
313 | Tag: fieldTag(field),
314 | Embed: fName == "",
315 | Indirect: indirect,
316 | IsArray: isArray,
317 | ArrayLen: arrayLen,
318 | IsSlice: isSlice,
319 | IsMap: isMap,
320 | IsExported: exportedField,
321 | Doc: normalizeComment(field.Doc),
322 | Comment: normalizeComment(field.Comment),
323 | MagicComments: magic,
324 | GenerateComments: generate,
325 | })
326 | }
327 |
328 | doc := normalizeComment(n.Doc)
329 | comment := normalizeComment(s.Comment)
330 |
331 | if strct, ok := structs[s.Name.Name]; ok {
332 | strct.Doc = doc
333 | strct.Comment = comment
334 | strct.Fields = fields
335 | } else {
336 | structs[s.Name.Name] = &Struct{
337 | Name: s.Name.Name,
338 | Doc: doc,
339 | Comment: comment,
340 | Fields: fields,
341 | }
342 | }
343 | }
344 |
345 | // find and stash the interfaces
346 | if iface, ok := s.Type.(*ast.InterfaceType); ok {
347 | magic, generate := specialComments(s.Doc)
348 | interfaces = append(interfaces, Interface{
349 | IsExported: isExported(s.Name),
350 | Name: s.Name.Name,
351 | Doc: normalizeComment(s.Doc),
352 | Comment: normalizeComment(s.Comment),
353 | MethodSet: methodSet(iface),
354 | MagicComments: magic,
355 | GenerateComments: generate,
356 | })
357 | }
358 |
359 | // find and stash other type definitions
360 | if ident, ok := s.Type.(*ast.Ident); ok {
361 | magic, generate := specialComments(s.Doc)
362 |
363 | def := &TypeDefinition{
364 | IsExported: isExported(s.Name),
365 | Name: s.Name.Name,
366 | Type: ident.Name,
367 | Doc: normalizeComment(s.Doc),
368 | Comment: normalizeComment(s.Comment),
369 | MagicComments: magic,
370 | GenerateComments: generate,
371 | }
372 |
373 | // if the type def was already encountered from finding
374 | // one of its methods, add the detailed data to the map
375 | // to the existing type def
376 | if td, ok := types[s.Name.Name]; ok {
377 | def.Methods = td.Methods
378 | types[s.Name.Name] = def
379 | } else {
380 | types[s.Name.Name] = def
381 | }
382 | }
383 | }
384 | }
385 | }
386 | }
387 |
388 | for k, v := range structs {
389 | // capture methods from unresolved type def map and provide to the
390 | // actual struct encountered
391 | if utd, ok := unresolvedTypes[k]; ok {
392 | v.Methods = append(v.Methods, utd.Methods...)
393 | }
394 |
395 | c.Structs = append(c.Structs, *v)
396 | }
397 |
398 | for k, v := range types {
399 | // capture methods from unresolved type def map and provide to the
400 | // actual type definition encountered
401 | if utd, ok := unresolvedTypes[k]; ok {
402 | v.Methods = append(v.Methods, utd.Methods...)
403 | }
404 |
405 | c.TypeDefs = append(c.TypeDefs, *v)
406 | }
407 |
408 | c.Vars = vars
409 | c.Consts = consts
410 | c.Funcs = funcs
411 | c.Interfaces = interfaces
412 |
413 | return c
414 | }
415 |
416 | func (c *FileCollector) collectImports(file *ast.File) {
417 | if file == nil {
418 | return
419 | }
420 |
421 | for _, imp := range file.Imports {
422 | var name string
423 | if imp.Name != nil {
424 | name = imp.Name.Name
425 | }
426 | magic, generate := specialComments(imp.Doc)
427 | c.Imports = append(c.Imports, Import{
428 | Name: name,
429 | Path: imp.Path.Value,
430 | Doc: normalizeComment(imp.Doc),
431 | Comment: normalizeComment(imp.Comment),
432 | MagicComments: magic,
433 | GenerateComments: generate,
434 | })
435 | }
436 | }
437 |
438 | func (c *FileCollector) collectComments(file *ast.File) {
439 | if file == nil {
440 | return
441 | }
442 |
443 | for _, group := range file.Comments {
444 | for _, com := range group.List {
445 | switch {
446 | case strings.HasPrefix(com.Text, buildPrefix):
447 | opts := strings.TrimSpace(
448 | strings.TrimPrefix(com.Text, buildPrefix),
449 | )
450 | constraint := Constraint{
451 | Options: strings.Split(opts, " "),
452 | }
453 | c.BuildTags = append(c.BuildTags, constraint)
454 |
455 | case strings.HasPrefix(com.Text, magicPrefix):
456 | switch nsc := nonStandardComment(com).(type) {
457 | case *MagicComment:
458 | if nsc != nil {
459 | c.MagicComments = append(c.MagicComments, *nsc)
460 | }
461 |
462 | case *GenerateComment:
463 | if nsc != nil {
464 | c.GenerateComments = append(c.GenerateComments, *nsc)
465 | }
466 | }
467 | }
468 | }
469 | c.Comments = append(c.Comments, normalizeComment(group))
470 | }
471 | }
472 |
473 | func mapType(m *ast.MapType) Map {
474 | var kv Map
475 | switch k := m.Key.(type) {
476 | case *ast.Ident:
477 | kv.KeyType = k.Name
478 |
479 | case *ast.BasicLit:
480 | kv.KeyType = k.Value
481 |
482 | case *ast.StarExpr:
483 | if selExp, ok := k.X.(*ast.SelectorExpr); ok {
484 | pkg := selExp.X.(*ast.Ident).Name
485 | sel := selExp.Sel.Name
486 | kv.KeyType = fmt.Sprintf("*%s.%s", pkg, sel)
487 | } else {
488 | kv.KeyType = "*" + k.X.(*ast.Ident).Name
489 | }
490 |
491 | case *ast.SelectorExpr:
492 | pkg := k.X.(*ast.Ident).Name
493 | sel := k.Sel.Name
494 | kv.KeyType = fmt.Sprintf("%s.%s", pkg, sel)
495 |
496 | case *ast.InterfaceType:
497 | kv.KeyType = interfaceLit
498 | }
499 |
500 | switch v := m.Value.(type) {
501 | case *ast.Ident:
502 | kv.ValueType = MapValue{
503 | Name: literalLit,
504 | Value: v.Name,
505 | }
506 |
507 | case *ast.BasicLit:
508 | kv.ValueType = MapValue{
509 | Name: literalLit,
510 | Value: v.Value,
511 | }
512 |
513 | case *ast.StarExpr:
514 | if selExp, ok := v.X.(*ast.SelectorExpr); ok {
515 | pkg := selExp.X.(*ast.Ident).Name
516 | sel := selExp.Sel.Name
517 | kv.ValueType = MapValue{
518 | Name: typeLit,
519 | Value: fmt.Sprintf("*%s.%s", pkg, sel),
520 | }
521 | } else {
522 | kv.ValueType = MapValue{
523 | Name: typeLit,
524 | Value: "*" + v.X.(*ast.Ident).Name,
525 | }
526 | }
527 |
528 | case *ast.SelectorExpr:
529 | pkg := v.X.(*ast.Ident).Name
530 | sel := v.Sel.Name
531 | kv.ValueType = MapValue{
532 | Name: typeLit,
533 | Value: fmt.Sprintf("%s.%s", pkg, sel),
534 | }
535 |
536 | case *ast.ArrayType:
537 | var size string
538 | valTypeName := arrayLit
539 | if v.Len != nil {
540 | valTypeName = sliceLit
541 | switch l := v.Len.(type) {
542 | case *ast.BasicLit:
543 | size = l.Value
544 |
545 | case *ast.Ellipsis:
546 | size = ellipsis
547 | }
548 |
549 | }
550 | arrType := typeName(v.Elt)
551 | kv.ValueType = MapValue{
552 | Name: valTypeName,
553 | Value: fmt.Sprintf("[%s]%s", size, arrType),
554 | }
555 |
556 | case *ast.MapType:
557 | kv.ValueType = MapValue{
558 | Name: mapLit,
559 | Value: mapType(v),
560 | }
561 |
562 | case *ast.FuncType:
563 | kv.ValueType = MapValue{
564 | Name: funcLit,
565 | Value: Func{
566 | IsExported: false, // documenting that func literal cannot be exported e.g. `func() {}`
567 | Params: funcFields(v.Params),
568 | Results: funcFields(v.Results),
569 | },
570 | }
571 | case *ast.InterfaceType:
572 | kv.ValueType = MapValue{
573 | Name: interfaceLit,
574 | Value: interfaceLit,
575 | }
576 |
577 | case *ast.ChanType:
578 | kv.ValueType = MapValue{
579 | Name: chanLit,
580 | Value: Channel{
581 | Type: typeName(v.Value),
582 | RecvOnly: v.Dir == ast.RECV,
583 | SendOnly: v.Dir == ast.SEND,
584 | },
585 | }
586 | }
587 |
588 | return kv
589 | }
590 |
591 | func channelType(ch *ast.ChanType) Channel {
592 | return Channel{
593 | Type: typeName(ch.Value),
594 | RecvOnly: ch.Dir == ast.RECV,
595 | SendOnly: ch.Dir == ast.SEND,
596 | }
597 | }
598 |
599 | func value(s *ast.ValueSpec) interface{} {
600 | for _, val := range s.Values {
601 | switch expr := val.(type) {
602 | case *ast.BasicLit:
603 | return expr.Value
604 | case *ast.CompositeLit:
605 | switch expr := expr.Type.(type) {
606 | case *ast.BasicLit:
607 | return expr.Value
608 | }
609 | case *ast.StarExpr:
610 | switch expr := expr.X.(type) {
611 | case *ast.BasicLit:
612 | return "*" + expr.Value
613 | case *ast.CompositeLit:
614 | switch expr := expr.Type.(type) {
615 | case *ast.BasicLit:
616 | return "*" + expr.Value
617 | }
618 | }
619 | }
620 | }
621 | return nil
622 | }
623 |
624 | func rawExpression(expr ast.Expr) (string, error) {
625 | buf := &strings.Builder{}
626 | err := printer.Fprint(buf, token.NewFileSet(), expr)
627 | if err != nil {
628 | return "", err
629 | }
630 | return buf.String(), nil
631 | }
632 |
633 | func specialComments(doc *ast.CommentGroup) ([]MagicComment, []GenerateComment) {
634 | if doc == nil {
635 | return nil, nil
636 | }
637 |
638 | var magicComments []MagicComment
639 | var generateComments []GenerateComment
640 | for _, doc := range doc.List {
641 | switch nsc := nonStandardComment(doc).(type) {
642 | case *MagicComment:
643 | if nsc != nil {
644 | magicComments = append(magicComments, *nsc)
645 | }
646 | case *GenerateComment:
647 | if nsc != nil {
648 | generateComments = append(generateComments, *nsc)
649 | }
650 | }
651 | }
652 |
653 | return magicComments, generateComments
654 | }
655 |
656 | func nonStandardComment(com *ast.Comment) interface{} {
657 | if com == nil {
658 | return nil
659 | }
660 |
661 | switch {
662 | case strings.HasPrefix(com.Text, gogenPrefix):
663 | return &GenerateComment{
664 | Command: strings.TrimSpace(
665 | strings.TrimPrefix(com.Text, gogenPrefix),
666 | ),
667 | Raw: com.Text,
668 | }
669 | case strings.HasPrefix(com.Text, magicPrefix):
670 | return &MagicComment{
671 | Pragma: strings.TrimSpace(
672 | strings.TrimPrefix(com.Text, magicPrefix),
673 | ),
674 | Raw: com.Text,
675 | }
676 | }
677 | return nil
678 | }
679 |
680 | func methodSet(iface *ast.InterfaceType) []InterfaceField {
681 | if iface == nil {
682 | return nil
683 | }
684 |
685 | var fields []InterfaceField
686 | for _, field := range iface.Methods.List {
687 | switch ifaceField := field.Type.(type) {
688 | case *ast.SelectorExpr:
689 | pkg := ifaceField.X.(*ast.Ident).Name
690 | sel := ifaceField.Sel.Name
691 | embd := Interface{
692 | Name: fmt.Sprintf("%s.%s", pkg, sel),
693 | IsExported: isExported(ifaceField.Sel),
694 | Embed: true,
695 | }
696 | fields = append(fields, embd)
697 |
698 | case *ast.Ident:
699 | embd := Interface{
700 | Name: ifaceField.Name,
701 | IsExported: isExported(ifaceField),
702 | Embed: true,
703 | }
704 | fields = append(fields, embd)
705 |
706 | case *ast.FuncType:
707 | var name string
708 | var exported bool
709 | if field.Names != nil {
710 | name = typeName(field.Names[0]).(string)
711 | exported = isExported(field.Names[0])
712 | }
713 | fn := Func{
714 | Name: name,
715 | IsExported: exported,
716 | Doc: normalizeComment(field.Doc),
717 | Comment: normalizeComment(field.Comment),
718 | Params: funcFields(ifaceField.Params),
719 | Results: funcFields(ifaceField.Results),
720 | }
721 | fields = append(fields, fn)
722 | }
723 | }
724 |
725 | return fields
726 | }
727 |
728 | func funcFields(list *ast.FieldList) []Value {
729 | if list == nil {
730 | return nil
731 | }
732 | if list.List == nil {
733 | return nil
734 | }
735 |
736 | var vals []Value
737 | for _, part := range list.List {
738 | var name *string
739 | if part.Names != nil {
740 | name = &part.Names[0].Name
741 | }
742 | vals = append(vals, Value{
743 | Name: name,
744 | Type: typeName(part.Type).(string),
745 | })
746 | }
747 |
748 | return vals
749 | }
750 |
751 | func typeName(expr ast.Expr) interface{} {
752 | str := &strings.Builder{}
753 | printer.Fprint(str, token.NewFileSet(), expr)
754 | return str.String()
755 | }
756 |
757 | func arrayTypeName(arr *ast.ArrayType) string {
758 | const tmpl = `[%s]%s`
759 |
760 | arrType := typeName(arr.Elt)
761 | if arr.Len == nil {
762 | return fmt.Sprintf(tmpl, "", arrType)
763 | }
764 |
765 | size := arr.Len.(*ast.BasicLit).Value
766 | return fmt.Sprintf(tmpl, size, arrType)
767 | }
768 |
769 | func normalizeComment(docs *ast.CommentGroup) Comment {
770 | if docs == nil {
771 | return Comment{Content: ""}
772 | }
773 |
774 | var all []string
775 | for _, c := range docs.List {
776 | // ignore non-standard comments, which will be available as properties
777 | // objects where appropriate
778 | switch nonStandardComment(c).(type) {
779 | case *MagicComment, *GenerateComment:
780 | continue
781 | }
782 |
783 | all = append(all, strings.TrimSpace(c.Text))
784 | }
785 |
786 | return Comment{Content: strings.Join(all, "")}
787 | }
788 |
789 | func identName(names []*ast.Ident) string {
790 | if names == nil {
791 | return ""
792 | }
793 |
794 | for _, name := range names {
795 | return name.Name
796 | }
797 |
798 | return ""
799 | }
800 |
801 | func fieldTag(field *ast.Field) string {
802 | if field.Tag != nil {
803 | return field.Tag.Value
804 | }
805 |
806 | return ""
807 | }
808 |
809 | func isExported(ident *ast.Ident) bool {
810 | if ident.Name == "" || ident == nil {
811 | return false
812 | }
813 |
814 | return ast.IsExported(ident.Name)
815 | }
816 |
--------------------------------------------------------------------------------
/collector/primitives.go:
--------------------------------------------------------------------------------
1 | package collector
2 |
3 | import (
4 | "fmt"
5 | "os/exec"
6 | "strings"
7 | )
8 |
9 | type Data struct {
10 | OutputBase string `json:"output_base"`
11 | Packages []Package `json:"packages,omitempty"`
12 | }
13 |
14 | type Package struct {
15 | Name string `json:"name,omitempty"`
16 | Files []File `json:"files,omitempty"`
17 | }
18 |
19 | type File struct {
20 | Name string `json:"name,omitempty"`
21 | Package string `json:"package,omitempty"`
22 | Imports []Import `json:"imports,omitempty"`
23 | TypeDefs []TypeDefinition `json:"type_defs,omitempty"`
24 | Structs []Struct `json:"structs,omitempty"`
25 | Interfaces []Interface `json:"interfaces,omitempty"`
26 | Funcs []Func `json:"funcs,omitempty"`
27 | Consts []Const `json:"consts,omitempty"`
28 | Vars []Var `json:"vars,omitempty"`
29 | Comments []Comment `json:"comments,omitempty"`
30 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
31 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
32 | BuildTags []Constraint `json:"build_tags,omitempty"`
33 | }
34 |
35 | type StructField struct {
36 | Indirect bool `json:"indirect,omitempty"`
37 | Embed bool `json:"embed,omitempty"`
38 | IsMap bool `json:"is_map,omitempty"`
39 | IsExported bool `json:"is_exported,omitempty"`
40 | IsInterface bool `json:"is_interface,omitempty"`
41 | IsSlice bool `json:"is_slice,omitempty"`
42 | IsArray bool `json:"is_array,omitempty"`
43 | ArrayLen string `json:"array_length,omitempty"`
44 | Name string `json:"name,omitempty"`
45 | Doc Comment `json:"doc,omitempty"`
46 | Comment Comment `json:"comment,omitempty"`
47 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
48 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
49 | Type interface{} `json:"field_type,omitempty"`
50 | Tag string `json:"tag,omitempty"`
51 | }
52 |
53 | type Method struct {
54 | IsExported bool `json:"is_exported,omitempty"`
55 | Name string `json:"name,omitempty"`
56 | Doc Comment `json:"doc,omitempty"`
57 | Comment Comment `json:"comment,omitempty"`
58 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
59 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
60 | Receiver string `json:"receiver,omitempty"`
61 | ReceiverIndirect bool `json:"receiver_indirect,omitempty"`
62 | Params []Value `json:"params,omitempty"`
63 | Results []Value `json:"results,omitempty"`
64 | }
65 |
66 | type Struct struct {
67 | IsExported bool `json:"is_exported,omitempty"`
68 | Name string `json:"name,omitempty"`
69 | Doc Comment `json:"doc,omitempty"`
70 | Comment Comment `json:"comment,omitempty"`
71 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
72 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
73 | Fields []StructField `json:"fields,omitempty"`
74 | Methods []Method `json:"methods,omitempty"`
75 | }
76 |
77 | type TypeDefinition struct {
78 | IsExported bool `json:"is_exported,omitempty"`
79 | Name string `json:"name,omitempty"`
80 | Type string `json:"type,omitempty"`
81 | Doc Comment `json:"doc,omitempty"`
82 | Comment Comment `json:"comment,omitempty"`
83 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
84 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
85 | Methods []Method `json:"methods,omitempty"`
86 | }
87 |
88 | type Map struct {
89 | KeyType string `json:"key_type,omitempty"`
90 | ValueType MapValue `json:"value_type,omitempty"`
91 | }
92 |
93 | func (m Map) String() string {
94 | return fmt.Sprintf("map[%s]%s", m.KeyType, m.ValueType)
95 | }
96 |
97 | type MapValue struct {
98 | Name string `json:"name,omitempty"`
99 | Value interface{} `json:"value,omitempty"`
100 | }
101 |
102 | type Func struct {
103 | IsExported bool `json:"is_exported,omitempty"`
104 | Name string `json:"name,omitempty"`
105 | Doc Comment `json:"doc,omitempty"`
106 | Comment Comment `json:"comment,omitempty"`
107 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
108 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
109 | Params []Value `json:"params,omitempty"`
110 | Results []Value `json:"results,omitempty"`
111 | }
112 |
113 | type Channel struct {
114 | Type interface{} `json:"type,omitempty"`
115 | RecvOnly bool `json:"recv_only,omitempty"`
116 | SendOnly bool `json:"send_only,omitempty"`
117 | }
118 |
119 | type ValueType struct {
120 | Kind string `json:"kind,omitempty"`
121 | Value interface{} `json:"value,omitempty"`
122 | }
123 |
124 | type Value struct {
125 | Name *string `json:"name,omitempty"`
126 | Type string `json:"type,omitempty"`
127 | }
128 |
129 | func (v Value) String() string {
130 | if v.Name == nil {
131 | n := ""
132 | v.Name = &n
133 | }
134 | return fmt.Sprintf("Value{%s, %s}", *v.Name, v.Type)
135 | }
136 |
137 | type Interface struct {
138 | IsExported bool `json:"is_exported,omitempty"`
139 | Embed bool `json:"embed,omitempty"`
140 | Name string `json:"name,omitempty"`
141 | Doc Comment `json:"doc,omitempty"`
142 | Comment Comment `json:"comment,omitempty"`
143 | MethodSet []InterfaceField `json:"method_set,omitempty"`
144 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
145 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
146 | }
147 |
148 | type InterfaceField interface{}
149 |
150 | type Import struct {
151 | Name string `json:"name,omitempty"`
152 | Path string `json:"path,omitempty"`
153 | Doc Comment `json:"doc,omitempty"`
154 | Comment Comment `json:"comment,omitempty"`
155 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
156 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
157 | }
158 |
159 | type MagicComment struct {
160 | Pragma string `json:"pragma,omitempty"` // noinline
161 | Raw string `json:"raw,omitempty"` // go:noinline
162 | }
163 |
164 | type GenerateComment struct {
165 | Command string `json:"command,omitempty"` // goyacc -o gopher.go -p parser gopher.y
166 | Raw string `json:"raw,omitempty"` // go:generate goyacc -o gopher.go -p parser gopher.y
167 | }
168 |
169 | func (g GenerateComment) Cmd() (*exec.Cmd, error) {
170 | cmd := strings.Split(g.Command, " ")
171 | if len(cmd) == 0 {
172 | return nil, fmt.Errorf("cmd error, not enough args: comment = %s", g.Raw)
173 | }
174 |
175 | return exec.Command(cmd[0], cmd[1:]...), nil
176 | }
177 |
178 | type Comment struct {
179 | Content string `json:"content,omitempty"`
180 | }
181 |
182 | // Lines converts any comment into a slice of strings based on their logical
183 | // line-based grouping.
184 | func (c Comment) Lines() []string {
185 | const pref = "/*"
186 | const suff = "*/"
187 | const basic = "// "
188 |
189 | com := c.Content
190 | switch {
191 | // check if multi-line /* ... */ style comment
192 | case strings.HasPrefix(com, pref) && strings.HasSuffix(com, suff):
193 | com = strings.Trim(com, pref+suff)
194 | com = strings.Replace(com, "\n\n", "\n", -1)
195 | com = strings.TrimPrefix(com, "\n")
196 | com = strings.TrimSuffix(com, "\n")
197 | return strings.Split(com, "\n")
198 |
199 | // check if basic comment, prefixed with //(+space)
200 | case strings.HasPrefix(com, basic):
201 | return strings.Split(com, basic)
202 | }
203 |
204 | return nil
205 | }
206 |
207 | type Const struct {
208 | IsExported bool `json:"is_exported,omitempty"`
209 | Name string `json:"name,omitempty"`
210 | Type string `json:"type,omitempty"`
211 | Value interface{} `json:"value,omitempty"`
212 | Doc Comment `json:"doc,omitempty"`
213 | Comment Comment `json:"comment,omitempty"`
214 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
215 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
216 | }
217 |
218 | type Var struct {
219 | IsExported bool `json:"is_exported,omitempty"`
220 | Name string `json:"name,omitempty"`
221 | Type string `json:"type,omitempty"`
222 | Value interface{} `json:"value,omitempty"`
223 | Doc Comment `json:"doc,omitempty"`
224 | Comment Comment `json:"comment,omitempty"`
225 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
226 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
227 | }
228 |
229 | // Constraint holds the options of a build tag.
230 | // +build linux,386 darwin,!cgo
231 | // |-------| |---------|
232 | // option option
233 | type Constraint struct {
234 | Options []string `json:"options,omitempty"`
235 | }
236 |
237 | func (c Constraint) String() string {
238 | return fmt.Sprintf("%s %s", buildPrefix, strings.Join(c.Options, " "))
239 | }
240 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Fanatics/toast
2 |
3 | require (
4 | github.com/tidwall/gjson v1.1.3 // indirect
5 | github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 // indirect
6 | github.com/tidwall/sjson v1.0.2
7 | )
8 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=
2 | github.com/tidwall/gjson v1.1.3/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
3 | github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1 h1:pWIN9LOlFRCJFqWIOEbHLvY0WWJddsjH2FQ6N0HKZdU=
4 | github.com/tidwall/match v0.0.0-20171002075945-1731857f09b1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
5 | github.com/tidwall/sjson v1.0.2 h1:WHiiu9LsxPZazjIUPC1EGBuUqQVWJksZszl9BasNNjg=
6 | github.com/tidwall/sjson v1.0.2/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y=
7 |
--------------------------------------------------------------------------------
/plugin-samples/toast-plugin/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strings"
7 |
8 | "github.com/Fanatics/toast/collector"
9 | "github.com/Fanatics/toast/plugin"
10 | )
11 |
12 | func main() {
13 | plugin.New("toast-plugin").Init(func(data *collector.Data) error {
14 | var files []string
15 | for _, pkg := range data.Packages {
16 | for _, file := range pkg.Files {
17 | /*
18 | within each package is a set of files. a collector.File is:
19 |
20 | type File struct {
21 | Name string `json:"name,omitempty"`
22 | Package string `json:"package,omitempty"`
23 | Imports []Import `json:"imports,omitempty"`
24 | TypeDefs []TypeDefinition `json:"type_defs,omitempty"`
25 | Structs []Struct `json:"structs,omitempty"`
26 | Interfaces []Interface `json:"interfaces,omitempty"`
27 | Funcs []Func `json:"funcs,omitempty"`
28 | Consts []Const `json:"consts,omitempty"`
29 | Vars []Var `json:"vars,omitempty"`
30 | Comments []Comment `json:"comments,omitempty"`
31 | MagicComments []MagicComment `json:"magic_comments,omitempty"`
32 | GenerateComments []GenerateComment `json:"generate_comments,omitempty"`
33 | BuildTags []Constraint `json:"build_tags,omitempty"`
34 | }
35 |
36 | using those fields, you can generate code based on the Go code
37 | which was parsed to a simplified AST.
38 | */
39 |
40 | // accumulate the file names, as a basic example
41 | files = append(files, file.Name)
42 | }
43 | }
44 |
45 | f, err := os.Create(filepath.Join(data.OutputBase, "my-file.txt"))
46 | if err != nil {
47 | return err
48 | }
49 | defer f.Close()
50 |
51 | _, err = f.Write([]byte(
52 | strings.Join(files, "\n"),
53 | ))
54 | if err != nil {
55 | return err
56 | }
57 |
58 | return nil
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/plugin/init.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "os"
9 |
10 | "github.com/Fanatics/toast/collector"
11 | )
12 |
13 | // Func is a function which defines plugin behavior, and is provided a
14 | // pointer to collector.Data.
15 | type Func func(d *collector.Data) error
16 |
17 | type Plugin struct {
18 | name string
19 | }
20 |
21 | // New returns a Plugin instance for a Plugin to be initialized.
22 | func New(name string) *Plugin {
23 | return &Plugin{
24 | name: name,
25 | }
26 | }
27 |
28 | // Init is called by Plugin code and is provided a PluginFunc from the caller
29 | // to handle the input Data (read from stdin).
30 | func (p *Plugin) Init(fn Func) {
31 | // read from stdin to get serialized bytes
32 | input := &bytes.Buffer{}
33 | _, err := io.Copy(input, os.Stdin)
34 | if err != nil {
35 | p.wrapErrAndLog(err)
36 | return
37 | }
38 |
39 | // deserialize bytes into *collector.Data
40 | inputData := &collector.Data{}
41 | err = json.Unmarshal(input.Bytes(), inputData)
42 | if err != nil {
43 | p.wrapErrAndLog(err)
44 | return
45 | }
46 |
47 | // execute "fn" and pass it the *collector.Data, where the Plugin would use
48 | // the simplified AST to generate other code.
49 | p.wrapErrAndLog(fn(inputData))
50 | }
51 |
52 | func (p *Plugin) wrapErrAndLog(err error) {
53 | if err != nil {
54 | fmt.Fprintf(os.Stdout, "[toast:plugin] %s: %v\n", p.name, err)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/plugin/util.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "bytes"
5 | "go/format"
6 | htmltmpl "html/template"
7 | "io"
8 | "path"
9 | "text/template"
10 | )
11 |
12 | // Gofmt formats Go source code.
13 | func Gofmt(src []byte) ([]byte, error) {
14 | return format.Source(src)
15 | }
16 |
17 | // GofmtReadWriter formats an io.ReadWriter, such as an *os.File.
18 | func GofmtReadWriter(rw io.ReadWriter) (io.ReadWriter, error) {
19 | buf := bytes.Buffer{}
20 | _, err := io.Copy(&buf, rw)
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | fmtd, err := Gofmt(buf.Bytes())
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | return bytes.NewBuffer(fmtd), nil
31 | }
32 |
33 | // OutputTemplate executes a text template using the provided data and writes it
34 | // to the destination io.Writer.
35 | func (p *Plugin) OutputTemplate(dst io.Writer, templatePath string, data interface{}) error {
36 | return template.Must(
37 | template.New(path.Base(templatePath)).ParseFiles(templatePath),
38 | ).Execute(dst, data)
39 | }
40 |
41 | // OutputTemplateHTML executes an HTML template using the provided data and
42 | // writes it to the destination io.Writer.
43 | func (p *Plugin) OutputTemplateHTML(dst io.Writer, templatePath string, data interface{}) error {
44 | return htmltmpl.Must(
45 | htmltmpl.New(path.Base(templatePath)).ParseFiles(templatePath),
46 | ).Execute(dst, data)
47 | }
48 |
--------------------------------------------------------------------------------
/test/base/data.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | type Data struct {
4 | SchemaVersion string
5 | *AuditLog
6 | }
7 |
8 | func (d Data) Export() bool { return true }
9 |
10 | func (d Data) Namespace() string { return "com.fanatics.amdm" }
11 |
12 | func (d Data) Validate() error { return nil }
13 |
14 | type AuditLog struct {
15 | CreatedAt int64 // unix nano
16 | UpdatedAt int64 // unix nano
17 | CreatedBy string
18 | UpdatedBy string
19 | }
20 |
21 | type EmbedMe interface {
22 | // Internal is a method from the embedded interface
23 | Internal(string) error
24 | }
25 |
--------------------------------------------------------------------------------
/test/item.go:
--------------------------------------------------------------------------------
1 | // +build darwin linux,386 windows,!cgo
2 |
3 | //go:generate ./scripts/test.sh -f
4 |
5 | // Package types is an example package
6 | package types
7 |
8 | import "github.com/Fanatics/toast/test/base"
9 |
10 | var (
11 | // doc for simpleValue
12 | //go:generate do_something -else -helpful
13 | simpleValue = "variableValue" // comment for simpleValue
14 | numeric = 1233.99
15 | t = thing{}
16 | known10Array = [10]int{}
17 | unknownArray = [...]string{}
18 | unary = &thing{Name: "steve"}
19 | star *thing
20 | binary = 1 | 1 ^ 1&1&2
21 | function = func() string { return "a string is returned" }
22 | aSimpleMap = map[string]int{
23 | "one": 1,
24 | "two": 2,
25 | }
26 | aComplexMap = map[*base.Data]*Item{}
27 | )
28 |
29 | // IntFunc is an int func
30 | func (m *MyInt) IntFunc() {
31 | }
32 |
33 | // OtherIntFunc is a func on MyInt
34 | //go:generate something forOtherIntFunc
35 | //go:noinline
36 | func (m MyInt) OtherIntFunc() {
37 | }
38 |
39 | type AnotherType string
40 |
41 | // MyInt is a type that is an int
42 | //go:generate something forMyIntType
43 | type MyInt int
44 |
45 | type thing struct {
46 | Name string
47 | }
48 |
49 | const (
50 | simpleConstant = "constantValue"
51 | numericConstant = 42
52 | // ExportedConstant doc string
53 | //go:noescape
54 | ExportedConstant = "EXPORTED" // comment about ExportedConstant
55 | )
56 |
57 | // Item is documented here and will grab other non-magic and non-generate
58 | // comments as well.
59 | //
60 | // @decl:export --formats=json,csv --providers=s3
61 | //go:generate make_item # bad example
62 | //go:noinline
63 | type Item struct {
64 | // field ignore -f
65 | base.Data
66 | ItemID int32 `json:"item_id"`
67 | ItemName string `json:"item_name"`
68 | Size string `json:"size"`
69 | // Dimensions is a field on an Item
70 | //go:generate dimensions 12,34,55
71 | // another comment on dimensions
72 | Dimensions []string `json:"dimensions"` // dimensions side comment
73 | Weight *float32 `json:"weight"`
74 | Color string `json:"color"`
75 | CountryOfOrigin string `json:"country_of_origin"`
76 | Cost int64 `json:"cost"`
77 | BasePrice int64 `json:"base_price"`
78 | DynamicOptions []map[string]map[string]interface{}
79 | DoneChan <-chan bool
80 | SimpleChan chan string
81 | SimpleMap map[string]interface{}
82 | } // item "Comment"
83 |
84 | /*
85 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque euismod, mi vulputate imperdiet viverra, erat massa rutrum purus, quis hendrerit justo diam non ligula. Quisque fermentum tortor ac dui fringilla feugiat. Nam ultrices euismod viverra. Nullam et sem ut lacus facilisis tincidunt. Suspendisse eget ante at nibh congue placerat sed a metus. Aenean scelerisque ut dui sed posuere. Aenean in risus ipsum. Vivamus cursus ultrices massa ut cursus. Vestibulum sem erat, elementum in varius vitae, sagittis et elit. Donec a consectetur massa, vel posuere sapien. Phasellus accumsan tortor velit, non gravida sapien vulputate at. Nunc tempus, massa nec sagittis euismod, diam nunc commodo nulla, at vestibulum magna magna ut erat. Donec suscipit dictum est euismod placerat. Morbi at pulvinar ante. Ut feugiat diam et neque interdum sodales.
86 |
87 | Aliquam erat volutpat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin posuere convallis sapien, eget condimentum eros vestibulum et. Donec tristique purus eget ligula aliquam dictum. Nullam vulputate tincidunt ultrices. Integer vel porttitor velit. Nulla non tortor rutrum, placerat ligula eget, commodo nibh. Vivamus luctus suscipit nunc, faucibus lacinia arcu vulputate quis. Aliquam non urna id enim ullamcorper elementum ac ac nunc. Curabitur velit nibh, vulputate in orci sagittis, aliquet laoreet ex. Morbi commodo, arcu in varius viverra, odio arcu finibus ligula, vel aliquet metus nulla non ligula. Duis cursus eleifend mauris, quis volutpat nunc viverra in. In ornare tellus elit, bibendum fringilla magna blandit non. Morbi elementum lacinia mi sit amet mollis.
88 |
89 | Etiam suscipit lacus at nisl facilisis, quis sagittis leo elementum. Aliquam erat volutpat. Nulla malesuada, ex quis pharetra egestas, ante erat malesuada nisl, nec viverra odio tellus eu dui. In id porttitor massa. Duis luctus justo id magna maximus dapibus molestie ac mi. Ut consequat varius metus non gravida. Duis eu dignissim ipsum. Suspendisse at urna id sem lobortis varius non sed enim. Aliquam non tincidunt nulla, non pretium erat.
90 | */
91 |
92 | // This is a multiline comment as well, and the lines are logically collected
93 | // as the whole group of comment lines without any new line breaks.
94 | // That is pretty convenient!
95 |
96 | // so alone :(
97 |
98 | // These
99 | //
100 | // are
101 | //
102 | // connected
103 | //
104 | //
105 | // together!
106 |
107 | // RPCItem is an interface, and this is a doc comment.
108 | type RPCItem interface {
109 | // ABOVE GetItem
110 | GetItem([]int64) ([]Item, error) // ASIDE GetItem
111 | CreateItem([]Item) ([]Item, error)
112 | UpdateItem([]Item) error
113 | DeleteItem([]Item) error
114 | base.EmbedMe
115 | RPCEmbed
116 | }
117 |
118 | type RPCEmbed interface {
119 | Embedded(int) error
120 | }
121 |
122 | // just a comment
123 |
124 | // Export would implement an interface, and by returning false, we indicate that
125 | // the override kicks in to prevent Items from being exported to S3, etc.
126 | func (i *Item) Export() bool { return false }
127 |
128 | // Try here is the documenation.
129 | // this is a comment above the func
130 | //go:generate something
131 | //go:noinline
132 | func Try(name string, id int64) error {
133 | // this is a comment inside the func
134 | return nil
135 | }
136 |
--------------------------------------------------------------------------------