github.com/shogo82148/goa-v1@v1.6.2/goagen/gen_app/generator.go (about) 1 package genapp 2 3 import ( 4 "flag" 5 "fmt" 6 "os" 7 "path/filepath" 8 "sort" 9 10 "github.com/shogo82148/goa-v1/design" 11 "github.com/shogo82148/goa-v1/goagen/codegen" 12 "github.com/shogo82148/goa-v1/goagen/utils" 13 ) 14 15 //NewGenerator returns an initialized instance of an Application Generator 16 func NewGenerator(options ...Option) *Generator { 17 g := &Generator{} 18 g.validator = codegen.NewValidator() 19 20 for _, option := range options { 21 option(g) 22 } 23 24 return g 25 } 26 27 // Generator is the application code generator. 28 type Generator struct { 29 API *design.APIDefinition // The API definition 30 OutDir string // Path to output directory 31 Target string // Name of generated package 32 NoTest bool // Whether to skip test generation 33 genfiles []string // Generated files 34 validator *codegen.Validator // Validation code generator 35 } 36 37 // Generate is the generator entry point called by the meta generator. 38 func Generate() (files []string, err error) { 39 var ( 40 outDir, toolDir, target, ver string 41 notest, notool, regen bool 42 ) 43 44 set := flag.NewFlagSet("app", flag.PanicOnError) 45 set.String("design", "", "") 46 set.StringVar(&outDir, "out", "", "") 47 set.StringVar(&target, "pkg", "app", "") 48 set.StringVar(&ver, "version", "", "") 49 set.StringVar(&toolDir, "tooldir", "tool", "") 50 set.BoolVar(¬est, "notest", false, "") 51 set.BoolVar(¬ool, "notool", false, "") 52 set.BoolVar(®en, "regen", false, "") 53 set.Bool("force", false, "") 54 set.Parse(os.Args[1:]) 55 outDir = filepath.Join(outDir, target) 56 57 if err := codegen.CheckVersion(ver); err != nil { 58 return nil, err 59 } 60 61 target = codegen.Goify(target, false) 62 g := &Generator{OutDir: outDir, Target: target, NoTest: notest, API: design.Design, validator: codegen.NewValidator()} 63 64 return g.Generate() 65 } 66 67 // Generate the application code, implement codegen.Generator. 68 func (g *Generator) Generate() (_ []string, err error) { 69 if g.API == nil { 70 return nil, fmt.Errorf("missing API definition, make sure design is properly initialized") 71 } 72 73 go utils.Catch(nil, func() { g.Cleanup() }) 74 75 defer func() { 76 if err != nil { 77 g.Cleanup() 78 } 79 }() 80 81 codegen.Reserved[g.Target] = true 82 83 os.RemoveAll(g.OutDir) 84 85 if err := os.MkdirAll(g.OutDir, 0755); err != nil { 86 return nil, err 87 } 88 g.genfiles = []string{g.OutDir} 89 if err := g.generateContexts(); err != nil { 90 return nil, err 91 } 92 if err := g.generateControllers(); err != nil { 93 return nil, err 94 } 95 if err := g.generateSecurity(); err != nil { 96 return nil, err 97 } 98 if err := g.generateHrefs(); err != nil { 99 return nil, err 100 } 101 if err := g.generateMediaTypes(); err != nil { 102 return nil, err 103 } 104 if err := g.generateUserTypes(); err != nil { 105 return nil, err 106 } 107 if !g.NoTest { 108 if err := g.generateResourceTest(); err != nil { 109 return nil, err 110 } 111 } 112 113 return g.genfiles, nil 114 } 115 116 // Cleanup removes the entire "app" directory if it was created by this generator. 117 func (g *Generator) Cleanup() { 118 if len(g.genfiles) == 0 { 119 return 120 } 121 os.RemoveAll(g.OutDir) 122 g.genfiles = nil 123 } 124 125 // generateContexts iterates through the API resources and actions and generates the action 126 // contexts. 127 func (g *Generator) generateContexts() (err error) { 128 var ( 129 ctxFile string 130 ctxWr *ContextsWriter 131 ) 132 { 133 ctxFile = filepath.Join(g.OutDir, "contexts.go") 134 ctxWr, err = NewContextsWriter(ctxFile) 135 if err != nil { 136 return 137 } 138 } 139 defer func() { 140 ctxWr.Close() 141 if err == nil { 142 err = ctxWr.FormatCode() 143 } 144 }() 145 title := fmt.Sprintf("%s: Application Contexts", g.API.Context()) 146 imports := []*codegen.ImportSpec{ 147 codegen.SimpleImport("fmt"), 148 codegen.SimpleImport("net/http"), 149 codegen.SimpleImport("strconv"), 150 codegen.SimpleImport("strings"), 151 codegen.SimpleImport("time"), 152 codegen.SimpleImport("unicode/utf8"), 153 codegen.NewImport("goa", "github.com/shogo82148/goa-v1"), 154 codegen.NewImport("uuid", "github.com/gofrs/uuid"), 155 codegen.SimpleImport("context"), 156 } 157 g.API.IterateResources(func(r *design.ResourceDefinition) error { 158 return r.IterateActions(func(a *design.ActionDefinition) error { 159 if a.Payload != nil { 160 imports = codegen.AttributeImports(a.Payload.AttributeDefinition, imports, nil) 161 } 162 return nil 163 }) 164 }) 165 166 g.genfiles = append(g.genfiles, ctxFile) 167 if err = ctxWr.WriteHeader(title, g.Target, imports); err != nil { 168 return 169 } 170 err = g.API.IterateResources(func(r *design.ResourceDefinition) error { 171 return r.IterateActions(func(a *design.ActionDefinition) error { 172 ctxName := codegen.Goify(a.Name, true) + codegen.Goify(a.Parent.Name, true) + "Context" 173 headers := &design.AttributeDefinition{ 174 Type: design.Object{}, 175 } 176 if r.Headers != nil { 177 headers.Merge(r.Headers) 178 headers.Validation = r.Headers.Validation 179 } 180 if a.Headers != nil { 181 headers.Merge(a.Headers) 182 headers.Validation = a.Headers.Validation 183 } 184 if headers != nil && len(headers.Type.ToObject()) == 0 { 185 headers = nil // So that {{if .Headers}} returns false in templates 186 } 187 params := a.AllParams() 188 if params != nil && len(params.Type.ToObject()) == 0 { 189 params = nil // So that {{if .Params}} returns false in templates 190 } 191 192 non101 := make(map[string]*design.ResponseDefinition) 193 for k, v := range a.Responses { 194 if v.Status != 101 { 195 non101[k] = v 196 } 197 } 198 ctxData := ContextTemplateData{ 199 Name: ctxName, 200 ResourceName: r.Name, 201 ActionName: a.Name, 202 Payload: a.Payload, 203 Params: params, 204 Headers: headers, 205 Routes: a.Routes, 206 Responses: non101, 207 API: g.API, 208 DefaultPkg: g.Target, 209 Security: a.Security, 210 } 211 return ctxWr.Execute(&ctxData) 212 }) 213 }) 214 return 215 } 216 217 // generateControllers iterates through the API resources and generates the low level 218 // controllers. 219 func (g *Generator) generateControllers() (err error) { 220 var ( 221 ctlFile string 222 ctlWr *ControllersWriter 223 ) 224 { 225 ctlFile = filepath.Join(g.OutDir, "controllers.go") 226 ctlWr, err = NewControllersWriter(ctlFile) 227 if err != nil { 228 return 229 } 230 } 231 defer func() { 232 ctlWr.Close() 233 if err == nil { 234 err = ctlWr.FormatCode() 235 } 236 }() 237 title := fmt.Sprintf("%s: Application Controllers", g.API.Context()) 238 imports := []*codegen.ImportSpec{ 239 codegen.SimpleImport("net/http"), 240 codegen.SimpleImport("fmt"), 241 codegen.SimpleImport("context"), 242 codegen.NewImport("goa", "github.com/shogo82148/goa-v1"), 243 codegen.SimpleImport("github.com/shogo82148/goa-v1/cors"), 244 codegen.SimpleImport("regexp"), 245 codegen.SimpleImport("strconv"), 246 codegen.SimpleImport("time"), 247 codegen.NewImport("uuid", "github.com/gofrs/uuid"), 248 codegen.SimpleImport("errors"), 249 } 250 encoders, err := BuildEncoders(g.API.Produces, true) 251 if err != nil { 252 return err 253 } 254 decoders, err := BuildEncoders(g.API.Consumes, false) 255 if err != nil { 256 return err 257 } 258 encoderImports := make(map[string]bool) 259 for _, data := range encoders { 260 encoderImports[data.PackagePath] = true 261 } 262 for _, data := range decoders { 263 encoderImports[data.PackagePath] = true 264 } 265 var packagePaths []string 266 for packagePath := range encoderImports { 267 if packagePath != "github.com/shogo82148/goa-v1" { 268 packagePaths = append(packagePaths, packagePath) 269 } 270 } 271 sort.Strings(packagePaths) 272 for _, packagePath := range packagePaths { 273 imports = append(imports, codegen.SimpleImport(packagePath)) 274 } 275 if err = ctlWr.WriteHeader(title, g.Target, imports); err != nil { 276 return err 277 } 278 if err = ctlWr.WriteInitService(encoders, decoders); err != nil { 279 return err 280 } 281 282 g.genfiles = append(g.genfiles, ctlFile) 283 var controllersData []*ControllerTemplateData 284 g.API.IterateResources(func(r *design.ResourceDefinition) error { 285 // Create file servers for all directory file servers that serve index.html. 286 fileServers := r.FileServers 287 for _, fs := range r.FileServers { 288 if fs.IsDir() { 289 rpath := design.WildcardRegex.ReplaceAllLiteralString(fs.RequestPath, "") 290 rpath += "/" 291 fileServers = append(fileServers, &design.FileServerDefinition{ 292 Parent: fs.Parent, 293 Description: fs.Description, 294 Docs: fs.Docs, 295 FilePath: filepath.Join(fs.FilePath, "index.html"), 296 RequestPath: rpath, 297 Metadata: fs.Metadata, 298 Security: fs.Security, 299 }) 300 } 301 } 302 data := &ControllerTemplateData{ 303 API: g.API, 304 Resource: codegen.Goify(r.Name, true), 305 PreflightPaths: r.PreflightPaths(), 306 FileServers: fileServers, 307 } 308 r.IterateActions(func(a *design.ActionDefinition) error { 309 context := fmt.Sprintf("%s%sContext", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true)) 310 unmarshal := fmt.Sprintf("unmarshal%s%sPayload", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true)) 311 action := map[string]interface{}{ 312 "Name": codegen.Goify(a.Name, true), 313 "DesignName": a.Name, 314 "Routes": a.Routes, 315 "Context": context, 316 "Unmarshal": unmarshal, 317 "Payload": a.Payload, 318 "PayloadOptional": a.PayloadOptional, 319 "PayloadMultipart": a.PayloadMultipart, 320 "Security": a.Security, 321 } 322 data.Actions = append(data.Actions, action) 323 return nil 324 }) 325 if len(data.Actions) > 0 || len(data.FileServers) > 0 { 326 data.Encoders = encoders 327 data.Decoders = decoders 328 data.Origins = r.AllOrigins() 329 controllersData = append(controllersData, data) 330 } 331 return nil 332 }) 333 err = ctlWr.Execute(controllersData) 334 return 335 } 336 337 // generateControllers iterates through the API resources and generates the low level 338 // controllers. 339 func (g *Generator) generateSecurity() (err error) { 340 if len(g.API.SecuritySchemes) == 0 { 341 return nil 342 } 343 344 var ( 345 secFile string 346 secWr *SecurityWriter 347 ) 348 { 349 secFile = filepath.Join(g.OutDir, "security.go") 350 secWr, err = NewSecurityWriter(secFile) 351 if err != nil { 352 return 353 } 354 } 355 defer func() { 356 secWr.Close() 357 if err == nil { 358 err = secWr.FormatCode() 359 } 360 }() 361 title := fmt.Sprintf("%s: Application Security", g.API.Context()) 362 imports := []*codegen.ImportSpec{ 363 codegen.SimpleImport("net/http"), 364 codegen.SimpleImport("errors"), 365 codegen.SimpleImport("context"), 366 codegen.NewImport("goa", "github.com/shogo82148/goa-v1"), 367 } 368 if err = secWr.WriteHeader(title, g.Target, imports); err != nil { 369 return err 370 } 371 g.genfiles = append(g.genfiles, secFile) 372 err = secWr.Execute(design.Design.SecuritySchemes) 373 374 return 375 } 376 377 // generateHrefs iterates through the API resources and generates the href factory methods. 378 func (g *Generator) generateHrefs() (err error) { 379 var ( 380 hrefFile string 381 resWr *ResourcesWriter 382 ) 383 { 384 hrefFile = filepath.Join(g.OutDir, "hrefs.go") 385 resWr, err = NewResourcesWriter(hrefFile) 386 if err != nil { 387 return 388 } 389 } 390 defer func() { 391 resWr.Close() 392 if err == nil { 393 err = resWr.FormatCode() 394 } 395 }() 396 title := fmt.Sprintf("%s: Application Resource Href Factories", g.API.Context()) 397 imports := []*codegen.ImportSpec{ 398 codegen.SimpleImport("fmt"), 399 codegen.SimpleImport("strings"), 400 } 401 if err = resWr.WriteHeader(title, g.Target, imports); err != nil { 402 return err 403 } 404 g.genfiles = append(g.genfiles, hrefFile) 405 err = g.API.IterateResources(func(r *design.ResourceDefinition) error { 406 m := g.API.MediaTypeWithIdentifier(r.MediaType) 407 var identifier string 408 if m != nil { 409 identifier = m.Identifier 410 } else { 411 identifier = "text/plain" 412 } 413 data := ResourceData{ 414 Name: codegen.Goify(r.Name, true), 415 Identifier: identifier, 416 Description: r.Description, 417 Type: m, 418 CanonicalTemplate: codegen.CanonicalTemplate(r), 419 CanonicalParams: codegen.CanonicalParams(r), 420 } 421 return resWr.Execute(&data) 422 }) 423 return 424 } 425 426 // generateMediaTypes iterates through the media types and generate the data structures and 427 // marshaling code. 428 func (g *Generator) generateMediaTypes() (err error) { 429 var ( 430 mtFile string 431 mtWr *MediaTypesWriter 432 ) 433 { 434 mtFile = filepath.Join(g.OutDir, "media_types.go") 435 mtWr, err = NewMediaTypesWriter(mtFile) 436 if err != nil { 437 return 438 } 439 } 440 defer func() { 441 mtWr.Close() 442 if err == nil { 443 err = mtWr.FormatCode() 444 } 445 }() 446 title := fmt.Sprintf("%s: Application Media Types", g.API.Context()) 447 imports := []*codegen.ImportSpec{ 448 codegen.NewImport("goa", "github.com/shogo82148/goa-v1"), 449 codegen.SimpleImport("fmt"), 450 codegen.SimpleImport("time"), 451 codegen.SimpleImport("unicode/utf8"), 452 codegen.NewImport("uuid", "github.com/gofrs/uuid"), 453 } 454 for _, v := range g.API.MediaTypes { 455 imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil) 456 } 457 if err = mtWr.WriteHeader(title, g.Target, imports); err != nil { 458 return err 459 } 460 g.genfiles = append(g.genfiles, mtFile) 461 err = g.API.IterateMediaTypes(func(mt *design.MediaTypeDefinition) error { 462 if mt.IsError() { 463 return nil 464 } 465 if mt.Type.IsObject() || mt.Type.IsArray() { 466 return mtWr.Execute(mt) 467 } 468 return nil 469 }) 470 return 471 } 472 473 // generateUserTypes iterates through the user types and generates the data structures and 474 // marshaling code. 475 func (g *Generator) generateUserTypes() (err error) { 476 var ( 477 utFile string 478 utWr *UserTypesWriter 479 ) 480 { 481 utFile = filepath.Join(g.OutDir, "user_types.go") 482 utWr, err = NewUserTypesWriter(utFile) 483 if err != nil { 484 return 485 } 486 } 487 defer func() { 488 utWr.Close() 489 if err == nil { 490 err = utWr.FormatCode() 491 } 492 }() 493 title := fmt.Sprintf("%s: Application User Types", g.API.Context()) 494 imports := []*codegen.ImportSpec{ 495 codegen.SimpleImport("fmt"), 496 codegen.SimpleImport("mime/multipart"), 497 codegen.SimpleImport("time"), 498 codegen.SimpleImport("unicode/utf8"), 499 codegen.NewImport("goa", "github.com/shogo82148/goa-v1"), 500 codegen.NewImport("uuid", "github.com/gofrs/uuid"), 501 } 502 for _, v := range g.API.Types { 503 imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil) 504 } 505 if err = utWr.WriteHeader(title, g.Target, imports); err != nil { 506 return err 507 } 508 g.genfiles = append(g.genfiles, utFile) 509 err = g.API.IterateUserTypes(func(t *design.UserTypeDefinition) error { 510 return utWr.Execute(t) 511 }) 512 return 513 }