├── .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 | ![toast logo](.github/toast.logo.png) 2 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](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 | --------------------------------------------------------------------------------