github.com/goldeneggg/goa@v1.3.1/goagen/gen_client/generator.go (about) 1 package genclient 2 3 import ( 4 "flag" 5 "fmt" 6 "os" 7 "path" 8 "path/filepath" 9 "sort" 10 "strings" 11 "text/template" 12 13 "github.com/goadesign/goa/design" 14 "github.com/goadesign/goa/dslengine" 15 "github.com/goadesign/goa/goagen/codegen" 16 "github.com/goadesign/goa/goagen/gen_app" 17 "github.com/goadesign/goa/goagen/utils" 18 ) 19 20 // Filename used to generate all data types (without the ".go" extension) 21 const typesFileName = "datatypes" 22 23 //NewGenerator returns an initialized instance of a Go Client Generator 24 func NewGenerator(options ...Option) *Generator { 25 g := &Generator{} 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 Target string // Name of generated package 39 ToolDirName string // Name of tool directory where CLI main is generated once 40 Tool string // Name of CLI tool 41 NoTool bool // Whether to skip tool generation 42 genfiles []string 43 encoders []*genapp.EncoderTemplateData 44 decoders []*genapp.EncoderTemplateData 45 encoderImports []string 46 } 47 48 // Generate is the generator entry point called by the meta generator. 49 func Generate() (files []string, err error) { 50 var ( 51 outDir, target, toolDir, tool, ver string 52 notool, regen bool 53 ) 54 dtool := defaultToolName(design.Design) 55 56 set := flag.NewFlagSet("client", flag.PanicOnError) 57 set.StringVar(&outDir, "out", "", "") 58 set.StringVar(&target, "pkg", "client", "") 59 set.StringVar(&toolDir, "tooldir", "tool", "") 60 set.StringVar(&tool, "tool", dtool, "") 61 set.StringVar(&ver, "version", "", "") 62 set.BoolVar(¬ool, "notool", false, "") 63 set.BoolVar(®en, "regen", false, "") 64 set.String("design", "", "") 65 set.Bool("force", false, "") 66 set.Bool("notest", false, "") 67 set.Parse(os.Args[1:]) 68 69 // First check compatibility 70 if err := codegen.CheckVersion(ver); err != nil { 71 return nil, err 72 } 73 74 // Now proceed 75 target = codegen.Goify(target, false) 76 g := &Generator{OutDir: outDir, Target: target, ToolDirName: toolDir, Tool: tool, NoTool: notool, API: design.Design} 77 78 return g.Generate() 79 } 80 81 // Generate generats the client package and CLI. 82 func (g *Generator) Generate() (_ []string, err error) { 83 if g.API == nil { 84 return nil, fmt.Errorf("missing API definition, make sure design is properly initialized") 85 } 86 87 go utils.Catch(nil, func() { g.Cleanup() }) 88 89 defer func() { 90 if err != nil { 91 g.Cleanup() 92 } 93 }() 94 95 firstNonEmpty := func(args ...string) string { 96 for _, value := range args { 97 if len(value) > 0 { 98 return value 99 } 100 } 101 return "" 102 } 103 104 g.Target = firstNonEmpty(g.Target, "client") 105 g.ToolDirName = firstNonEmpty(g.ToolDirName, "tool") 106 g.Tool = firstNonEmpty(g.Tool, defaultToolName(g.API)) 107 108 codegen.Reserved[g.Target] = true 109 110 // Setup output directories as needed 111 var pkgDir, toolDir, cliDir string 112 { 113 if !g.NoTool { 114 toolDir = filepath.Join(g.OutDir, g.ToolDirName, g.Tool) 115 if _, err = os.Stat(toolDir); err != nil { 116 if err = os.MkdirAll(toolDir, 0755); err != nil { 117 return 118 } 119 } 120 121 cliDir = filepath.Join(g.OutDir, g.ToolDirName, "cli") 122 if err = os.RemoveAll(cliDir); err != nil { 123 return 124 } 125 if err = os.MkdirAll(cliDir, 0755); err != nil { 126 return 127 } 128 } 129 130 pkgDir = filepath.Join(g.OutDir, g.Target) 131 if err = os.RemoveAll(pkgDir); err != nil { 132 return 133 } 134 if err = os.MkdirAll(pkgDir, 0755); err != nil { 135 return 136 } 137 } 138 139 // Setup generation 140 var funcs template.FuncMap 141 var clientPkg string 142 { 143 funcs = template.FuncMap{ 144 "add": func(a, b int) int { return a + b }, 145 "cmdFieldType": cmdFieldType, 146 "defaultPath": defaultPath, 147 "escapeBackticks": escapeBackticks, 148 "goify": codegen.Goify, 149 "gotypedef": codegen.GoTypeDef, 150 "gotypedesc": codegen.GoTypeDesc, 151 "gotypename": codegen.GoTypeName, 152 "gotyperef": codegen.GoTypeRef, 153 "gotyperefext": goTypeRefExt, 154 "join": join, 155 "joinStrings": strings.Join, 156 "multiComment": multiComment, 157 "pathParams": pathParams, 158 "pathTemplate": pathTemplate, 159 "signerType": signerType, 160 "tempvar": codegen.Tempvar, 161 "title": strings.Title, 162 "toString": toString, 163 "typeName": typeName, 164 "format": format, 165 "handleSpecialTypes": handleSpecialTypes, 166 } 167 clientPkg, err = codegen.PackagePath(pkgDir) 168 if err != nil { 169 return 170 } 171 arrayToStringTmpl = template.Must(template.New("client").Funcs(funcs).Parse(arrayToStringT)) 172 } 173 174 if !g.NoTool { 175 var cliPkg string 176 cliPkg, err = codegen.PackagePath(cliDir) 177 if err != nil { 178 return 179 } 180 181 // Generate tool/main.go (only once) 182 mainFile := filepath.Join(toolDir, "main.go") 183 if _, err := os.Stat(mainFile); err != nil { 184 g.genfiles = append(g.genfiles, toolDir) 185 if err = g.generateMain(mainFile, clientPkg, cliPkg, funcs); err != nil { 186 return nil, err 187 } 188 } 189 190 // Generate tool/cli/commands.go 191 g.genfiles = append(g.genfiles, cliDir) 192 if err = g.generateCommands(filepath.Join(cliDir, "commands.go"), clientPkg, funcs); err != nil { 193 return 194 } 195 } 196 197 // Generate client/client.go 198 g.genfiles = append(g.genfiles, pkgDir) 199 if err = g.generateClient(filepath.Join(pkgDir, "client.go"), clientPkg, funcs); err != nil { 200 return 201 } 202 203 // Generate client/$res.go and types.go 204 if err = g.generateClientResources(pkgDir, clientPkg, funcs); err != nil { 205 return 206 } 207 208 return g.genfiles, nil 209 } 210 211 func defaultToolName(api *design.APIDefinition) string { 212 if api == nil { 213 return "" 214 } 215 return strings.Replace(strings.ToLower(api.Name), " ", "-", -1) + "-cli" 216 } 217 218 // Cleanup removes all the files generated by this generator during the last invokation of Generate. 219 func (g *Generator) Cleanup() { 220 for _, f := range g.genfiles { 221 os.Remove(f) 222 } 223 g.genfiles = nil 224 } 225 226 func (g *Generator) generateClient(clientFile string, clientPkg string, funcs template.FuncMap) (err error) { 227 var file *codegen.SourceFile 228 { 229 file, err = codegen.SourceFileFor(clientFile) 230 if err != nil { 231 return 232 } 233 } 234 defer func() { 235 file.Close() 236 if err == nil { 237 err = file.FormatCode() 238 } 239 }() 240 clientTmpl := template.Must(template.New("client").Funcs(funcs).Parse(clientTmpl)) 241 242 // Compute list of encoders and decoders 243 encoders, err := genapp.BuildEncoders(g.API.Produces, true) 244 if err != nil { 245 return err 246 } 247 decoders, err := genapp.BuildEncoders(g.API.Consumes, false) 248 if err != nil { 249 return err 250 } 251 im := make(map[string]bool) 252 for _, data := range encoders { 253 im[data.PackagePath] = true 254 } 255 for _, data := range decoders { 256 im[data.PackagePath] = true 257 } 258 var packagePaths []string 259 for packagePath := range im { 260 if packagePath != "github.com/goadesign/goa" { 261 packagePaths = append(packagePaths, packagePath) 262 } 263 } 264 sort.Strings(packagePaths) 265 266 // Setup codegen 267 imports := []*codegen.ImportSpec{ 268 codegen.SimpleImport("net/http"), 269 codegen.SimpleImport("github.com/goadesign/goa"), 270 codegen.NewImport("goaclient", "github.com/goadesign/goa/client"), 271 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 272 } 273 for _, packagePath := range packagePaths { 274 imports = append(imports, codegen.SimpleImport(packagePath)) 275 } 276 title := fmt.Sprintf("%s: Client", g.API.Context()) 277 if err = file.WriteHeader(title, g.Target, imports); err != nil { 278 return err 279 } 280 g.genfiles = append(g.genfiles, clientFile) 281 282 // Generate 283 data := struct { 284 API *design.APIDefinition 285 Encoders []*genapp.EncoderTemplateData 286 Decoders []*genapp.EncoderTemplateData 287 }{ 288 API: g.API, 289 Encoders: encoders, 290 Decoders: decoders, 291 } 292 err = clientTmpl.Execute(file, data) 293 return 294 } 295 296 func (g *Generator) generateClientResources(pkgDir, clientPkg string, funcs template.FuncMap) error { 297 err := g.API.IterateResources(func(res *design.ResourceDefinition) error { 298 return g.generateResourceClient(pkgDir, res, funcs) 299 }) 300 if err != nil { 301 return err 302 } 303 if err := g.generateUserTypes(pkgDir); err != nil { 304 return err 305 } 306 307 return g.generateMediaTypes(pkgDir, funcs) 308 } 309 310 func (g *Generator) generateResourceClient(pkgDir string, res *design.ResourceDefinition, funcs template.FuncMap) (err error) { 311 payloadTmpl := template.Must(template.New("payload").Funcs(funcs).Parse(payloadTmpl)) 312 pathTmpl := template.Must(template.New("pathTemplate").Funcs(funcs).Parse(pathTmpl)) 313 314 resFilename := codegen.SnakeCase(res.Name) 315 if resFilename == typesFileName { 316 // Avoid clash with datatypes.go 317 resFilename += "_client" 318 } 319 filename := filepath.Join(pkgDir, resFilename+".go") 320 321 var file *codegen.SourceFile 322 file, err = codegen.SourceFileFor(filename) 323 if err != nil { 324 return err 325 } 326 defer func() { 327 file.Close() 328 if err == nil { 329 err = file.FormatCode() 330 } 331 }() 332 imports := []*codegen.ImportSpec{ 333 codegen.SimpleImport("bytes"), 334 codegen.SimpleImport("encoding/json"), 335 codegen.SimpleImport("fmt"), 336 codegen.SimpleImport("io"), 337 codegen.SimpleImport("io/ioutil"), 338 codegen.SimpleImport("net/http"), 339 codegen.SimpleImport("net/url"), 340 codegen.SimpleImport("os"), 341 codegen.SimpleImport("path"), 342 codegen.SimpleImport("strconv"), 343 codegen.SimpleImport("strings"), 344 codegen.SimpleImport("time"), 345 codegen.SimpleImport("context"), 346 codegen.SimpleImport("golang.org/x/net/websocket"), 347 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 348 } 349 title := fmt.Sprintf("%s: %s Resource Client", g.API.Context(), res.Name) 350 if err = file.WriteHeader(title, g.Target, imports); err != nil { 351 return err 352 } 353 g.genfiles = append(g.genfiles, filename) 354 355 err = res.IterateFileServers(func(fs *design.FileServerDefinition) error { 356 return g.generateFileServer(file, fs, funcs) 357 }) 358 if err != nil { 359 return err 360 } 361 362 err = res.IterateActions(func(action *design.ActionDefinition) error { 363 if action.Payload != nil { 364 found := false 365 typeName := action.Payload.TypeName 366 for _, t := range design.Design.Types { 367 if t.TypeName == typeName { 368 found = true 369 break 370 } 371 } 372 if !found { 373 if err := payloadTmpl.Execute(file, action); err != nil { 374 return err 375 } 376 } 377 } 378 for i, r := range action.Routes { 379 routeParams := r.Params() 380 var pd []*paramData 381 382 for _, p := range routeParams { 383 requiredParams, _ := initParams(&design.AttributeDefinition{ 384 Type: &design.Object{ 385 p: action.Params.Type.ToObject()[p], 386 }, 387 Validation: &dslengine.ValidationDefinition{ 388 Required: routeParams, 389 }, 390 }) 391 pd = append(pd, requiredParams...) 392 } 393 394 data := struct { 395 Route *design.RouteDefinition 396 Index int 397 Params []*paramData 398 }{ 399 Route: r, 400 Index: i, 401 Params: pd, 402 } 403 if err := pathTmpl.Execute(file, data); err != nil { 404 return err 405 } 406 } 407 return g.generateActionClient(action, file, funcs) 408 }) 409 return 410 } 411 412 func (g *Generator) generateFileServer(file *codegen.SourceFile, fs *design.FileServerDefinition, funcs template.FuncMap) error { 413 var ( 414 dir string 415 416 fsTmpl = template.Must(template.New("fileserver").Funcs(funcs).Parse(fsTmpl)) 417 name = g.fileServerMethod(fs) 418 wcs = design.ExtractWildcards(fs.RequestPath) 419 scheme = "http" 420 ) 421 422 if len(wcs) > 0 { 423 dir = "/" 424 fileElems := filepath.SplitList(fs.FilePath) 425 if len(fileElems) > 1 { 426 dir = fileElems[len(fileElems)-2] 427 } 428 } 429 if len(design.Design.Schemes) > 0 { 430 scheme = design.Design.Schemes[0] 431 } 432 requestDir, _ := path.Split(fs.RequestPath) 433 434 data := struct { 435 Name string // Download functionn name 436 RequestPath string // File server request path 437 FilePath string // File server file path 438 FileName string // Filename being download if request path has no wildcard 439 DirName string // Parent directory name if request path has wildcard 440 RequestDir string // Request path without wildcard suffix 441 CanonicalScheme string // HTTP scheme 442 }{ 443 Name: name, 444 RequestPath: fs.RequestPath, 445 FilePath: fs.FilePath, 446 FileName: filepath.Base(fs.FilePath), 447 DirName: dir, 448 RequestDir: requestDir, 449 CanonicalScheme: scheme, 450 } 451 return fsTmpl.Execute(file, data) 452 } 453 454 func (g *Generator) generateActionClient(action *design.ActionDefinition, file *codegen.SourceFile, funcs template.FuncMap) error { 455 var ( 456 params []string 457 names []string 458 queryParams []*paramData 459 headers []*paramData 460 signer string 461 clientsTmpl = template.Must(template.New("clients").Funcs(funcs).Parse(clientsTmpl)) 462 requestsTmpl = template.Must(template.New("requests").Funcs(funcs).Parse(requestsTmpl)) 463 clientsWSTmpl = template.Must(template.New("clientsws").Funcs(funcs).Parse(clientsWSTmpl)) 464 ) 465 if action.Payload != nil { 466 params = append(params, "payload "+codegen.GoTypeRef(action.Payload, action.Payload.AllRequired(), 1, false)) 467 names = append(names, "payload") 468 } 469 470 initParamsScoped := func(att *design.AttributeDefinition) []*paramData { 471 reqData, optData := initParams(att) 472 473 sort.Sort(byParamName(reqData)) 474 sort.Sort(byParamName(optData)) 475 476 // Update closure 477 for _, p := range reqData { 478 names = append(names, p.VarName) 479 params = append(params, p.VarName+" "+cmdFieldType(p.Attribute.Type, false)) 480 } 481 for _, p := range optData { 482 names = append(names, p.VarName) 483 params = append(params, p.VarName+" "+cmdFieldType(p.Attribute.Type, p.Attribute.Type.IsPrimitive())) 484 } 485 return append(reqData, optData...) 486 } 487 queryParams = initParamsScoped(action.QueryParams) 488 headers = initParamsScoped(action.Headers) 489 490 if action.Security != nil { 491 signer = codegen.Goify(action.Security.Scheme.SchemeName, true) 492 } 493 data := struct { 494 Name string 495 ResourceName string 496 Description string 497 Routes []*design.RouteDefinition 498 HasPayload bool 499 HasMultiContent bool 500 DefaultContentType string 501 Params string 502 ParamNames string 503 CanonicalScheme string 504 Signer string 505 QueryParams []*paramData 506 Headers []*paramData 507 }{ 508 Name: action.Name, 509 ResourceName: action.Parent.Name, 510 Description: action.Description, 511 Routes: action.Routes, 512 HasPayload: action.Payload != nil, 513 HasMultiContent: len(design.Design.Consumes) > 1, 514 DefaultContentType: design.Design.Consumes[0].MIMETypes[0], 515 Params: strings.Join(params, ", "), 516 ParamNames: strings.Join(names, ", "), 517 CanonicalScheme: action.CanonicalScheme(), 518 Signer: signer, 519 QueryParams: queryParams, 520 Headers: headers, 521 } 522 if action.WebSocket() { 523 return clientsWSTmpl.Execute(file, data) 524 } 525 if err := clientsTmpl.Execute(file, data); err != nil { 526 return err 527 } 528 return requestsTmpl.Execute(file, data) 529 } 530 531 // fileServerMethod returns the name of the client method for downloading assets served by the given 532 // file server. 533 // Note: the implementation opts for generating good names rather than names that are guaranteed to 534 // be unique. This means that the generated code could be potentially incorrect in the rare cases 535 // where it produces the same names for two different file servers. This should be addressed later 536 // (when it comes up?) using metadata to let users override the default. 537 func (g *Generator) fileServerMethod(fs *design.FileServerDefinition) string { 538 var ( 539 suffix string 540 541 wcs = design.ExtractWildcards(fs.RequestPath) 542 reqElems = strings.Split(fs.RequestPath, "/") 543 ) 544 545 if len(wcs) == 0 { 546 suffix = path.Base(fs.RequestPath) 547 ext := filepath.Ext(suffix) 548 suffix = strings.TrimSuffix(suffix, ext) 549 suffix += codegen.Goify(ext, true) 550 } else { 551 if len(reqElems) == 1 { 552 suffix = filepath.Base(fs.RequestPath) 553 suffix = suffix[1:] // remove "*" prefix 554 } else { 555 suffix = reqElems[len(reqElems)-2] // should work most of the time 556 } 557 } 558 return "Download" + codegen.Goify(suffix, true) 559 } 560 561 // generateMediaTypes iterates through the media types and generate the data structures and 562 // marshaling code. 563 func (g *Generator) generateMediaTypes(pkgDir string, funcs template.FuncMap) (err error) { 564 funcs["decodegotyperef"] = decodeGoTypeRef 565 funcs["decodegotypename"] = decodeGoTypeName 566 typeDecodeTmpl := template.Must(template.New("typeDecode").Funcs(funcs).Parse(typeDecodeTmpl)) 567 var ( 568 mtFile string 569 mtWr *genapp.MediaTypesWriter 570 ) 571 { 572 mtFile = filepath.Join(pkgDir, "media_types.go") 573 mtWr, err = genapp.NewMediaTypesWriter(mtFile) 574 if err != nil { 575 return 576 } 577 } 578 defer func() { 579 mtWr.Close() 580 if err == nil { 581 err = mtWr.FormatCode() 582 } 583 }() 584 title := fmt.Sprintf("%s: Application Media Types", g.API.Context()) 585 imports := []*codegen.ImportSpec{ 586 codegen.SimpleImport("github.com/goadesign/goa"), 587 codegen.SimpleImport("fmt"), 588 codegen.SimpleImport("net/http"), 589 codegen.SimpleImport("time"), 590 codegen.SimpleImport("unicode/utf8"), 591 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 592 } 593 for _, v := range g.API.MediaTypes { 594 imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil) 595 } 596 if err = mtWr.WriteHeader(title, g.Target, imports); err != nil { 597 return err 598 } 599 g.genfiles = append(g.genfiles, mtFile) 600 err = g.API.IterateMediaTypes(func(mt *design.MediaTypeDefinition) error { 601 if (mt.Type.IsObject() || mt.Type.IsArray()) && !mt.IsError() { 602 if err := mtWr.Execute(mt); err != nil { 603 return err 604 } 605 } 606 err := mt.IterateViews(func(view *design.ViewDefinition) error { 607 p, _, err := mt.Project(view.Name) 608 if err != nil { 609 return err 610 } 611 return typeDecodeTmpl.Execute(mtWr.SourceFile, p) 612 }) 613 return err 614 }) 615 return 616 } 617 618 // generateUserTypes iterates through the user types and generates the data structures and 619 // marshaling code. 620 func (g *Generator) generateUserTypes(pkgDir string) (err error) { 621 var ( 622 utFile string 623 utWr *genapp.UserTypesWriter 624 ) 625 { 626 utFile = filepath.Join(pkgDir, "user_types.go") 627 utWr, err = genapp.NewUserTypesWriter(utFile) 628 if err != nil { 629 return 630 } 631 } 632 defer func() { 633 utWr.Close() 634 if err == nil { 635 err = utWr.FormatCode() 636 } 637 }() 638 title := fmt.Sprintf("%s: Application User Types", g.API.Context()) 639 imports := []*codegen.ImportSpec{ 640 codegen.SimpleImport("github.com/goadesign/goa"), 641 codegen.SimpleImport("fmt"), 642 codegen.SimpleImport("time"), 643 codegen.SimpleImport("unicode/utf8"), 644 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 645 } 646 for _, v := range g.API.Types { 647 imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil) 648 } 649 if err = utWr.WriteHeader(title, g.Target, imports); err != nil { 650 return err 651 } 652 g.genfiles = append(g.genfiles, utFile) 653 err = g.API.IterateUserTypes(func(t *design.UserTypeDefinition) error { 654 return utWr.Execute(t) 655 }) 656 return 657 } 658 659 // join is a code generation helper function that generates a function signature built from 660 // concatenating the properties (name type) of the given attribute type (assuming it's an object). 661 // join accepts an optional slice of strings which indicates the order in which the parameters 662 // should appear in the signature. If pos is specified then it must list all the parameters. If 663 // it's not specified then parameters are sorted alphabetically. 664 func join(att *design.AttributeDefinition, usePointers bool, pos ...[]string) string { 665 if att == nil { 666 return "" 667 } 668 obj := att.Type.ToObject() 669 elems := make([]string, len(obj)) 670 var keys []string 671 if len(pos) > 0 { 672 keys = pos[0] 673 if len(keys) != len(obj) { 674 panic("invalid position slice, lenght does not match attribute field count") // bug 675 } 676 } else { 677 keys = make([]string, len(obj)) 678 i := 0 679 for n := range obj { 680 keys[i] = n 681 i++ 682 } 683 sort.Strings(keys) 684 } 685 for i, n := range keys { 686 a := obj[n] 687 elems[i] = fmt.Sprintf("%s %s", codegen.Goify(n, false), 688 cmdFieldType(a.Type, usePointers && !a.IsRequired(n))) 689 } 690 return strings.Join(elems, ", ") 691 } 692 693 // escapeBackticks is a code generation helper that escapes backticks in a string. 694 func escapeBackticks(text string) string { 695 return strings.Replace(text, "`", "`+\"`\"+`", -1) 696 } 697 698 // multiComment produces a Go comment containing the given string taking into account newlines. 699 func multiComment(text string) string { 700 lines := strings.Split(text, "\n") 701 nl := make([]string, len(lines)) 702 for i, l := range lines { 703 nl[i] = "// " + strings.TrimSpace(l) 704 } 705 return strings.Join(nl, "\n") 706 } 707 708 // gotTypeRefExt computes the type reference for a type in a different package. 709 func goTypeRefExt(t design.DataType, tabs int, pkg string) string { 710 ref := codegen.GoTypeRef(t, nil, tabs, false) 711 if strings.HasPrefix(ref, "*") { 712 return fmt.Sprintf("%s.%s", pkg, ref[1:]) 713 } 714 return fmt.Sprintf("%s.%s", pkg, ref) 715 } 716 717 // decodeGoTypeRef handles the case where the type being decoded is a error response media type. 718 func decodeGoTypeRef(t design.DataType, required []string, tabs int, private bool) string { 719 mt, ok := t.(*design.MediaTypeDefinition) 720 if ok && mt.IsError() { 721 return "*goa.ErrorResponse" 722 } 723 return codegen.GoTypeRef(t, required, tabs, private) 724 } 725 726 // decodeGoTypeName handles the case where the type being decoded is a error response media type. 727 func decodeGoTypeName(t design.DataType, required []string, tabs int, private bool) string { 728 mt, ok := t.(*design.MediaTypeDefinition) 729 if ok && mt.IsError() { 730 return "goa.ErrorResponse" 731 } 732 return codegen.GoTypeName(t, required, tabs, private) 733 } 734 735 // cmdFieldType computes the Go type name used to store command flags of the given design type. 736 func cmdFieldType(t design.DataType, point bool) string { 737 var pointer, suffix string 738 if point && !t.IsArray() { 739 pointer = "*" 740 } 741 suffix = codegen.GoNativeType(t) 742 return pointer + suffix 743 } 744 745 // cmdFieldTypeString computes the Go type name used to store command flags of the given design type. Complex types are String 746 func cmdFieldTypeString(t design.DataType, point bool) string { 747 var pointer, suffix string 748 if point && !t.IsArray() { 749 pointer = "*" 750 } 751 if t.Kind() == design.UUIDKind || t.Kind() == design.DateTimeKind || t.Kind() == design.AnyKind || t.Kind() == design.NumberKind || t.Kind() == design.BooleanKind { 752 suffix = "string" 753 } else if isArrayOfType(t, design.UUIDKind, design.DateTimeKind, design.AnyKind, design.NumberKind, design.BooleanKind) { 754 suffix = "[]string" 755 } else { 756 suffix = codegen.GoNativeType(t) 757 } 758 return pointer + suffix 759 } 760 761 func isArrayOfType(array design.DataType, kinds ...design.Kind) bool { 762 if !array.IsArray() { 763 return false 764 } 765 kind := array.ToArray().ElemType.Type.Kind() 766 for _, t := range kinds { 767 if t == kind { 768 return true 769 } 770 } 771 return false 772 } 773 774 // template used to produce code that serializes arrays of simple values into comma separated 775 // strings. 776 var arrayToStringTmpl *template.Template 777 778 // toString generates Go code that converts the given simple type attribute into a string. 779 func toString(name, target string, att *design.AttributeDefinition) string { 780 switch actual := att.Type.(type) { 781 case design.Primitive: 782 switch actual.Kind() { 783 case design.IntegerKind: 784 return fmt.Sprintf("%s := strconv.Itoa(%s)", target, name) 785 case design.BooleanKind: 786 return fmt.Sprintf("%s := strconv.FormatBool(%s)", target, name) 787 case design.NumberKind: 788 return fmt.Sprintf("%s := strconv.FormatFloat(%s, 'f', -1, 64)", target, name) 789 case design.StringKind: 790 return fmt.Sprintf("%s := %s", target, name) 791 case design.DateTimeKind: 792 return fmt.Sprintf("%s := %s.Format(time.RFC3339)", target, strings.Replace(name, "*", "", -1)) // remove pointer if present 793 case design.UUIDKind: 794 return fmt.Sprintf("%s := %s.String()", target, strings.Replace(name, "*", "", -1)) // remove pointer if present 795 case design.AnyKind: 796 return fmt.Sprintf("%s := fmt.Sprintf(\"%%v\", %s)", target, name) 797 default: 798 panic("unknown primitive type") 799 } 800 case *design.Array: 801 data := map[string]interface{}{ 802 "Name": name, 803 "Target": target, 804 "ElemType": actual.ElemType, 805 } 806 return codegen.RunTemplate(arrayToStringTmpl, data) 807 default: 808 panic("cannot convert non simple type " + att.Type.Name() + " to string") // bug 809 } 810 } 811 812 // defaultPath returns the first route path for the given action that does not take any wildcard, 813 // empty string if none. 814 func defaultPath(action *design.ActionDefinition) string { 815 for _, r := range action.Routes { 816 candidate := r.FullPath() 817 if !strings.ContainsRune(candidate, ':') { 818 return candidate 819 } 820 } 821 return "" 822 } 823 824 // signerType returns the name of the client signer used for the defined security model on the Action 825 func signerType(scheme *design.SecuritySchemeDefinition) string { 826 switch scheme.Kind { 827 case design.JWTSecurityKind: 828 return "goaclient.JWTSigner" // goa client package imported under goaclient 829 case design.OAuth2SecurityKind: 830 return "goaclient.OAuth2Signer" 831 case design.APIKeySecurityKind: 832 return "goaclient.APIKeySigner" 833 case design.BasicAuthSecurityKind: 834 return "goaclient.BasicSigner" 835 } 836 return "" 837 } 838 839 // pathTemplate returns a fmt format suitable to build a request path to the route. 840 func pathTemplate(r *design.RouteDefinition) string { 841 return design.WildcardRegex.ReplaceAllLiteralString(r.FullPath(), "/%s") 842 } 843 844 // pathParams return the function signature of the path factory function for the given route. 845 func pathParams(r *design.RouteDefinition) string { 846 pnames := r.Params() 847 params := make(design.Object, len(pnames)) 848 for _, p := range pnames { 849 params[p] = r.Parent.Params.Type.ToObject()[p] 850 } 851 return join(&design.AttributeDefinition{Type: params}, false, pnames) 852 } 853 854 // typeName returns Go type name of given MediaType definition. 855 func typeName(mt *design.MediaTypeDefinition) string { 856 if mt.IsError() { 857 return "ErrorResponse" 858 } 859 return codegen.GoTypeName(mt, mt.AllRequired(), 1, false) 860 } 861 862 // initParams returns required and optional paramData extracted from given attribute definition. 863 func initParams(att *design.AttributeDefinition) ([]*paramData, []*paramData) { 864 if att == nil { 865 return nil, nil 866 } 867 obj := att.Type.ToObject() 868 var reqParamData []*paramData 869 var optParamData []*paramData 870 for n, q := range obj { 871 varName := codegen.Goify(n, false) 872 param := ¶mData{ 873 Name: n, 874 VarName: varName, 875 Attribute: q, 876 } 877 if q.Type.IsPrimitive() { 878 param.MustToString = q.Type.Kind() != design.StringKind 879 if att.IsRequired(n) { 880 param.ValueName = varName 881 reqParamData = append(reqParamData, param) 882 } else { 883 param.ValueName = "*" + varName 884 param.CheckNil = true 885 optParamData = append(optParamData, param) 886 } 887 } else { 888 if q.Type.IsArray() { 889 param.IsArray = true 890 param.ElemAttribute = q.Type.ToArray().ElemType 891 } 892 param.MustToString = true 893 param.ValueName = varName 894 param.CheckNil = true 895 if att.IsRequired(n) { 896 reqParamData = append(reqParamData, param) 897 } else { 898 optParamData = append(optParamData, param) 899 } 900 } 901 } 902 903 return reqParamData, optParamData 904 } 905 906 // paramData is the data structure holding the information needed to generate query params and 907 // headers handling code. 908 type paramData struct { 909 Name string 910 VarName string 911 ValueName string 912 Attribute *design.AttributeDefinition 913 ElemAttribute *design.AttributeDefinition 914 MustToString bool 915 IsArray bool 916 CheckNil bool 917 } 918 919 type byParamName []*paramData 920 921 func (b byParamName) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 922 func (b byParamName) Less(i, j int) bool { return b[i].Name < b[j].Name } 923 func (b byParamName) Len() int { return len(b) } 924 925 const ( 926 arrayToStringT = ` {{ $tmp := tempvar }}{{ $tmp }} := make([]string, len({{ .Name }})) 927 for i, e := range {{ .Name }} { 928 {{ $tmp2 := tempvar }}{{ toString "e" $tmp2 .ElemType }} 929 {{ $tmp }}[i] = {{ $tmp2 }} 930 } 931 {{ .Target }} := strings.Join({{ $tmp }}, ",")` 932 933 payloadTmpl = `// {{ gotypename .Payload nil 0 false }} is the {{ .Parent.Name }} {{ .Name }} action payload. 934 type {{ gotypename .Payload nil 1 false }} {{ gotypedef .Payload 0 true false }} 935 ` 936 937 typeDecodeTmpl = `{{ $typeName := typeName . }}{{ $funcName := printf "Decode%s" $typeName }}// {{ $funcName }} decodes the {{ $typeName }} instance encoded in resp body. 938 func (c *Client) {{ $funcName }}(resp *http.Response) ({{ decodegotyperef . .AllRequired 0 false }}, error) { 939 var decoded {{ decodegotypename . .AllRequired 0 false }} 940 err := c.Decoder.Decode(&decoded, resp.Body, resp.Header.Get("Content-Type")) 941 return {{ if .IsObject }}&{{ end }}decoded, err 942 } 943 ` 944 945 pathTmpl = `{{ $funcName := printf "%sPath%s" (goify (printf "%s%s" .Route.Parent.Name (title .Route.Parent.Parent.Name)) true) ((or (and .Index (add .Index 1)) "") | printf "%v") }}{{/* 946 */}}// {{ $funcName }} computes a request path to the {{ .Route.Parent.Name }} action of {{ .Route.Parent.Parent.Name }}. 947 func {{ $funcName }}({{ pathParams .Route }}) string { 948 {{ range $i, $param := .Params }}{{/* 949 */}}{{ toString $param.VarName (printf "param%d" $i) $param.Attribute }} 950 {{ end }} 951 return fmt.Sprintf({{ printf "%q" (pathTemplate .Route) }}{{ range $i, $param := .Params }}, {{ printf "param%d" $i }}{{ end }}) 952 } 953 ` 954 955 clientsTmpl = `{{ $funcName := goify (printf "%s%s" .Name (title .ResourceName)) true }}{{ $desc := .Description }}{{/* 956 */}}{{ if $desc }}{{ multiComment $desc }}{{ else }}{{/* 957 */}}// {{ $funcName }} makes a request to the {{ .Name }} action endpoint of the {{ .ResourceName }} resource{{ end }} 958 func (c *Client) {{ $funcName }}(ctx context.Context, path string{{ if .Params }}, {{ .Params }}{{ end }}{{ if and .HasPayload .HasMultiContent }}, contentType string{{ end }}) (*http.Response, error) { 959 req, err := c.New{{ $funcName }}Request(ctx, path{{ if .ParamNames }}, {{ .ParamNames }}{{ end }}{{ if and .HasPayload .HasMultiContent }}, contentType{{ end }}) 960 if err != nil { 961 return nil, err 962 } 963 return c.Client.Do(ctx, req) 964 } 965 ` 966 967 clientsWSTmpl = `{{ $funcName := goify (printf "%s%s" .Name (title .ResourceName)) true }}{{ $desc := .Description }}{{/* 968 */}}{{ if $desc }}{{ multiComment $desc }}{{ else }}// {{ $funcName }} establishes a websocket connection to the {{ .Name }} action endpoint of the {{ .ResourceName }} resource{{ end }} 969 func (c *Client) {{ $funcName }}(ctx context.Context, path string{{ if .Params }}, {{ .Params }}{{ end }}) (*websocket.Conn, error) { 970 scheme := c.Scheme 971 if scheme == "" { 972 scheme = "{{ .CanonicalScheme }}" 973 } 974 u := url.URL{Host: c.Host, Scheme: scheme, Path: path} 975 {{ if .QueryParams }} values := u.Query() 976 {{ range .QueryParams }}{{ if .CheckNil }} if {{ .VarName }} != nil { 977 {{ end }}{{/* 978 979 // ARRAY 980 */}}{{ if .IsArray }} for _, p := range {{ .VarName }} { 981 {{ if .MustToString }}{{ $tmp := tempvar }} {{ toString "p" $tmp .ElemAttribute }} 982 values.Add("{{ .Name }}", {{ $tmp }}) 983 {{ else }} values.Add("{{ .Name }}", {{ .ValueName }}) 984 {{ end }}}{{/* 985 986 // NON STRING 987 */}}{{ else if .MustToString }}{{ $tmp := tempvar }} {{ toString .ValueName $tmp .Attribute }} 988 values.Set("{{ .Name }}", {{ $tmp }}) 989 {{/* 990 991 // STRING 992 */}}{{ else }} values.Set("{{ .Name }}", {{ .ValueName }}) 993 {{ end }}{{ if .CheckNil }} } 994 {{ end }}{{ end }} u.RawQuery = values.Encode() 995 {{ end }} url_ := u.String() 996 cfg, err := websocket.NewConfig(url_, url_) 997 if err != nil { 998 return nil, err 999 } 1000 {{ range $header := .Headers }}{{ $tmp := tempvar }} {{ toString $header.VarName $tmp $header.Attribute }} 1001 cfg.Header["{{ $header.Name }}"] = []string{ {{ $tmp }} } 1002 {{ end }} return websocket.DialConfig(cfg) 1003 } 1004 ` 1005 1006 fsTmpl = `// {{ .Name }} downloads {{ if .DirName }}{{ .DirName }}files with the given filename{{ else }}{{ .FileName }}{{ end }} and writes it to the file dest. 1007 // It returns the number of bytes downloaded in case of success. 1008 func (c * Client) {{ .Name }}(ctx context.Context, {{ if .DirName }}filename, {{ end }}dest string) (int64, error) { 1009 scheme := c.Scheme 1010 if scheme == "" { 1011 scheme = "{{ .CanonicalScheme }}" 1012 } 1013 {{ if .DirName }} p := path.Join("{{ .RequestDir }}", filename) 1014 {{ end }} u := url.URL{Host: c.Host, Scheme: scheme, Path: {{ if .DirName }}p{{ else }}"{{ .RequestPath }}"{{ end }}} 1015 req, err := http.NewRequest("GET", u.String(), nil) 1016 if err != nil { 1017 return 0, err 1018 } 1019 resp, err := c.Client.Do(ctx, req) 1020 if err != nil { 1021 return 0, err 1022 } 1023 if resp.StatusCode != 200 { 1024 var body string 1025 if b, err := ioutil.ReadAll(resp.Body); err != nil { 1026 if len(b) > 0 { 1027 body = ": "+ string(b) 1028 } 1029 } 1030 return 0, fmt.Errorf("%s%s", resp.Status, body) 1031 } 1032 defer resp.Body.Close() 1033 out, err := os.Create(dest) 1034 if err != nil { 1035 return 0, err 1036 } 1037 defer out.Close() 1038 return io.Copy(out, resp.Body) 1039 } 1040 ` 1041 1042 requestsTmpl = `{{ $funcName := goify (printf "New%s%sRequest" (title .Name) (title .ResourceName)) true }}{{/* 1043 */}}// {{ $funcName }} create the request corresponding to the {{ .Name }} action endpoint of the {{ .ResourceName }} resource. 1044 func (c *Client) {{ $funcName }}(ctx context.Context, path string{{ if .Params }}, {{ .Params }}{{ end }}{{ if .HasPayload }}{{ if .HasMultiContent }}, contentType string{{ end }}{{ end }}) (*http.Request, error) { 1045 {{ if .HasPayload }} var body bytes.Buffer 1046 {{ if .HasMultiContent }} if contentType == "" { 1047 contentType = "*/*" // Use default encoder 1048 } 1049 {{ end }} err := c.Encoder.Encode(payload, &body, {{ if .HasMultiContent }}contentType{{ else }}"*/*"{{ end }}) 1050 if err != nil { 1051 return nil, fmt.Errorf("failed to encode body: %s", err) 1052 } 1053 {{ end }} scheme := c.Scheme 1054 if scheme == "" { 1055 scheme = "{{ .CanonicalScheme }}" 1056 } 1057 u := url.URL{Host: c.Host, Scheme: scheme, Path: path} 1058 {{ if .QueryParams }} values := u.Query() 1059 {{ range .QueryParams }}{{/* 1060 1061 // ARRAY 1062 */}}{{ if .IsArray }} for _, p := range {{ .VarName }} { 1063 {{ if .MustToString }}{{ $tmp := tempvar }} {{ toString "p" $tmp .ElemAttribute }} 1064 values.Add("{{ .Name }}", {{ $tmp }}) 1065 {{ else }} values.Add("{{ .Name }}", {{ .ValueName }}) 1066 {{ end }} } 1067 {{/* 1068 1069 // NON STRING 1070 */}}{{ else if .MustToString }}{{ if .CheckNil }} if {{ .VarName }} != nil { 1071 {{ end }}{{ $tmp := tempvar }} {{ toString .ValueName $tmp .Attribute }} 1072 values.Set("{{ .Name }}", {{ $tmp }}) 1073 {{ if .CheckNil }} } 1074 {{ end }}{{/* 1075 1076 // STRING 1077 */}}{{ else }}{{ if .CheckNil }} if {{ .VarName }} != nil { 1078 {{ end }} values.Set("{{ .Name }}", {{ .ValueName }}) 1079 {{ if .CheckNil }} } 1080 {{ end }}{{ end }}{{ end }} u.RawQuery = values.Encode() 1081 {{ end }}{{ if .HasPayload }} req, err := http.NewRequest({{ $route := index .Routes 0 }}"{{ $route.Verb }}", u.String(), &body) 1082 {{ else }} req, err := http.NewRequest({{ $route := index .Routes 0 }}"{{ $route.Verb }}", u.String(), nil) 1083 {{ end }} if err != nil { 1084 return nil, err 1085 } 1086 {{ if or .HasPayload .Headers }} header := req.Header 1087 {{ if .HasPayload }}{{ if .HasMultiContent }} if contentType == "*/*" { 1088 header.Set("Content-Type", "{{ .DefaultContentType }}") 1089 } else { 1090 header.Set("Content-Type", contentType) 1091 } 1092 {{ else }} header.Set("Content-Type", "{{ .DefaultContentType }}") 1093 {{ end }}{{ end }}{{ range .Headers }}{{ if .CheckNil }} if {{ .VarName }} != nil { 1094 {{ end }}{{ if .MustToString }}{{ $tmp := tempvar }} {{ toString .ValueName $tmp .Attribute }} 1095 header.Set("{{ .Name }}", {{ $tmp }}){{ else }} 1096 header.Set("{{ .Name }}", {{ .ValueName }}) 1097 {{ end }}{{ if .CheckNil }} }{{ end }} 1098 {{ end }}{{ end }}{{ if .Signer }} if c.{{ .Signer }}Signer != nil { 1099 if err := c.{{ .Signer }}Signer.Sign(req); err != nil { 1100 return nil, err 1101 } 1102 } 1103 {{ end }} return req, nil 1104 } 1105 ` 1106 1107 clientTmpl = `// Client is the {{ .API.Name }} service client. 1108 type Client struct { 1109 *goaclient.Client{{range $security := .API.SecuritySchemes }}{{ $signer := signerType $security }}{{ if $signer }} 1110 {{ goify $security.SchemeName true }}Signer goaclient.Signer{{ end }}{{ end }} 1111 Encoder *goa.HTTPEncoder 1112 Decoder *goa.HTTPDecoder 1113 } 1114 1115 // New instantiates the client. 1116 func New(c goaclient.Doer) *Client { 1117 client := &Client{ 1118 Client: goaclient.New(c), 1119 Encoder: goa.NewHTTPEncoder(), 1120 Decoder: goa.NewHTTPDecoder(), 1121 } 1122 1123 {{ if .Encoders }} // Setup encoders and decoders 1124 {{ range .Encoders }}{{/* 1125 */}} client.Encoder.Register({{ .PackageName }}.{{ .Function }}, "{{ joinStrings .MIMETypes "\", \"" }}") 1126 {{ end }}{{ range .Decoders }}{{/* 1127 */}} client.Decoder.Register({{ .PackageName }}.{{ .Function }}, "{{ joinStrings .MIMETypes "\", \"" }}") 1128 {{ end }} 1129 1130 // Setup default encoder and decoder 1131 {{ range .Encoders }}{{ if .Default }}{{/* 1132 */}} client.Encoder.Register({{ .PackageName }}.{{ .Function }}, "*/*") 1133 {{ end }}{{ end }}{{ range .Decoders }}{{ if .Default }}{{/* 1134 */}} client.Decoder.Register({{ .PackageName }}.{{ .Function }}, "*/*") 1135 {{ end }}{{ end }} 1136 {{ end }} return client 1137 } 1138 1139 {{range $security := .API.SecuritySchemes }}{{ $signer := signerType $security }}{{ if $signer }}{{/* 1140 */}}{{ $name := printf "%sSigner" (goify $security.SchemeName true) }}{{/* 1141 */}}// Set{{ $name }} sets the request signer for the {{ $security.SchemeName }} security scheme. 1142 func (c *Client) Set{{ $name }}(signer goaclient.Signer) { 1143 c.{{ $name }} = signer 1144 } 1145 {{ end }}{{ end }} 1146 ` 1147 )