github.com/goldeneggg/goa@v1.3.1/goagen/gen_client/cli_generator.go (about) 1 package genclient 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "path" 8 "path/filepath" 9 "sort" 10 "strings" 11 "text/template" 12 13 "github.com/goadesign/goa/design" 14 "github.com/goadesign/goa/goagen/codegen" 15 ) 16 17 func (g *Generator) generateMain(mainFile string, clientPkg, cliPkg string, funcs template.FuncMap) (err error) { 18 var file *codegen.SourceFile 19 file, err = codegen.SourceFileFor(mainFile) 20 if err != nil { 21 return err 22 } 23 defer func() { 24 file.Close() 25 if err == nil { 26 err = file.FormatCode() 27 } 28 }() 29 imports := []*codegen.ImportSpec{ 30 codegen.SimpleImport("encoding/json"), 31 codegen.SimpleImport("fmt"), 32 codegen.SimpleImport("io/ioutil"), 33 codegen.SimpleImport("net/http"), 34 codegen.SimpleImport("os"), 35 codegen.SimpleImport("time"), 36 codegen.SimpleImport(clientPkg), 37 codegen.SimpleImport(cliPkg), 38 codegen.SimpleImport("github.com/spf13/cobra"), 39 codegen.NewImport("goaclient", "github.com/goadesign/goa/client"), 40 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 41 } 42 if err = file.WriteHeader("", "main", imports); err != nil { 43 return err 44 } 45 46 funcs["defaultRouteParams"] = defaultRouteParams 47 funcs["defaultRouteTemplate"] = defaultRouteTemplate 48 funcs["joinNames"] = joinNames 49 funcs["signerSignature"] = signerSignature 50 funcs["signerArgs"] = signerArgs 51 52 g.genfiles = append(g.genfiles, mainFile) 53 version := design.Design.Version 54 if version == "" { 55 version = "0" 56 } 57 58 hasSigners := false 59 hasBasicAuthSigners := false 60 hasAPIKeySigners := false 61 hasTokenSigners := false 62 for _, s := range g.API.SecuritySchemes { 63 if signerType(s) != "" { 64 hasSigners = true 65 switch s.Type { 66 case "basic": 67 hasBasicAuthSigners = true 68 case "apiKey": 69 hasAPIKeySigners = true 70 case "jwt", "oauth2": 71 hasTokenSigners = true 72 } 73 } 74 } 75 76 data := struct { 77 API *design.APIDefinition 78 Version string 79 Package string 80 HasSigners bool 81 HasBasicAuthSigners bool 82 HasAPIKeySigners bool 83 HasTokenSigners bool 84 }{ 85 API: g.API, 86 Version: version, 87 Package: g.Target, 88 HasSigners: hasSigners, 89 HasBasicAuthSigners: hasBasicAuthSigners, 90 HasAPIKeySigners: hasAPIKeySigners, 91 HasTokenSigners: hasTokenSigners, 92 } 93 err = file.ExecuteTemplate("main", mainTmpl, funcs, data) 94 return 95 } 96 97 func (g *Generator) generateCommands(commandsFile string, clientPkg string, funcs template.FuncMap) (err error) { 98 var file *codegen.SourceFile 99 file, err = codegen.SourceFileFor(commandsFile) 100 if err != nil { 101 return err 102 } 103 defer func() { 104 file.Close() 105 if err == nil { 106 err = file.FormatCode() 107 } 108 }() 109 110 funcs["defaultRouteParams"] = defaultRouteParams 111 funcs["defaultRouteTemplate"] = defaultRouteTemplate 112 funcs["joinNames"] = joinNames 113 funcs["joinRouteParams"] = joinRouteParams 114 funcs["routes"] = routes 115 funcs["flagType"] = flagType 116 funcs["cmdFieldType"] = cmdFieldTypeString 117 funcs["formatExample"] = formatExample 118 funcs["shouldAddExample"] = shouldAddExample 119 funcs["kebabCase"] = codegen.KebabCase 120 121 commandTypesTmpl := template.Must(template.New("commandTypes").Funcs(funcs).Parse(commandTypesTmpl)) 122 commandsTmpl := template.Must(template.New("commands").Funcs(funcs).Parse(commandsTmpl)) 123 commandsTmplWS := template.Must(template.New("commandsWS").Funcs(funcs).Parse(commandsTmplWS)) 124 downloadCommandTmpl := template.Must(template.New("download").Funcs(funcs).Parse(downloadCommandTmpl)) 125 registerTmpl := template.Must(template.New("register").Funcs(funcs).Parse(registerTmpl)) 126 127 imports := []*codegen.ImportSpec{ 128 codegen.SimpleImport("encoding/json"), 129 codegen.SimpleImport("fmt"), 130 codegen.SimpleImport("log"), 131 codegen.SimpleImport("net/url"), 132 codegen.SimpleImport("os"), 133 codegen.SimpleImport("path"), 134 codegen.SimpleImport("path/filepath"), 135 codegen.SimpleImport("strings"), 136 codegen.SimpleImport("strconv"), 137 codegen.SimpleImport("time"), 138 codegen.SimpleImport("github.com/goadesign/goa"), 139 codegen.SimpleImport("github.com/spf13/cobra"), 140 codegen.SimpleImport(clientPkg), 141 codegen.SimpleImport("context"), 142 codegen.SimpleImport("golang.org/x/net/websocket"), 143 codegen.NewImport("uuid", "github.com/goadesign/goa/uuid"), 144 } 145 if len(g.API.Resources) > 0 { 146 imports = append(imports, codegen.NewImport("goaclient", "github.com/goadesign/goa/client")) 147 } 148 title := fmt.Sprintf("%s: CLI Commands", g.API.Context()) 149 if err = file.WriteHeader(title, "cli", imports); err != nil { 150 return err 151 } 152 g.genfiles = append(g.genfiles, commandsFile) 153 154 file.Write([]byte("type (\n")) 155 var fs []*design.FileServerDefinition 156 if err = g.API.IterateResources(func(res *design.ResourceDefinition) error { 157 fs = append(fs, res.FileServers...) 158 return res.IterateActions(func(action *design.ActionDefinition) error { 159 return commandTypesTmpl.Execute(file, action) 160 }) 161 }); err != nil { 162 return err 163 } 164 if len(fs) > 0 { 165 file.Write([]byte(downloadCommandType)) 166 } 167 file.Write([]byte(")\n\n")) 168 169 actions := make(map[string][]*design.ActionDefinition) 170 hasDownloads := false 171 g.API.IterateResources(func(res *design.ResourceDefinition) error { 172 if len(res.FileServers) > 0 { 173 hasDownloads = true 174 } 175 return res.IterateActions(func(action *design.ActionDefinition) error { 176 name := codegen.Goify(action.Name, false) 177 if as, ok := actions[name]; ok { 178 actions[name] = append(as, action) 179 } else { 180 actions[name] = []*design.ActionDefinition{action} 181 } 182 return nil 183 }) 184 }) 185 data := struct { 186 Actions map[string][]*design.ActionDefinition 187 Package string 188 HasDownloads bool 189 }{ 190 Actions: actions, 191 Package: g.Target, 192 HasDownloads: hasDownloads, 193 } 194 if err = file.ExecuteTemplate("registerCmds", registerCmdsT, funcs, data); err != nil { 195 return err 196 } 197 198 var fsdata []map[string]interface{} 199 g.API.IterateResources(func(res *design.ResourceDefinition) error { 200 if res.FileServers != nil { 201 res.IterateFileServers(func(fs *design.FileServerDefinition) error { 202 wcs := design.ExtractWildcards(fs.RequestPath) 203 isDir := len(wcs) > 0 204 var reqDir, filename string 205 if isDir { 206 reqDir, _ = path.Split(fs.RequestPath) 207 } else { 208 _, filename = filepath.Split(fs.FilePath) 209 } 210 fsdata = append(fsdata, map[string]interface{}{ 211 "IsDir": isDir, 212 "RequestPath": fs.RequestPath, 213 "FilePath": fs.FilePath, 214 "FileName": filename, 215 "Name": g.fileServerMethod(fs), 216 "RequestDir": reqDir, 217 }) 218 return nil 219 }) 220 } 221 return nil 222 }) 223 if fsdata != nil { 224 data := struct { 225 Package string 226 FileServers []map[string]interface{} 227 }{ 228 Package: g.Target, 229 FileServers: fsdata, 230 } 231 if err = downloadCommandTmpl.Execute(file, data); err != nil { 232 return err 233 } 234 } 235 err = g.API.IterateResources(func(res *design.ResourceDefinition) error { 236 return res.IterateActions(func(action *design.ActionDefinition) error { 237 data := map[string]interface{}{ 238 "Action": action, 239 "Resource": action.Parent, 240 "Package": g.Target, 241 "HasMultiContent": len(g.API.Consumes) > 1, 242 } 243 var err error 244 if action.WebSocket() { 245 err = commandsTmplWS.Execute(file, data) 246 } else { 247 err = commandsTmpl.Execute(file, data) 248 249 } 250 if err != nil { 251 return err 252 } 253 err = registerTmpl.Execute(file, data) 254 return err 255 }) 256 }) 257 return 258 } 259 260 // defaultRouteParams returns the parameters needed to build the first route of the given action. 261 func defaultRouteParams(a *design.ActionDefinition) *design.AttributeDefinition { 262 r := a.Routes[0] 263 params := r.Params() 264 o := make(design.Object, len(params)) 265 nz := make(map[string]bool, len(params)) 266 pparams := a.PathParams() 267 for _, p := range params { 268 o[p] = pparams.Type.ToObject()[p] 269 nz[p] = true 270 } 271 return &design.AttributeDefinition{Type: o, NonZeroAttributes: nz} 272 } 273 274 // produces a fmt template to render the first route of action. 275 func defaultRouteTemplate(a *design.ActionDefinition) string { 276 return design.WildcardRegex.ReplaceAllLiteralString(a.Routes[0].FullPath(), "/%v") 277 } 278 279 // return a ',' joined list of Params as a reference to cmd.XFieldName 280 // ordered by the required first rules. 281 func joinRouteParams(action *design.ActionDefinition, att *design.AttributeDefinition) string { 282 var ( 283 params = action.Routes[0].Params() 284 elems = make([]string, len(params)) 285 ) 286 for i, p := range params { 287 patt, ok := att.Type.ToObject()[p] 288 if !ok { 289 continue 290 } 291 pf := "cmd.%s" 292 if patt.Type.Kind() == design.StringKind { 293 pf = "url.QueryEscape(cmd.%s)" 294 } 295 field := fmt.Sprintf(pf, codegen.Goify(p, true)) 296 elems[i] = field 297 } 298 return strings.Join(elems, ", ") 299 } 300 301 // joinNames is a code generation helper function that generates a string built from concatenating 302 // the keys of the given attribute type (assuming it's an object). 303 func joinNames(useNil bool, atts ...*design.AttributeDefinition) string { 304 var elems []string 305 for _, att := range atts { 306 if att == nil { 307 continue 308 } 309 obj := att.Type.ToObject() 310 var names, optNames []string 311 312 keys := make([]string, len(obj)) 313 i := 0 314 for n := range obj { 315 keys[i] = n 316 i++ 317 } 318 sort.Strings(keys) 319 320 for _, n := range keys { 321 a := obj[n] 322 field := fmt.Sprintf("cmd.%s", codegen.Goify(n, true)) 323 if !a.Type.IsArray() && !att.IsRequired(n) && !att.IsNonZero(n) { 324 if useNil { 325 field = flagTypeVal(a, n, field) 326 } else { 327 field = "&" + field 328 } 329 } else if a.Type.IsArray() { 330 field = flagTypeArrayVal(a, field) 331 } else { 332 field = flagRequiredTypeVal(a, field) 333 } 334 if att.IsRequired(n) { 335 names = append(names, field) 336 } else { 337 optNames = append(optNames, field) 338 } 339 } 340 elems = append(elems, names...) 341 elems = append(elems, optNames...) 342 } 343 return strings.Join(elems, ", ") 344 } 345 346 // resolve non required, non array Param/QueryParam for access via CII flags. 347 // Some types need convertion from string to 'Type' before calling rich client Commands. 348 func flagTypeVal(a *design.AttributeDefinition, key string, field string) string { 349 switch a.Type { 350 case design.Integer: 351 return `intFlagVal("` + key + `", ` + field + ")" 352 case design.String: 353 return `stringFlagVal("` + key + `", ` + field + ")" 354 case design.Number, design.Boolean, design.UUID, design.DateTime, design.Any: 355 return "%s" 356 default: 357 return "&" + field 358 } 359 } 360 361 // resolve required Param/QueryParam for access via CII flags. 362 // Required Params are not generated as pointers 363 // Special types like Number/UUID need to be converted from String 364 // %s maps to specialTypeResult.Temps 365 func flagRequiredTypeVal(a *design.AttributeDefinition, field string) string { 366 switch a.Type { 367 case design.Number, design.Boolean, design.UUID, design.DateTime, design.Any: 368 return "*%s" 369 default: 370 return field 371 } 372 } 373 374 // resolve required Param/QueryParam for access via CII flags. 375 // Special types like Number/UUID need to be converted from String 376 // %s maps to specialTypeResult.Temps 377 func flagTypeArrayVal(a *design.AttributeDefinition, field string) string { 378 switch a.Type.ToArray().ElemType.Type { 379 case design.Number, design.Boolean, design.UUID, design.DateTime, design.Any: 380 return "%s" 381 } 382 return field 383 } 384 385 // format a stirng format("%s") with the given vars as argument 386 func format(format string, vars []string) string { 387 new := make([]interface{}, len(vars)) 388 for i, v := range vars { 389 new[i] = v 390 } 391 return fmt.Sprintf(format, new...) 392 } 393 394 // temp structure to describe the relationship between XParams 395 // and their tmp var as generated in the Output. See handleSpecialTypes 396 type specialTypeResult struct { 397 Temps []string 398 Output string 399 } 400 401 // generate the relation and output of specially typed Params that need 402 // custom convertion from String Flags to Rich objects in Client action 403 // 404 // tmp, err := uuidVal(cmd.X) 405 // if err != nil { 406 // goa.LogError(ctx, "argument parse failed", "err", err) 407 // return err 408 // } 409 // resp, err := c.ShowX(ctx, path, tmp) 410 // 411 func handleSpecialTypes(atts ...*design.AttributeDefinition) specialTypeResult { 412 result := specialTypeResult{} 413 for _, att := range atts { 414 if att == nil { 415 continue 416 } 417 obj := att.Type.ToObject() 418 var names, optNames []string 419 420 keys := make([]string, len(obj)) 421 i := 0 422 for n := range obj { 423 keys[i] = n 424 i++ 425 } 426 sort.Strings(keys) 427 for _, n := range keys { 428 a := obj[n] 429 field := fmt.Sprintf("cmd.%s", codegen.Goify(n, true)) 430 typ := cmdFieldType(a.Type, true) 431 var typeHandler, nilVal string 432 if !a.Type.IsArray() { 433 nilVal = `""` 434 switch a.Type { 435 case design.Number: 436 typeHandler = "float64Val" 437 case design.Boolean: 438 typeHandler = "boolVal" 439 case design.UUID: 440 typeHandler = "uuidVal" 441 case design.DateTime: 442 typeHandler = "timeVal" 443 case design.Any: 444 typeHandler = "jsonVal" 445 } 446 447 } else if a.Type.IsArray() { 448 nilVal = "nil" 449 switch a.Type.ToArray().ElemType.Type { 450 case design.Number: 451 typeHandler = "float64Array" 452 case design.Boolean: 453 typeHandler = "boolArray" 454 case design.UUID: 455 typeHandler = "uuidArray" 456 case design.DateTime: 457 typeHandler = "timeArray" 458 case design.Any: 459 typeHandler = "jsonArray" 460 } 461 } 462 if typeHandler != "" { 463 tmpVar := codegen.Tempvar() 464 if att.IsRequired(n) { 465 names = append(names, tmpVar) 466 } else { 467 optNames = append(optNames, tmpVar) 468 } 469 470 //result.Temps = append(result.Temps, tmpVar) 471 result.Output += fmt.Sprintf(` 472 var %s %s 473 if %s != %s { 474 var err error 475 %s, err = %s(%s) 476 if err != nil { 477 goa.LogError(ctx, "failed to parse flag into %s value", "flag", "--%s", "err", err) 478 return err 479 } 480 }`, tmpVar, typ, field, nilVal, tmpVar, typeHandler, field, typ, n) 481 if att.IsRequired(n) { 482 result.Output += fmt.Sprintf(` 483 if %s == nil { 484 goa.LogError(ctx, "required flag is missing", "flag", "--%s") 485 return fmt.Errorf("required flag %s is missing") 486 }`, tmpVar, n, n) 487 } 488 } 489 } 490 result.Temps = append(result.Temps, names...) 491 result.Temps = append(result.Temps, optNames...) 492 } 493 return result 494 } 495 496 // routes create the action command "Use" suffix. 497 func routes(action *design.ActionDefinition) string { 498 var buf bytes.Buffer 499 routes := action.Routes 500 buf.WriteRune('[') 501 if len(routes) > 1 { 502 buf.WriteRune('(') 503 } 504 paths := make([]string, len(routes)) 505 for i, r := range routes { 506 path := r.FullPath() 507 matches := design.WildcardRegex.FindAllStringSubmatch(path, -1) 508 for _, match := range matches { 509 paramName := match[1] 510 path = strings.Replace(path, ":"+paramName, strings.ToUpper(paramName), 1) 511 } 512 paths[i] = fmt.Sprintf("%q", path) 513 } 514 buf.WriteString(strings.Join(paths, "|")) 515 if len(routes) > 1 { 516 buf.WriteRune(')') 517 } 518 buf.WriteRune(']') 519 return buf.String() 520 } 521 522 // signerSignature returns the callee signature for the signer factory function for the given security 523 // scheme. 524 func signerSignature(sec *design.SecuritySchemeDefinition) string { 525 switch sec.Type { 526 case "basic": 527 return "user, pass string" 528 case "apiKey": 529 return "key, format string" 530 case "jwt": 531 return "source goaclient.TokenSource" 532 case "oauth2": 533 return "source goaclient.TokenSource" 534 default: 535 return "" 536 } 537 } 538 539 // signerArgs returns the caller signature for the signer factory function for the given security 540 // scheme. 541 func signerArgs(sec *design.SecuritySchemeDefinition) string { 542 switch sec.Type { 543 case "basic": 544 return "user, pass" 545 case "apiKey": 546 return "key, format" 547 case "jwt": 548 return "source" 549 case "oauth2": 550 return "source" 551 default: 552 return "" 553 } 554 } 555 556 // flagType returns the flag type for the given (basic type) attribute definition. 557 func flagType(att *design.AttributeDefinition) string { 558 switch att.Type.Kind() { 559 case design.IntegerKind: 560 return "Int" 561 case design.NumberKind: 562 return "String" 563 case design.BooleanKind: 564 return "String" 565 case design.StringKind: 566 return "String" 567 case design.DateTimeKind: 568 return "String" 569 case design.UUIDKind: 570 return "String" 571 case design.AnyKind: 572 return "String" 573 case design.ArrayKind: 574 switch att.Type.ToArray().ElemType.Type.Kind() { 575 case design.NumberKind: 576 return "StringSlice" 577 case design.BooleanKind: 578 return "StringSlice" 579 default: 580 return flagType(att.Type.(*design.Array).ElemType) + "Slice" 581 } 582 case design.UserTypeKind: 583 return flagType(att.Type.(*design.UserTypeDefinition).AttributeDefinition) 584 case design.MediaTypeKind: 585 return flagType(att.Type.(*design.MediaTypeDefinition).AttributeDefinition) 586 default: 587 panic("invalid flag attribute type " + att.Type.Name()) 588 } 589 } 590 591 func shouldAddExample(ut *design.UserTypeDefinition) bool { 592 if ut == nil { 593 return false 594 } 595 return ut.Example != nil 596 } 597 598 func formatExample(example interface{}) string { 599 if example == nil { 600 return "" 601 } 602 data, _ := json.MarshalIndent(example, "", " ") 603 return string(data) 604 } 605 606 const mainTmpl = ` 607 func main() { 608 // Create command line parser 609 app := &cobra.Command{ 610 Use: "{{ .API.Name }}-cli", 611 Short: ` + "`" + `CLI client for the {{ .API.Name }} service{{ if .API.Docs }} ({{ escapeBackticks .API.Docs.URL }}){{ end }}` + "`" + `, 612 } 613 614 // Create client struct 615 httpClient := newHTTPClient() 616 c := {{ .Package }}.New(goaclient.HTTPClientDoer(httpClient)) 617 618 // Register global flags 619 app.PersistentFlags().StringVarP(&c.Scheme, "scheme", "s", "", "Set the requests scheme") 620 app.PersistentFlags().StringVarP(&c.Host, "host", "H", "{{ .API.Host }}", "API hostname") 621 app.PersistentFlags().DurationVarP(&httpClient.Timeout, "timeout", "t", time.Duration(20) * time.Second, "Set the request timeout") 622 app.PersistentFlags().BoolVar(&c.Dump, "dump", false, "Dump HTTP request and response.") 623 624 {{ if .HasSigners }} // Register signer flags 625 {{ if .HasBasicAuthSigners }} var user, pass string 626 app.PersistentFlags().StringVar(&user, "user", "", "Username used for authentication") 627 app.PersistentFlags().StringVar(&pass, "pass", "", "Password used for authentication") 628 {{ end }}{{ if .HasAPIKeySigners }} var key, format string 629 app.PersistentFlags().StringVar(&key, "key", "", "API key used for authentication") 630 app.PersistentFlags().StringVar(&format, "format", "Bearer %s", "Format used to create auth header or query from key") 631 {{ end }}{{ if .HasTokenSigners }} var token, typ string 632 app.PersistentFlags().StringVar(&token, "token", "", "Token used for authentication") 633 app.PersistentFlags().StringVar(&typ, "token-type", "Bearer", "Token type used for authentication") 634 {{ end }} 635 // Parse flags and setup signers 636 app.ParseFlags(os.Args) 637 {{ if .HasTokenSigners }} source := &goaclient.StaticTokenSource{ 638 StaticToken: &goaclient.StaticToken{Type: typ, Value: token}, 639 } 640 {{ end }}{{ end }}{{ range $security := .API.SecuritySchemes }}{{ $signer := signerType $security }}{{ if $signer }}{{/* 641 */}} {{ goify $security.SchemeName false }}Signer := new{{ goify $security.SchemeName true }}Signer({{ signerArgs $security }}){{ end }} 642 {{ end }} 643 644 // Initialize API client 645 {{ range $security := .API.SecuritySchemes }}{{ $signer := signerType $security }}{{ if $signer }}{{/* 646 */}} c.Set{{ goify $security.SchemeName true }}Signer({{ goify $security.SchemeName false }}Signer) 647 {{ end }}{{ end }} c.UserAgent = "{{ .API.Name }}-cli/{{ .Version }}" 648 649 // Register API commands 650 cli.RegisterCommands(app, c) 651 652 // Execute! 653 if err := app.Execute(); err != nil { 654 fmt.Fprintf(os.Stderr, err.Error()) 655 os.Exit(-1) 656 } 657 } 658 659 // newHTTPClient returns the HTTP client used by the API client to make requests to the service. 660 func newHTTPClient() *http.Client { 661 // TBD: Change as needed (e.g. to use a different transport to control redirection policy or 662 // disable cert validation or...) 663 return http.DefaultClient 664 } 665 666 {{ range $security := .API.SecuritySchemes }}{{ $signer := signerType $security }}{{ if $signer }} 667 // new{{ goify $security.SchemeName true }}Signer returns the request signer used for authenticating 668 // against the {{ $security.SchemeName }} security scheme. 669 func new{{ goify $security.SchemeName true }}Signer({{ signerSignature $security }}) goaclient.Signer { 670 {{ if eq .Type "basic" }} return &goaclient.BasicSigner{ 671 Username: user, 672 Password: pass, 673 } 674 {{ else if eq .Type "apiKey" }} return &goaclient.APIKeySigner{ 675 SignQuery: {{ if eq $security.In "query" }}true{{ else }}false{{ end }}, 676 KeyName: "{{ $security.Name }}", 677 KeyValue: key, 678 Format: {{ if eq $security.In "query" }}"%s"{{ else }}format{{ end }}, 679 } 680 {{ else if eq .Type "jwt" }} return &goaclient.JWTSigner{ 681 TokenSource: source, 682 } 683 {{ else if eq .Type "oauth2" }} return &goaclient.OAuth2Signer{ 684 TokenSource: source, 685 } 686 {{ end }} 687 } 688 {{ end }}{{ end }} 689 ` 690 691 const commandTypesTmpl = `{{ $cmdName := goify (printf "%s%sCommand" .Name (title (kebabCase .Parent.Name))) true }} // {{ $cmdName }} is the command line data structure for the {{ .Name }} action of {{ .Parent.Name }} 692 {{ $cmdName }} struct { 693 {{ if .Payload }} Payload string 694 ContentType string 695 {{ end }}{{ $params := defaultRouteParams . }}{{ if $params }}{{ range $name, $att := $params.Type.ToObject }}{{ if $att.Description }} {{ multiComment $att.Description }} 696 {{ end }} {{ goify $name true }} {{ cmdFieldType $att.Type false }} 697 {{ end }}{{ end }}{{ $params := .QueryParams }}{{ if $params }}{{ range $name, $att := $params.Type.ToObject }}{{ if $att.Description }} {{ multiComment $att.Description }} 698 {{ end }} {{ goify $name true }} {{ cmdFieldType $att.Type false}} 699 {{ end }}{{ end }}{{ $headers := .Headers }}{{ if $headers }}{{ range $name, $att := $headers.Type.ToObject }}{{ if $att.Description }} {{ multiComment $att.Description }} 700 {{ end }} {{ goify $name true }} {{ cmdFieldType $att.Type false}} 701 {{ end }}{{ end }} PrettyPrint bool 702 } 703 704 ` 705 706 const downloadCommandType = `// DownloadCommand is the command line data structure for the download command. 707 DownloadCommand struct { 708 // OutFile is the path to the download output file. 709 OutFile string 710 } 711 712 ` 713 714 const commandsTmplWS = ` 715 {{ $cmdName := goify (printf "%s%sCommand" .Action.Name (title (kebabCase .Resource.Name))) true }}// Run establishes a websocket connection for the {{ $cmdName }} command. 716 func (cmd *{{ $cmdName }}) Run(c *{{ .Package }}.Client, args []string) error { 717 var path string 718 if len(args) > 0 { 719 path = args[0] 720 } else { 721 {{ $default := defaultPath .Action }}{{ if $default }} path = "{{ $default }}" 722 {{ else }}{{ $pparams := defaultRouteParams .Action }} path = fmt.Sprintf({{ printf "%q" (defaultRouteTemplate .Action)}}, {{ joinRouteParams .Action $pparams }}) 723 {{ end }} } 724 logger := goa.NewLogger(log.New(os.Stderr, "", log.LstdFlags)) 725 ctx := goa.WithLogger(context.Background(), logger){{ $specialTypeResult := handleSpecialTypes .Action.QueryParams .Action.Headers }}{{ $specialTypeResult.Output }} 726 ws, err := c.{{ goify (printf "%s%s" .Action.Name (title .Resource.Name)) true }}(ctx, path{{/* 727 */}}{{ $params := joinNames true .Action.QueryParams .Action.Headers }}{{ if $params }}, {{ format $params $specialTypeResult.Temps }}{{ end }}) 728 if err != nil { 729 goa.LogError(ctx, "failed", "err", err) 730 return err 731 } 732 go goaclient.WSWrite(ws) 733 goaclient.WSRead(ws) 734 735 return nil 736 } 737 ` 738 739 const downloadCommandTmpl = ` 740 // Run downloads files with given paths. 741 func (cmd *DownloadCommand) Run(c *{{ .Package }}.Client, args []string) error { 742 var ( 743 fnf func (context.Context, string) (int64, error) 744 fnd func (context.Context, string, string) (int64, error) 745 746 rpath = args[0] 747 outfile = cmd.OutFile 748 logger = goa.NewLogger(log.New(os.Stderr, "", log.LstdFlags)) 749 ctx = goa.WithLogger(context.Background(), logger) 750 err error 751 ) 752 753 if rpath[0] != '/' { 754 rpath = "/" + rpath 755 } 756 {{ range .FileServers }}{{ if not .IsDir }} if rpath == "{{ .RequestPath }}" { 757 fnf = c.{{ .Name }} 758 if outfile == "" { 759 outfile = "{{ .FileName }}" 760 } 761 goto found 762 } 763 {{ end }}{{ end }}{{ range .FileServers }}{{ if .IsDir }} if strings.HasPrefix(rpath, "{{ .RequestDir }}") { 764 fnd = c.{{ .Name }} 765 rpath = rpath[{{ len .RequestDir }}:] 766 if outfile == "" { 767 _, outfile = path.Split(rpath) 768 } 769 goto found 770 } 771 {{ end }}{{ end }} return fmt.Errorf("don't know how to download %s", rpath) 772 found: 773 ctx = goa.WithLogContext(ctx, "file", outfile) 774 if fnf != nil { 775 _, err = fnf(ctx, outfile) 776 } else { 777 _, err = fnd(ctx, rpath, outfile) 778 } 779 if err != nil { 780 goa.LogError(ctx, "failed", "err", err) 781 return err 782 } 783 784 return nil 785 } 786 ` 787 788 const registerTmpl = `{{ $cmdName := goify (printf "%s%sCommand" .Action.Name (title (kebabCase .Resource.Name))) true }}// RegisterFlags registers the command flags with the command line. 789 func (cmd *{{ $cmdName }}) RegisterFlags(cc *cobra.Command, c *{{ .Package }}.Client) { 790 {{ if .Action.Payload }} cc.Flags().StringVar(&cmd.Payload, "payload", "", "Request body encoded in JSON") 791 cc.Flags().StringVar(&cmd.ContentType, "content", "", "Request content type override, e.g. 'application/x-www-form-urlencoded'") 792 {{ end }}{{ $pparams := defaultRouteParams .Action }}{{ if $pparams }}{{ range $pname, $pparam := $pparams.Type.ToObject }}{{ $tmp := goify $pname false }}{{/* 793 */}}{{ if not $pparam.DefaultValue }} var {{ $tmp }} {{ cmdFieldType $pparam.Type false }} 794 {{ end }} cc.Flags().{{ flagType $pparam }}Var(&cmd.{{ goify $pname true }}, "{{ $pname }}", {{/* 795 */}}{{ if $pparam.DefaultValue }}{{ printf "%#v" $pparam.DefaultValue }}{{ else }}{{ $tmp }}{{ end }}, ` + "`" + `{{ escapeBackticks $pparam.Description }}` + "`" + `) 796 {{ end }}{{ end }}{{ $params := .Action.QueryParams }}{{ if $params }}{{ range $name, $param := $params.Type.ToObject }}{{ $tmp := goify $name false }}{{/* 797 */}}{{ if not $param.DefaultValue }} var {{ $tmp }} {{ cmdFieldType $param.Type false }} 798 {{ end }} cc.Flags().{{ flagType $param }}Var(&cmd.{{ goify $name true }}, "{{ $name }}", {{/* 799 */}}{{ if $param.DefaultValue }}{{ printf "%#v" $param.DefaultValue }}{{ else }}{{ $tmp }}{{ end }}, ` + "`" + `{{ escapeBackticks $param.Description }}` + "`" + `) 800 {{ end }}{{ end }}{{ $headers := .Action.Headers }}{{ if $headers }}{{ range $name, $header := $headers.Type.ToObject }}{{/* 801 */}} cc.Flags().StringVar(&cmd.{{ goify $name true }}, "{{ $name }}", {{/* 802 */}}{{ if $header.DefaultValue }}{{ printf "%q" $header.DefaultValue }}{{ else }}""{{ end }}, ` + "`" + `{{ escapeBackticks $header.Description }}` + "`" + `) 803 {{ end }}{{ end }}}` 804 805 const commandsTmpl = ` 806 {{ $cmdName := goify (printf "%s%sCommand" .Action.Name (title (kebabCase .Resource.Name))) true }}// Run makes the HTTP request corresponding to the {{ $cmdName }} command. 807 func (cmd *{{ $cmdName }}) Run(c *{{ .Package }}.Client, args []string) error { 808 var path string 809 if len(args) > 0 { 810 path = args[0] 811 } else { 812 {{ $default := defaultPath .Action }}{{ if $default }} path = "{{ $default }}" 813 {{ else }}{{ $pparams := defaultRouteParams .Action }} path = fmt.Sprintf({{ printf "%q" (defaultRouteTemplate .Action) }}, {{ joinRouteParams .Action $pparams }}) 814 {{ end }} } 815 {{ if .Action.Payload }}var payload {{ gotyperefext .Action.Payload 2 .Package }} 816 if cmd.Payload != "" { 817 err := json.Unmarshal([]byte(cmd.Payload), &payload) 818 if err != nil { 819 {{ if eq .Action.Payload.Type.Kind 4 }} payload = cmd.Payload 820 {{ else }} return fmt.Errorf("failed to deserialize payload: %s", err) 821 {{ end }} } 822 } 823 {{ end }} logger := goa.NewLogger(log.New(os.Stderr, "", log.LstdFlags)) 824 ctx := goa.WithLogger(context.Background(), logger){{ $specialTypeResult := handleSpecialTypes .Action.QueryParams .Action.Headers }}{{ $specialTypeResult.Output }} 825 resp, err := c.{{ goify (printf "%s%s" .Action.Name (title .Resource.Name)) true }}(ctx, path{{ if .Action.Payload }}, {{/* 826 */}}{{ if or .Action.Payload.Type.IsObject .Action.Payload.IsPrimitive }}&{{ end }}payload{{ else }}{{ end }}{{/* 827 */}}{{ $params := joinNames true .Action.QueryParams .Action.Headers }}{{ if $params }}, {{ format $params $specialTypeResult.Temps }}{{ end }}{{/* 828 */}}{{ if and .Action.Payload .HasMultiContent }}, cmd.ContentType{{ end }}) 829 if err != nil { 830 goa.LogError(ctx, "failed", "err", err) 831 return err 832 } 833 834 goaclient.HandleResponse(c.Client, resp, cmd.PrettyPrint) 835 return nil 836 } 837 ` 838 839 // Takes map[string][]*design.ActionDefinition as input 840 const registerCmdsT = `// RegisterCommands registers the resource action CLI commands. 841 func RegisterCommands(app *cobra.Command, c *{{ .Package }}.Client) { 842 {{ with .Actions }}{{ if gt (len .) 0 }} var command, sub *cobra.Command 843 {{ end }}{{ range $name, $actions := . }} command = &cobra.Command{ 844 Use: "{{ kebabCase $name }}", 845 Short: ` + "`" + `{{ if eq (len $actions) 1 }}{{ $a := index $actions 0 }}{{ escapeBackticks $a.Description }}{{ else }}{{ $name }} action{{ end }}` + "`" + `, 846 } 847 {{ range $action := $actions }}{{ $cmdName := goify (printf "%s%sCommand" $action.Name (title (kebabCase $action.Parent.Name))) true }}{{/* 848 */}}{{ $tmp := tempvar }} {{ $tmp }} := new({{ $cmdName }}) 849 sub = &cobra.Command{ 850 Use: ` + "`" + `{{ kebabCase $action.Parent.Name }} {{ routes $action }}` + "`" + `, 851 Short: ` + "`" + `{{ escapeBackticks $action.Parent.Description }}` + "`" + `,{{ if shouldAddExample $action.Payload }} 852 Long: ` + "`" + `{{ escapeBackticks $action.Parent.Description }} 853 854 Payload example: 855 856 {{ formatExample $action.Payload.Example }}` + "`" + `,{{ end }} 857 RunE: func(cmd *cobra.Command, args []string) error { return {{ $tmp }}.Run(c, args) }, 858 } 859 {{ $tmp }}.RegisterFlags(sub, c) 860 sub.PersistentFlags().BoolVar(&{{ $tmp }}.PrettyPrint, "pp", false, "Pretty print response body") 861 command.AddCommand(sub) 862 {{ end }}app.AddCommand(command) 863 {{ end }}{{ end }}{{ if .HasDownloads }} 864 dl := new(DownloadCommand) 865 dlc := &cobra.Command{ 866 Use: "download [PATH]", 867 Short: "Download file with given path", 868 RunE: func(cmd *cobra.Command, args []string) error { 869 return dl.Run(c, args) 870 }, 871 } 872 dlc.Flags().StringVar(&dl.OutFile, "out", "", "Output file") 873 app.AddCommand(dlc) 874 {{ end }}} 875 876 func intFlagVal(name string, parsed int) *int { 877 if hasFlag(name) { 878 return &parsed 879 } 880 return nil 881 } 882 883 func float64FlagVal(name string, parsed float64) *float64 { 884 if hasFlag(name) { 885 return &parsed 886 } 887 return nil 888 } 889 890 func boolFlagVal(name string, parsed bool) *bool { 891 if hasFlag(name) { 892 return &parsed 893 } 894 return nil 895 } 896 897 func stringFlagVal(name string, parsed string) *string { 898 if hasFlag(name) { 899 return &parsed 900 } 901 return nil 902 } 903 904 func hasFlag(name string) bool { 905 for _, arg := range os.Args[1:] { 906 if strings.HasPrefix(arg, "--"+name) { 907 return true 908 } 909 } 910 return false 911 } 912 913 func jsonVal(val string) (*interface{}, error) { 914 var t interface{} 915 err := json.Unmarshal([]byte(val), &t) 916 if err != nil { 917 return nil, err 918 } 919 return &t, nil 920 } 921 922 func jsonArray(ins []string) ([]interface{}, error) { 923 if ins == nil { 924 return nil, nil 925 } 926 var vals []interface{} 927 for _, id := range ins { 928 val, err := jsonVal(id) 929 if err != nil { 930 return nil, err 931 } 932 vals = append(vals, val) 933 } 934 return vals, nil 935 } 936 937 func timeVal(val string) (*time.Time, error) { 938 t, err := time.Parse(time.RFC3339, val) 939 if err != nil { 940 return nil, err 941 } 942 return &t, nil 943 } 944 945 func timeArray(ins []string) ([]time.Time, error) { 946 if ins == nil { 947 return nil, nil 948 } 949 var vals []time.Time 950 for _, id := range ins { 951 val, err := timeVal(id) 952 if err != nil { 953 return nil, err 954 } 955 vals = append(vals, *val) 956 } 957 return vals, nil 958 } 959 960 func uuidVal(val string) (*uuid.UUID, error) { 961 t, err := uuid.FromString(val) 962 if err != nil { 963 return nil, err 964 } 965 return &t, nil 966 } 967 968 func uuidArray(ins []string) ([]uuid.UUID, error) { 969 if ins == nil { 970 return nil, nil 971 } 972 var vals []uuid.UUID 973 for _, id := range ins { 974 val, err := uuidVal(id) 975 if err != nil { 976 return nil, err 977 } 978 vals = append(vals, *val) 979 } 980 return vals, nil 981 } 982 983 func float64Val(val string) (*float64, error) { 984 t, err := strconv.ParseFloat(val, 64) 985 if err != nil { 986 return nil, err 987 } 988 return &t, nil 989 } 990 991 func float64Array(ins []string) ([]float64, error) { 992 if ins == nil { 993 return nil, nil 994 } 995 var vals []float64 996 for _, id := range ins { 997 val, err := float64Val(id) 998 if err != nil { 999 return nil, err 1000 } 1001 vals = append(vals, *val) 1002 } 1003 return vals, nil 1004 } 1005 1006 func boolVal(val string) (*bool, error) { 1007 t, err := strconv.ParseBool(val) 1008 if err != nil { 1009 return nil, err 1010 } 1011 return &t, nil 1012 } 1013 1014 func boolArray(ins []string) ([]bool, error) { 1015 if ins == nil { 1016 return nil, nil 1017 } 1018 var vals []bool 1019 for _, id := range ins { 1020 val, err := boolVal(id) 1021 if err != nil { 1022 return nil, err 1023 } 1024 vals = append(vals, *val) 1025 } 1026 return vals, nil 1027 }`