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