github.com/blp1526/goa@v1.4.0/goagen/gen_main/generator.go (about) 1 package genmain 2 3 import ( 4 "bufio" 5 "flag" 6 "fmt" 7 "go/ast" 8 "go/parser" 9 "go/token" 10 "net" 11 "os" 12 "path" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "text/template" 17 18 "github.com/goadesign/goa/design" 19 "github.com/goadesign/goa/goagen/codegen" 20 "github.com/goadesign/goa/goagen/utils" 21 ) 22 23 //NewGenerator returns an initialized instance of a JavaScript Client Generator 24 func NewGenerator(options ...Option) *Generator { 25 g := &Generator{OutDir: "."} 26 27 for _, option := range options { 28 option(g) 29 } 30 31 return g 32 } 33 34 // Generator is the application code generator. 35 type Generator struct { 36 API *design.APIDefinition // The API definition 37 OutDir string // Path to output directory 38 DesignPkg string // Path to design package, only used to mark generated files. 39 Target string // Name of generated "app" package 40 Force bool // Whether to override existing files 41 Regen bool // Whether to regenerate scaffolding in place, maintaining controller implementation 42 genfiles []string // Generated files 43 } 44 45 // Generate is the generator entry point called by the meta generator. 46 func Generate() (files []string, err error) { 47 var ( 48 outDir, toolDir, designPkg, target, ver string 49 force, notool, regen bool 50 ) 51 52 set := flag.NewFlagSet("main", flag.PanicOnError) 53 set.StringVar(&outDir, "out", "", "") 54 set.StringVar(&designPkg, "design", "", "") 55 set.StringVar(&target, "pkg", "app", "") 56 set.StringVar(&ver, "version", "", "") 57 set.StringVar(&toolDir, "tooldir", "tool", "") 58 set.BoolVar(¬ool, "notool", false, "") 59 set.BoolVar(&force, "force", false, "") 60 set.BoolVar(®en, "regen", false, "") 61 set.Bool("notest", false, "") 62 set.Parse(os.Args[1:]) 63 64 if err := codegen.CheckVersion(ver); err != nil { 65 return nil, err 66 } 67 68 target = codegen.Goify(target, false) 69 g := &Generator{OutDir: outDir, DesignPkg: designPkg, Target: target, Force: force, Regen: regen, API: design.Design} 70 71 return g.Generate() 72 } 73 74 func extractControllerBody(filename string) (map[string]string, []*ast.ImportSpec, error) { 75 // First check if a file is there. If not, return empty results to let generation proceed. 76 if _, e := os.Stat(filename); e != nil { 77 return map[string]string{}, []*ast.ImportSpec{}, nil 78 } 79 fset := token.NewFileSet() 80 pfile, err := parser.ParseFile(fset, filename, nil, parser.ImportsOnly) 81 if err != nil { 82 return nil, nil, err 83 } 84 f, err := os.Open(filename) 85 if err != nil { 86 return nil, nil, err 87 } 88 defer f.Close() 89 var ( 90 inBlock bool 91 block []string 92 ) 93 actionImpls := map[string]string{} 94 scanner := bufio.NewScanner(f) 95 for scanner.Scan() { 96 line := scanner.Text() 97 match := linePattern.FindStringSubmatch(line) 98 if len(match) == 3 { 99 switch match[2] { 100 case "start": 101 inBlock = true 102 case "end": 103 inBlock = false 104 actionImpls[match[1]] = strings.Join(block, "\n") 105 block = []string{} 106 } 107 continue 108 } 109 if inBlock { 110 block = append(block, line) 111 } 112 } 113 if err := scanner.Err(); err != nil { 114 return nil, nil, err 115 } 116 return actionImpls, pfile.Imports, nil 117 } 118 119 // GenerateController generates the controller corresponding to the given 120 // resource and returns the generated filename. 121 func GenerateController(force, regen bool, appPkg, outDir, pkg, name string, r *design.ResourceDefinition) (filename string, err error) { 122 filename = filepath.Join(outDir, codegen.SnakeCase(name)+".go") 123 var ( 124 actionImpls map[string]string 125 extractedImports []*ast.ImportSpec 126 ) 127 if regen { 128 actionImpls, extractedImports, err = extractControllerBody(filename) 129 if err != nil { 130 return "", err 131 } 132 os.Remove(filename) 133 } 134 if force { 135 os.Remove(filename) 136 } 137 if _, e := os.Stat(filename); e == nil { 138 return "", nil 139 } 140 if err = os.MkdirAll(outDir, 0755); err != nil { 141 return "", err 142 } 143 144 var file *codegen.SourceFile 145 file, err = codegen.SourceFileFor(filename) 146 if err != nil { 147 return "", err 148 } 149 defer func() { 150 file.Close() 151 if err == nil { 152 err = file.FormatCode() 153 } 154 }() 155 156 elems := strings.Split(appPkg, "/") 157 pkgName := elems[len(elems)-1] 158 var imp string 159 if _, err := codegen.PackageSourcePath(appPkg); err == nil { 160 imp = appPkg 161 } else { 162 imp, err = codegen.PackagePath(outDir) 163 if err != nil { 164 return "", err 165 } 166 imp = path.Join(filepath.ToSlash(imp), appPkg) 167 } 168 169 imports := []*codegen.ImportSpec{ 170 codegen.SimpleImport("io"), 171 codegen.SimpleImport("github.com/goadesign/goa"), 172 codegen.SimpleImport(imp), 173 codegen.SimpleImport("golang.org/x/net/websocket"), 174 } 175 for _, imp := range extractedImports { 176 // This may introduce duplicate imports of the defaults, but 177 // that'll get worked out by Format later. 178 var cgimp *codegen.ImportSpec 179 path := strings.Trim(imp.Path.Value, `"`) 180 if imp.Name != nil { 181 cgimp = codegen.NewImport(imp.Name.Name, path) 182 } else { 183 cgimp = codegen.SimpleImport(path) 184 } 185 imports = append(imports, cgimp) 186 } 187 188 funcs := funcMap(pkgName, actionImpls) 189 if err = file.WriteHeader("", pkg, imports); err != nil { 190 return "", err 191 } 192 if err = file.ExecuteTemplate("controller", ctrlT, funcs, r); err != nil { 193 return "", err 194 } 195 err = r.IterateActions(func(a *design.ActionDefinition) error { 196 if a.WebSocket() { 197 return file.ExecuteTemplate("actionWS", actionWST, funcs, a) 198 } 199 return file.ExecuteTemplate("action", actionT, funcs, a) 200 }) 201 if err != nil { 202 return "", err 203 } 204 return 205 } 206 207 // Generate produces the skeleton main. 208 func (g *Generator) Generate() (_ []string, err error) { 209 if g.API == nil { 210 return nil, fmt.Errorf("missing API definition, make sure design is properly initialized") 211 } 212 213 go utils.Catch(nil, func() { g.Cleanup() }) 214 215 defer func() { 216 if err != nil { 217 g.Cleanup() 218 } 219 }() 220 221 if g.Target == "" { 222 g.Target = "app" 223 } 224 225 codegen.Reserved[g.Target] = true 226 227 mainFile := filepath.Join(g.OutDir, "main.go") 228 if g.Force { 229 os.Remove(mainFile) 230 } 231 _, err = os.Stat(mainFile) 232 if err != nil { 233 // ensure that the output directory exists before creating a new main 234 if err = os.MkdirAll(g.OutDir, 0755); err != nil { 235 return nil, err 236 } 237 if err = g.createMainFile(mainFile, funcMap(g.Target, nil)); err != nil { 238 return nil, err 239 } 240 } 241 242 err = g.API.IterateResources(func(r *design.ResourceDefinition) error { 243 filename, err := GenerateController(g.Force, g.Regen, g.Target, g.OutDir, "main", r.Name, r) 244 if err != nil { 245 return err 246 } 247 248 g.genfiles = append(g.genfiles, filename) 249 return nil 250 }) 251 if err != nil { 252 return 253 } 254 255 return g.genfiles, nil 256 } 257 258 // Cleanup removes all the files generated by this generator during the last invokation of Generate. 259 func (g *Generator) Cleanup() { 260 for _, f := range g.genfiles { 261 os.Remove(f) 262 } 263 g.genfiles = nil 264 } 265 266 func (g *Generator) createMainFile(mainFile string, funcs template.FuncMap) (err error) { 267 var file *codegen.SourceFile 268 file, err = codegen.SourceFileFor(mainFile) 269 if err != nil { 270 return err 271 } 272 defer func() { 273 file.Close() 274 if err == nil { 275 err = file.FormatCode() 276 } 277 }() 278 g.genfiles = append(g.genfiles, mainFile) 279 funcs["getPort"] = func(hostport string) string { 280 _, port, err := net.SplitHostPort(hostport) 281 if err != nil { 282 return "8080" 283 } 284 return port 285 } 286 outPkg, err := codegen.PackagePath(g.OutDir) 287 if err != nil { 288 return err 289 } 290 appPkg := path.Join(outPkg, "app") 291 imports := []*codegen.ImportSpec{ 292 codegen.SimpleImport("time"), 293 codegen.SimpleImport("github.com/goadesign/goa"), 294 codegen.SimpleImport("github.com/goadesign/goa/middleware"), 295 codegen.SimpleImport(appPkg), 296 } 297 file.Write([]byte("//go:generate goagen bootstrap -d " + g.DesignPkg + "\n\n")) 298 if err = file.WriteHeader("", "main", imports); err != nil { 299 return err 300 } 301 data := map[string]interface{}{ 302 "Name": g.API.Name, 303 "API": g.API, 304 } 305 err = file.ExecuteTemplate("main", mainT, funcs, data) 306 return 307 } 308 309 // tempCount is the counter used to create unique temporary variable names. 310 var tempCount int 311 312 // tempvar generates a unique temp var name. 313 func tempvar() string { 314 tempCount++ 315 if tempCount == 1 { 316 return "c" 317 } 318 return fmt.Sprintf("c%d", tempCount) 319 } 320 321 func okResp(a *design.ActionDefinition, appPkg string) map[string]interface{} { 322 var ok *design.ResponseDefinition 323 for _, resp := range a.Responses { 324 if resp.Status == 200 { 325 ok = resp 326 break 327 } 328 } 329 if ok == nil { 330 return nil 331 } 332 var mt *design.MediaTypeDefinition 333 var ok2 bool 334 if mt, ok2 = design.Design.MediaTypes[design.CanonicalIdentifier(ok.MediaType)]; !ok2 { 335 return nil 336 } 337 view := ok.ViewName 338 if view == "" { 339 view = design.DefaultView 340 } 341 pmt, _, err := mt.Project(view) 342 if err != nil { 343 return nil 344 } 345 var typeref string 346 if pmt.IsError() { 347 typeref = `goa.ErrInternal("not implemented")` 348 } else { 349 name := codegen.GoTypeRef(pmt, pmt.AllRequired(), 1, false) 350 var pointer string 351 if strings.HasPrefix(name, "*") { 352 name = name[1:] 353 pointer = "*" 354 } 355 typeref = fmt.Sprintf("%s%s.%s", pointer, appPkg, name) 356 if strings.HasPrefix(typeref, "*") { 357 typeref = "&" + typeref[1:] 358 } 359 typeref += "{}" 360 } 361 var nameSuffix string 362 if view != "default" { 363 nameSuffix = codegen.Goify(view, true) 364 } 365 return map[string]interface{}{ 366 "Name": ok.Name + nameSuffix, 367 "GoType": codegen.GoNativeType(pmt), 368 "TypeRef": typeref, 369 } 370 } 371 372 // funcMap creates the funcMap used to render the controller code. 373 func funcMap(appPkg string, actionImpls map[string]string) template.FuncMap { 374 return template.FuncMap{ 375 "tempvar": tempvar, 376 "okResp": okResp, 377 "targetPkg": func() string { return appPkg }, 378 "actionBody": func(name string) string { 379 body, ok := actionImpls[name] 380 if !ok { 381 return defaultActionBody 382 } 383 return body 384 }, 385 "printResp": func(name string) bool { 386 _, ok := actionImpls[name] 387 return !ok 388 }, 389 } 390 } 391 392 var linePattern = regexp.MustCompile(`^\s*// ([^:]+): (\w+)_implement\s*$`) 393 394 const defaultActionBody = `// Put your logic here` 395 396 const ctrlT = `// {{ $ctrlName := printf "%s%s" (goify .Name true) "Controller" }}{{ $ctrlName }} implements the {{ .Name }} resource. 397 type {{ $ctrlName }} struct { 398 *goa.Controller 399 } 400 401 // New{{ $ctrlName }} creates a {{ .Name }} controller. 402 func New{{ $ctrlName }}(service *goa.Service) *{{ $ctrlName }} { 403 return &{{ $ctrlName }}{Controller: service.NewController("{{ $ctrlName }}")} 404 } 405 ` 406 407 const actionT = ` 408 {{- $ctrlName := printf "%s%s" (goify .Parent.Name true) "Controller" -}} 409 {{- $actionDescr := printf "%s_%s" $ctrlName (goify .Name true) -}} 410 // {{ goify .Name true }} runs the {{ .Name }} action. 411 func (c *{{ $ctrlName }}) {{ goify .Name true }}(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) error { 412 // {{ $actionDescr }}: start_implement 413 414 {{ actionBody $actionDescr }} 415 416 {{ if printResp $actionDescr }} 417 {{ $ok := okResp . targetPkg }}{{ if $ok }} res := {{ $ok.TypeRef }} 418 {{ end }} return {{ if $ok }}ctx.{{ $ok.Name }}(res){{ else }}nil{{ end }} 419 {{ end }} // {{ $actionDescr }}: end_implement 420 } 421 ` 422 423 const actionWST = ` 424 {{- $ctrlName := printf "%s%s" (goify .Parent.Name true) "Controller" -}} 425 {{- $actionDescr := printf "%s_%s" $ctrlName (goify .Name true) -}} 426 // {{ goify .Name true }} runs the {{ .Name }} action. 427 func (c *{{ $ctrlName }}) {{ goify .Name true }}(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) error { 428 c.{{ goify .Name true }}WSHandler(ctx).ServeHTTP(ctx.ResponseWriter, ctx.Request) 429 return nil 430 } 431 432 // {{ goify .Name true }}WSHandler establishes a websocket connection to run the {{ .Name }} action. 433 func (c *{{ $ctrlName }}) {{ goify .Name true }}WSHandler(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) websocket.Handler { 434 return func(ws *websocket.Conn) { 435 // {{ $actionDescr }}: start_implement 436 437 {{ actionBody $actionDescr }} 438 {{ if printResp $actionDescr }} 439 ws.Write([]byte("{{ .Name }} {{ .Parent.Name }}")) 440 // Dummy echo websocket server 441 io.Copy(ws, ws) 442 {{ end }} // {{ $actionDescr }}: end_implement 443 } 444 }` 445 446 const mainT = ` 447 func main() { 448 // Create service 449 service := goa.New({{ printf "%q" .Name }}) 450 451 // Mount middleware 452 service.Use(middleware.RequestID()) 453 service.Use(middleware.LogRequest(true)) 454 service.Use(middleware.ErrorHandler(service, true)) 455 service.Use(middleware.Recover()) 456 {{ $api := .API }} 457 {{ range $name, $res := $api.Resources }}{{ $name := goify $res.Name true }} // Mount "{{$res.Name}}" controller 458 {{ $tmp := tempvar }}{{ $tmp }} := New{{ $name }}Controller(service) 459 {{ targetPkg }}.Mount{{ $name }}Controller(service, {{ $tmp }}) 460 {{ end }} 461 462 // Start service 463 if err := service.ListenAndServe(":{{ getPort .API.Host }}"); err != nil { 464 service.LogError("startup", "err", err) 465 } 466 } 467 `