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