github.com/unionj-cloud/go-doudou@v1.3.8-0.20221011095552-0088008e5b31/cmd/internal/openapi/v3/codegen/client/go.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "github.com/go-resty/resty/v2" 8 "github.com/iancoleman/strcase" 9 "github.com/pkg/errors" 10 "github.com/sirupsen/logrus" 11 "github.com/unionj-cloud/go-doudou/cmd/internal/astutils" 12 "github.com/unionj-cloud/go-doudou/toolkit/copier" 13 v3 "github.com/unionj-cloud/go-doudou/toolkit/openapi/v3" 14 "github.com/unionj-cloud/go-doudou/toolkit/sliceutils" 15 "github.com/unionj-cloud/go-doudou/toolkit/stringutils" 16 "io/ioutil" 17 "os" 18 "path/filepath" 19 "regexp" 20 "strings" 21 "text/template" 22 ) 23 24 var votmpl = `package {{.Pkg}} 25 26 {{- range $k, $v := .Schemas }} 27 {{ toComment $v.Description ($k | toCamel)}} 28 type {{$k | toCamel}} struct { 29 {{- range $pk, $pv := $v.Properties }} 30 {{ $pv.Description | toComment }} 31 {{- if stringContains $v.Required $pk }} 32 // required 33 {{ $pk | toCamel}} {{$pv | toGoType }} ` + "`" + `json:"{{$pk}}{{if $.Omit}},omitempty{{end}}" url:"{{$pk}}"` + "`" + ` 34 {{- else }} 35 {{ $pk | toCamel}} {{$pv | toOptionalGoType }} ` + "`" + `json:"{{$pk}}{{if $.Omit}},omitempty{{end}}" url:"{{$pk}}"` + "`" + ` 36 {{- end }} 37 {{- end }} 38 } 39 {{- end }} 40 ` 41 42 var httptmpl = `package {{.Pkg}} 43 44 import ( 45 "context" 46 "encoding/json" 47 "github.com/go-resty/resty/v2" 48 "github.com/pkg/errors" 49 "github.com/opentracing-contrib/go-stdlib/nethttp" 50 "github.com/opentracing/opentracing-go" 51 "github.com/unionj-cloud/go-doudou/framework/registry" 52 _querystring "github.com/google/go-querystring/query" 53 "github.com/unionj-cloud/go-doudou/toolkit/fileutils" 54 "github.com/unionj-cloud/go-doudou/toolkit/stringutils" 55 ddhttp "github.com/unionj-cloud/go-doudou/framework/http" 56 v3 "github.com/unionj-cloud/go-doudou/toolkit/openapi/v3" 57 "io" 58 "mime/multipart" 59 "net/url" 60 "os" 61 "path/filepath" 62 "strings" 63 ) 64 65 type {{.Meta.Name}}Client struct { 66 provider registry.IServiceProvider 67 client *resty.Client 68 rootPath string 69 } 70 71 func (receiver *{{.Meta.Name}}Client) SetRootPath(rootPath string) { 72 receiver.rootPath = rootPath 73 } 74 75 func (receiver *{{.Meta.Name}}Client) SetProvider(provider registry.IServiceProvider) { 76 receiver.provider = provider 77 } 78 79 func (receiver *{{.Meta.Name}}Client) SetClient(client *resty.Client) { 80 receiver.client = client 81 } 82 83 {{- range $m := .Meta.Methods }} 84 {{- range $i, $c := $m.Comments }} 85 {{- if eq $i 0}} 86 // {{$m.Name}} {{$c}} 87 {{- else}} 88 // {{$c}} 89 {{- end}} 90 {{- end }} 91 func (receiver *{{$.Meta.Name}}Client) {{$m.Name}}(ctx context.Context, _headers map[string]string, {{ range $i, $p := $m.Params}} 92 {{- if $i}},{{end}} 93 {{- range $c := $p.Comments }} 94 // {{$c}} 95 {{- end }} 96 {{ $p.Name}} {{$p.Type}} 97 {{- end }}) ({{(index $m.Results 0).Name}} {{(index $m.Results 0).Type}}, _resp *resty.Response, err error) { 98 var _err error 99 100 _req := receiver.client.R() 101 _req.SetContext(ctx) 102 if len(_headers) > 0 { 103 _req.SetHeaders(_headers) 104 } 105 {{- if $m.QueryParams }} 106 _queryParams, _ := _querystring.Values({{$m.QueryParams.Name}}) 107 _req.SetQueryParamsFromValues(_queryParams) 108 {{- end }} 109 {{- if $m.PathVars }} 110 {{- range $p := $m.PathVars }} 111 {{- if isOptional $p.Type }} 112 if {{$p.Name}} != nil { 113 _req.SetPathParam("{{$p.Name}}", fmt.Sprintf("%v", *{{$p.Name}})) 114 } 115 {{- else }} 116 _req.SetPathParam("{{$p.Name}}", fmt.Sprintf("%v", {{$p.Name}})) 117 {{- end }} 118 {{- end }} 119 {{- end }} 120 {{- if $m.HeaderVars }} 121 {{- range $p := $m.HeaderVars }} 122 {{- if isOptional $p.Type }} 123 if {{$p.Name}} != nil { 124 _req.SetHeader("{{$p.Name}}", fmt.Sprintf("%v", *{{$p.Name}})) 125 } 126 {{- else }} 127 _req.SetHeader("{{$p.Name}}", fmt.Sprintf("%v", {{$p.Name}})) 128 {{- end }} 129 {{- end }} 130 {{- end }} 131 {{- if $m.BodyParams }} 132 _bodyParams, _ := _querystring.Values({{$m.BodyParams.Name}}) 133 _req.SetFormDataFromValues(_bodyParams) 134 {{- end }} 135 {{- if $m.BodyJSON }} 136 _req.SetBody({{$m.BodyJSON.Name}}) 137 {{- end }} 138 {{- if $m.Files }} 139 {{- range $p := $m.Files }} 140 {{- if contains $p.Type "["}} 141 {{- if isOptional $p.Type }} 142 if {{$p.Name}} != nil { 143 for _, _f := range *{{$p.Name}} { 144 _req.SetFileReader("{{$p.Name}}", _f.Filename, _f.Reader) 145 } 146 } 147 {{- else }} 148 if len({{$p.Name}}) == 0 { 149 err = errors.New("at least one file should be uploaded for parameter {{$p.Name}}") 150 return 151 } 152 for _, _f := range {{$p.Name}} { 153 _req.SetFileReader("{{$p.Name}}", _f.Filename, _f.Reader) 154 } 155 {{- end }} 156 {{- else}} 157 {{- if isOptional $p.Type }} 158 if {{$p.Name}} != nil { 159 _req.SetFileReader("{{$p.Name}}", {{$p.Name}}.Filename, {{$p.Name}}.Reader) 160 } 161 {{- else }} 162 _req.SetFileReader("{{$p.Name}}", {{$p.Name}}.Filename, {{$p.Name}}.Reader) 163 {{- end }} 164 {{- end }} 165 {{- end }} 166 {{- end }} 167 168 {{- range $r := $m.Results }} 169 {{- if eq $r.Type "*os.File" }} 170 _req.SetDoNotParseResponse(true) 171 {{- end }} 172 {{- end }} 173 174 _resp, _err = _req.{{$m.Name | restyMethod}}("{{$m.Path}}") 175 if _err != nil { 176 err = errors.Wrap(_err, "") 177 return 178 } 179 if _resp.IsError() { 180 err = errors.New(_resp.String()) 181 return 182 } 183 {{- $done := false }} 184 {{- range $r := $m.Results }} 185 {{- if eq $r.Type "*os.File" }} 186 _disp := _resp.Header().Get("Content-Disposition") 187 _file := strings.TrimPrefix(_disp, "attachment; filename=") 188 _output := os.TempDir() 189 if stringutils.IsNotEmpty(_output) { 190 _file = _output + string(filepath.Separator) + _file 191 } 192 _file = filepath.Clean(_file) 193 if _err = fileutils.CreateDirectory(filepath.Dir(_file)); _err != nil { 194 err = errors.Wrap(_err, "") 195 return 196 } 197 _outFile, _err := os.Create(_file) 198 if _err != nil { 199 err = errors.Wrap(_err, "") 200 return 201 } 202 defer _outFile.Close() 203 defer _resp.RawBody().Close() 204 _, _err = io.Copy(_outFile, _resp.RawBody()) 205 if _err != nil { 206 err = errors.Wrap(_err, "") 207 return 208 } 209 {{ $r.Name }} = _outFile 210 return 211 {{- $done = true }} 212 {{- end }} 213 {{- end }} 214 {{- if not $done }} 215 {{- if eq (index $m.Results 0).Type "string" }} 216 {{(index $m.Results 0).Name}} = _resp.String() 217 {{- else }} 218 if _err = json.Unmarshal(_resp.Body(), &{{(index $m.Results 0).Name}}); _err != nil { 219 err = errors.Wrap(_err, "") 220 return 221 } 222 {{- end }} 223 return 224 {{- end }} 225 } 226 {{- end }} 227 228 func New{{.Meta.Name}}(opts ...ddhttp.DdClientOption) *{{.Meta.Name}}Client { 229 {{- if .Env }} 230 defaultProvider := ddhttp.NewServiceProvider("{{.Env}}") 231 {{- else }} 232 defaultProvider := ddhttp.NewServiceProvider("{{.Meta.Name | toUpper}}") 233 {{- end }} 234 defaultClient := ddhttp.NewClient() 235 236 svcClient := &{{.Meta.Name}}Client{ 237 provider: defaultProvider, 238 client: defaultClient, 239 } 240 241 for _, opt := range opts { 242 opt(svcClient) 243 } 244 245 svcClient.client.OnBeforeRequest(func(_ *resty.Client, request *resty.Request) error { 246 request.URL = svcClient.provider.SelectServer() + svcClient.rootPath + request.URL 247 return nil 248 }) 249 250 svcClient.client.SetPreRequestHook(func(_ *resty.Client, request *http.Request) error { 251 traceReq, _ := nethttp.TraceRequest(opentracing.GlobalTracer(), request, 252 nethttp.OperationName(fmt.Sprintf("HTTP %s: %s", request.Method, request.URL.Path))) 253 *request = *traceReq 254 return nil 255 }) 256 257 svcClient.client.OnAfterResponse(func(_ *resty.Client, response *resty.Response) error { 258 nethttp.TracerFromRequest(response.Request.RawRequest).Finish() 259 return nil 260 }) 261 262 return svcClient 263 } 264 ` 265 266 func toMethod(endpoint string) string { 267 endpoint = strings.ReplaceAll(strings.ReplaceAll(endpoint, "{", ""), "}", "") 268 endpoint = strings.ReplaceAll(strings.Trim(endpoint, "/"), "/", "_") 269 nosymbolreg := regexp.MustCompile(`[^a-zA-Z0-9_]`) 270 endpoint = nosymbolreg.ReplaceAllLiteralString(endpoint, "") 271 endpoint = strcase.ToCamel(endpoint) 272 numberstartreg := regexp.MustCompile(`^[0-9]+`) 273 if numberstartreg.MatchString(endpoint) { 274 startNumbers := numberstartreg.FindStringSubmatch(endpoint) 275 endpoint = numberstartreg.ReplaceAllLiteralString(endpoint, "") 276 endpoint += startNumbers[0] 277 } 278 return endpoint 279 } 280 281 func httpMethod(method string) string { 282 httpMethods := []string{"GET", "POST", "PUT", "DELETE"} 283 snake := strcase.ToSnake(method) 284 splits := strings.Split(snake, "_") 285 head := strings.ToUpper(splits[0]) 286 for _, m := range httpMethods { 287 if head == m { 288 return m 289 } 290 } 291 return "POST" 292 } 293 294 func restyMethod(method string) string { 295 return strings.Title(strings.ToLower(httpMethod(method))) 296 } 297 298 func isOptional(t string) bool { 299 return strings.HasPrefix(t, "*") 300 } 301 302 func genGoHTTP(paths map[string]v3.Path, svcname, dir, env, pkg string) { 303 _ = os.MkdirAll(dir, os.ModePerm) 304 output := filepath.Join(dir, svcname+"client.go") 305 fi, err := os.Stat(output) 306 if err != nil && !os.IsNotExist(err) { 307 panic(err) 308 } 309 if fi != nil { 310 logrus.Warningln("file " + svcname + "client.go will be overwritten") 311 } 312 var f *os.File 313 if f, err = os.Create(output); err != nil { 314 panic(err) 315 } 316 defer func(f *os.File) { 317 _ = f.Close() 318 }(f) 319 320 funcMap := make(map[string]interface{}) 321 funcMap["toCamel"] = strcase.ToCamel 322 funcMap["contains"] = strings.Contains 323 funcMap["restyMethod"] = restyMethod 324 funcMap["toUpper"] = strings.ToUpper 325 funcMap["isOptional"] = isOptional 326 tpl, _ := template.New("http.go.tmpl").Funcs(funcMap).Parse(httptmpl) 327 var sqlBuf bytes.Buffer 328 _ = tpl.Execute(&sqlBuf, struct { 329 Meta astutils.InterfaceMeta 330 Env string 331 Pkg string 332 }{ 333 Meta: api2Interface(paths, svcname), 334 Env: env, 335 Pkg: pkg, 336 }) 337 source := strings.TrimSpace(sqlBuf.String()) 338 astutils.FixImport([]byte(source), output) 339 } 340 341 func api2Interface(paths map[string]v3.Path, svcname string) astutils.InterfaceMeta { 342 var meta astutils.InterfaceMeta 343 meta.Name = strcase.ToCamel(svcname) 344 for endpoint, path := range paths { 345 if path.Get != nil { 346 if method, err := operation2Method(endpoint, "Get", path.Get, path.Parameters); err == nil { 347 meta.Methods = append(meta.Methods, method) 348 } else { 349 logrus.Errorln(err) 350 } 351 } 352 if path.Post != nil { 353 if method, err := operation2Method(endpoint, "Post", path.Post, path.Parameters); err == nil { 354 meta.Methods = append(meta.Methods, method) 355 } else { 356 logrus.Errorln(err) 357 } 358 } 359 if path.Put != nil { 360 if method, err := operation2Method(endpoint, "Put", path.Put, path.Parameters); err == nil { 361 meta.Methods = append(meta.Methods, method) 362 } else { 363 logrus.Errorln(err) 364 } 365 } 366 if path.Delete != nil { 367 if method, err := operation2Method(endpoint, "Delete", path.Delete, path.Parameters); err == nil { 368 meta.Methods = append(meta.Methods, method) 369 } else { 370 logrus.Errorln(err) 371 } 372 } 373 } 374 return meta 375 } 376 377 func operation2Method(endpoint, httpMethod string, operation *v3.Operation, gparams []v3.Parameter) (astutils.MethodMeta, error) { 378 var files, params []astutils.FieldMeta 379 var bodyJSON, bodyParams, qparams *astutils.FieldMeta 380 comments := commentLines(operation) 381 qSchema, pathvars, headervars := globalParams(gparams) 382 operationParams(operation.Parameters, &qSchema, &pathvars, &headervars) 383 384 if len(qSchema.Properties) > 0 { 385 qparams = schema2Field(&qSchema, "queryParams") 386 if qSchema.Type == v3.ObjectT && len(qSchema.Required) == 0 { 387 qparams.Type = toOptional(qparams.Type) 388 } 389 } 390 391 if httpMethod != "Get" && operation.RequestBody != nil { 392 bodyJSON, bodyParams, files = requestBody(operation) 393 } 394 395 if operation.Responses == nil { 396 return astutils.MethodMeta{}, errors.Errorf("response definition not found in api %s %s", httpMethod, endpoint) 397 } 398 399 if operation.Responses.Resp200 == nil { 400 return astutils.MethodMeta{}, errors.Errorf("200 response definition not found in api %s %s", httpMethod, endpoint) 401 } 402 403 results, err := responseBody(endpoint, httpMethod, operation) 404 if err != nil { 405 return astutils.MethodMeta{}, err 406 } 407 408 if qparams != nil { 409 params = append(params, *qparams) 410 } 411 412 params = append(params, pathvars...) 413 params = append(params, headervars...) 414 415 if bodyParams != nil { 416 params = append(params, *bodyParams) 417 } 418 419 if bodyJSON != nil { 420 params = append(params, *bodyJSON) 421 } 422 423 params = append(params, files...) 424 425 return astutils.MethodMeta{ 426 Name: httpMethod + toMethod(endpoint), 427 Params: params, 428 Results: results, 429 PathVars: pathvars, 430 HeaderVars: headervars, 431 BodyParams: bodyParams, 432 BodyJSON: bodyJSON, 433 Files: files, 434 Comments: comments, 435 Path: endpoint, 436 QueryParams: qparams, 437 }, nil 438 } 439 440 func operationParams(parameters []v3.Parameter, qSchema *v3.Schema, pathvars, headervars *[]astutils.FieldMeta) { 441 for _, item := range parameters { 442 switch item.In { 443 case v3.InQuery: 444 qSchema.Properties[item.Name] = item.Schema 445 if item.Required { 446 qSchema.Required = append(qSchema.Required, item.Name) 447 } 448 case v3.InPath: 449 *pathvars = append(*pathvars, parameter2Field(item)) 450 case v3.InHeader: 451 *headervars = append(*headervars, parameter2Field(item)) 452 default: 453 panic(fmt.Errorf("not support %s parameter yet", item.In)) 454 } 455 } 456 } 457 458 func responseBody(endpoint, httpMethod string, operation *v3.Operation) (results []astutils.FieldMeta, err error) { 459 if stringutils.IsNotEmpty(operation.Responses.Resp200.Ref) { 460 key := strings.TrimPrefix(operation.Responses.Resp200.Ref, "#/components/responses/") 461 if response, exists := responses[key]; exists { 462 operation.Responses.Resp200 = &response 463 } else { 464 panic(fmt.Errorf("response %s not exists", operation.Responses.Resp200.Ref)) 465 } 466 } 467 468 content := operation.Responses.Resp200.Content 469 if content == nil { 470 return nil, errors.Errorf("200 response content definition not found in api %s %s", httpMethod, endpoint) 471 } 472 473 if content.JSON != nil { 474 results = append(results, *schema2Field(content.JSON.Schema, "ret")) 475 } else if content.Stream != nil { 476 results = append(results, astutils.FieldMeta{ 477 Name: "_downloadFile", 478 Type: "*os.File", 479 }) 480 } else if content.TextPlain != nil { 481 results = append(results, *schema2Field(content.TextPlain.Schema, "ret")) 482 } else if content.Default != nil { 483 results = append(results, *schema2Field(content.Default.Schema, "ret")) 484 } else { 485 return nil, errors.Errorf("200 response content definition not support yet in api %s %s", httpMethod, endpoint) 486 } 487 return 488 } 489 490 func requestBody(operation *v3.Operation) (bodyJSON, bodyParams *astutils.FieldMeta, files []astutils.FieldMeta) { 491 resolveSchemaFromRef(operation) 492 493 content := operation.RequestBody.Content 494 if content.JSON != nil { 495 bodyJSON = schema2Field(content.JSON.Schema, "bodyJSON") 496 if !operation.RequestBody.Required && bodyJSON != nil { 497 bodyJSON.Type = toOptional(bodyJSON.Type) 498 } 499 } else if content.FormURL != nil { 500 bodyParams = schema2Field(content.FormURL.Schema, "bodyParams") 501 if !operation.RequestBody.Required && bodyParams != nil { 502 bodyParams.Type = toOptional(bodyParams.Type) 503 } 504 } else if content.FormData != nil { 505 bodyParams, files = parseFormData(content.FormData) 506 if !operation.RequestBody.Required && bodyParams != nil { 507 bodyParams.Type = toOptional(bodyParams.Type) 508 } 509 } else if content.Stream != nil { 510 f := astutils.FieldMeta{ 511 Name: "file", 512 Type: "v3.FileModel", 513 } 514 if !operation.RequestBody.Required { 515 f.Type = toOptional(f.Type) 516 } 517 files = append(files, f) 518 } else if content.TextPlain != nil { 519 bodyJSON = schema2Field(content.TextPlain.Schema, "bodyJSON") 520 if !operation.RequestBody.Required && bodyJSON != nil { 521 bodyJSON.Type = toOptional(bodyJSON.Type) 522 } 523 } else if content.Default != nil { 524 bodyJSON = schema2Field(content.Default.Schema, "bodyJSON") 525 if !operation.RequestBody.Required && bodyJSON != nil { 526 bodyJSON.Type = toOptional(bodyJSON.Type) 527 } 528 } 529 return 530 } 531 532 func parseFormData(formData *v3.MediaType) (bodyParams *astutils.FieldMeta, files []astutils.FieldMeta) { 533 schema := *formData.Schema 534 if stringutils.IsNotEmpty(schema.Ref) { 535 schema = schemas[strings.TrimPrefix(formData.Schema.Ref, "#/components/schemas/")] 536 } 537 aSchema := v3.Schema{ 538 Type: v3.ObjectT, 539 Properties: make(map[string]*v3.Schema), 540 } 541 for k, v := range schema.Properties { 542 var gotype string 543 if v.Type == v3.StringT && v.Format == v3.BinaryF { 544 gotype = "v3.FileModel" 545 } else if v.Type == v3.ArrayT && v.Items.Type == v3.StringT && v.Items.Format == v3.BinaryF { 546 gotype = "[]v3.FileModel" 547 } 548 if stringutils.IsNotEmpty(gotype) && !sliceutils.StringContains(schema.Required, k) { 549 gotype = toOptional(gotype) 550 } 551 if stringutils.IsNotEmpty(gotype) { 552 files = append(files, astutils.FieldMeta{ 553 Name: k, 554 Type: gotype, 555 }) 556 continue 557 } 558 aSchema.Properties[k] = v 559 if sliceutils.StringContains(schema.Required, k) { 560 aSchema.Required = append(aSchema.Required, k) 561 } 562 } 563 if len(aSchema.Properties) > 0 { 564 bodyParams = schema2Field(&aSchema, "bodyParams") 565 } 566 return 567 } 568 569 // resolveSchemaFromRef resolves schema from ref 570 func resolveSchemaFromRef(operation *v3.Operation) { 571 if stringutils.IsNotEmpty(operation.RequestBody.Ref) { 572 // #/components/requestBodies/Raw3 573 key := strings.TrimPrefix(operation.RequestBody.Ref, "#/components/requestBodies/") 574 if requestBody, exists := requestBodies[key]; exists { 575 operation.RequestBody = &requestBody 576 } else { 577 panic(fmt.Errorf("requestBody %s not exists", operation.RequestBody.Ref)) 578 } 579 } 580 } 581 582 func globalParams(gparams []v3.Parameter) (v3.Schema, []astutils.FieldMeta, []astutils.FieldMeta) { 583 var pathvars, headervars []astutils.FieldMeta 584 qSchema := v3.Schema{ 585 Type: v3.ObjectT, 586 Properties: make(map[string]*v3.Schema), 587 } 588 for _, item := range gparams { 589 switch item.In { 590 case v3.InQuery: 591 qSchema.Properties[item.Name] = item.Schema 592 if item.Required { 593 qSchema.Required = append(qSchema.Required, item.Name) 594 } 595 case v3.InPath: 596 pathvars = append(pathvars, parameter2Field(item)) 597 case v3.InHeader: 598 headervars = append(headervars, parameter2Field(item)) 599 default: 600 panic(fmt.Errorf("not support %s parameter yet", item.In)) 601 } 602 } 603 return qSchema, pathvars, headervars 604 } 605 606 func commentLines(operation *v3.Operation) []string { 607 var comments []string 608 if stringutils.IsNotEmpty(operation.Summary) { 609 comments = append(comments, strings.Split(operation.Summary, "\n")...) 610 } 611 if stringutils.IsNotEmpty(operation.Description) { 612 comments = append(comments, strings.Split(operation.Description, "\n")...) 613 } 614 return comments 615 } 616 617 func schema2Field(schema *v3.Schema, name string) *astutils.FieldMeta { 618 var comments []string 619 if stringutils.IsNotEmpty(schema.Description) { 620 comments = append(comments, strings.Split(schema.Description, "\n")...) 621 } 622 return &astutils.FieldMeta{ 623 Name: name, 624 Type: toGoType(schema), 625 Comments: comments, 626 } 627 } 628 629 func parameter2Field(param v3.Parameter) astutils.FieldMeta { 630 var comments []string 631 if stringutils.IsNotEmpty(param.Description) { 632 comments = append(comments, strings.Split(param.Description, "\n")...) 633 } 634 t := toGoType(param.Schema) 635 if param.Required { 636 comments = append(comments, "required") 637 } else { 638 t = toOptional(t) 639 } 640 return astutils.FieldMeta{ 641 Name: param.Name, 642 Type: t, 643 Comments: comments, 644 } 645 } 646 647 // toGoType converts schema to golang type 648 // IntegerT Type = "integer" 649 // StringT Type = "string" 650 // BooleanT Type = "boolean" 651 // NumberT Type = "number" 652 // ObjectT Type = "object" 653 // ArrayT Type = "array" 654 func toGoType(schema *v3.Schema) string { 655 if stringutils.IsNotEmpty(schema.Ref) { 656 refName := strings.TrimPrefix(schema.Ref, "#/components/schemas/") 657 if realSchema, exists := schemas[refName]; exists { 658 if realSchema.Type == v3.ObjectT && realSchema.AdditionalProperties != nil { 659 result := additionalProperties2Map(realSchema.AdditionalProperties) 660 if stringutils.IsNotEmpty(result) { 661 return result 662 } 663 } 664 } 665 return toCamel(clean(refName)) 666 } 667 switch schema.Type { 668 case v3.IntegerT: 669 return integer2Go(schema) 670 case v3.StringT: 671 return string2Go(schema) 672 case v3.BooleanT: 673 return "bool" 674 case v3.NumberT: 675 return number2Go(schema) 676 case v3.ObjectT: 677 return object2Struct(schema) 678 case v3.ArrayT: 679 return "[]" + toGoType(schema.Items) 680 default: 681 return "interface{}" 682 } 683 } 684 685 func toOptionalGoType(schema *v3.Schema) string { 686 if stringutils.IsNotEmpty(schema.Ref) { 687 refName := strings.TrimPrefix(schema.Ref, "#/components/schemas/") 688 if realSchema, exists := schemas[refName]; exists { 689 if realSchema.Type == v3.ObjectT && realSchema.AdditionalProperties != nil { 690 result := additionalProperties2Map(realSchema.AdditionalProperties) 691 if stringutils.IsNotEmpty(result) { 692 return result 693 } 694 } 695 } 696 return "*" + toCamel(clean(refName)) 697 } 698 switch schema.Type { 699 case v3.IntegerT: 700 return "*" + integer2Go(schema) 701 case v3.StringT: 702 return "*" + string2Go(schema) 703 case v3.BooleanT: 704 return "*bool" 705 case v3.NumberT: 706 return "*" + number2Go(schema) 707 case v3.ObjectT: 708 result := object2Struct(schema) 709 if strings.HasPrefix(result, "struct {") { 710 return "*" + result 711 } 712 return result 713 case v3.ArrayT: 714 return "[]" + toGoType(schema.Items) 715 default: 716 return "interface{}" 717 } 718 } 719 720 func number2Go(schema *v3.Schema) string { 721 switch schema.Format { 722 case v3.FloatF: 723 return "float32" 724 case v3.DoubleF: 725 return "float64" 726 default: 727 return "float64" 728 } 729 } 730 731 func string2Go(schema *v3.Schema) string { 732 switch schema.Format { 733 case v3.DateTimeF: 734 return "time.Time" 735 case v3.BinaryF: 736 return "v3.FileModel" 737 default: 738 return "string" 739 } 740 } 741 742 // integer2Go converts integer schema to golang basic type 743 // Int32F Format = "int32" 744 // Int64F Format = "int64" 745 // FloatF Format = "float" 746 // DoubleF Format = "double" 747 // DateTimeF Format = "date-time" 748 // BinaryF Format = "binary" 749 func integer2Go(schema *v3.Schema) string { 750 switch schema.Format { 751 case v3.Int32F: 752 return "int" 753 case v3.Int64F: 754 return "int64" 755 default: 756 return "int" 757 } 758 } 759 760 func additionalProperties2Map(additionalProperties interface{}) string { 761 if additionalProperties == nil { 762 return "" 763 } 764 if value, ok := additionalProperties.(map[string]interface{}); ok { 765 var additionalSchema v3.Schema 766 copier.DeepCopy(value, &additionalSchema) 767 return "map[string]" + toGoType(&additionalSchema) 768 } 769 return "" 770 } 771 772 func object2Struct(schema *v3.Schema) string { 773 if schema.AdditionalProperties != nil { 774 result := additionalProperties2Map(schema.AdditionalProperties) 775 if stringutils.IsNotEmpty(result) { 776 return result 777 } 778 } 779 if len(schema.Properties) == 0 { 780 return "interface{}" 781 } 782 b := new(strings.Builder) 783 b.WriteString("struct {\n") 784 for k, v := range schema.Properties { 785 if stringutils.IsNotEmpty(v.Description) { 786 descs := strings.Split(v.Description, "\n") 787 for _, desc := range descs { 788 b.WriteString(fmt.Sprintf(" // %s\n", desc)) 789 } 790 } 791 if sliceutils.StringContains(schema.Required, k) { 792 b.WriteString(" // required\n") 793 } 794 jsontag := k 795 if omitempty { 796 jsontag += ",omitempty" 797 } 798 if sliceutils.StringContains(schema.Required, k) { 799 b.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" url:\"%s\"`\n", strcase.ToCamel(k), toGoType(v), jsontag, k)) 800 } else { 801 b.WriteString(fmt.Sprintf(" %s %s `json:\"%s\" url:\"%s\"`\n", strcase.ToCamel(k), "*"+toGoType(v), jsontag, k)) 802 } 803 } 804 b.WriteString("}") 805 return b.String() 806 } 807 808 func toComment(comment string, title ...string) string { 809 if stringutils.IsEmpty(comment) { 810 return "" 811 } 812 b := new(strings.Builder) 813 lines := strings.Split(comment, "\n") 814 for i, line := range lines { 815 if len(title) > 0 && i == 0 { 816 b.WriteString(fmt.Sprintf("// %s %s\n", title[0], line)) 817 } else { 818 b.WriteString(fmt.Sprintf("// %s\n", line)) 819 } 820 } 821 return strings.TrimSuffix(b.String(), "\n") 822 } 823 824 func clean(str string) string { 825 return strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(str, "«", ""), "»", "")) 826 } 827 828 func toCamel(str string) string { 829 return strcase.ToCamel(clean(str)) 830 } 831 832 func genGoVo(schemas map[string]v3.Schema, output, pkg string) { 833 if err := os.MkdirAll(filepath.Dir(output), os.ModePerm); err != nil { 834 panic(err) 835 } 836 funcMap := make(map[string]interface{}) 837 funcMap["toCamel"] = toCamel 838 funcMap["toGoType"] = toGoType 839 funcMap["toComment"] = toComment 840 funcMap["toOptionalGoType"] = toOptionalGoType 841 funcMap["stringContains"] = sliceutils.StringContains 842 filterMap := make(map[string]v3.Schema) 843 for k, v := range schemas { 844 result := additionalProperties2Map(v.AdditionalProperties) 845 if stringutils.IsEmpty(result) { 846 filterMap[k] = v 847 } 848 } 849 tpl, _ := template.New("vo.go.tmpl").Funcs(funcMap).Parse(votmpl) 850 var sqlBuf bytes.Buffer 851 _ = tpl.Execute(&sqlBuf, struct { 852 Schemas map[string]v3.Schema 853 Omit bool 854 Pkg string 855 }{ 856 Schemas: filterMap, 857 Omit: omitempty, 858 Pkg: pkg, 859 }) 860 source := strings.TrimSpace(sqlBuf.String()) 861 astutils.FixImport([]byte(source), output) 862 } 863 864 var schemas map[string]v3.Schema 865 var requestBodies map[string]v3.RequestBody 866 var responses map[string]v3.Response 867 var omitempty bool 868 869 // GenGoClient generate go http client code from OpenAPI3.0 json document 870 func GenGoClient(dir string, file string, omit bool, env, pkg string) { 871 var ( 872 err error 873 f *os.File 874 clientDir string 875 fi os.FileInfo 876 api v3.API 877 vofile string 878 ) 879 clientDir = filepath.Join(dir, pkg) 880 if err = os.MkdirAll(clientDir, os.ModePerm); err != nil { 881 panic(err) 882 } 883 api = loadAPI(file) 884 schemas = api.Components.Schemas 885 requestBodies = api.Components.RequestBodies 886 responses = api.Components.Responses 887 omitempty = omit 888 svcmap := make(map[string]map[string]v3.Path) 889 for endpoint, path := range api.Paths { 890 svcname := strings.Split(strings.Trim(endpoint, "/"), "/")[0] 891 if value, exists := svcmap[svcname]; exists { 892 value[endpoint] = path 893 } else { 894 svcmap[svcname] = make(map[string]v3.Path) 895 svcmap[svcname][endpoint] = path 896 } 897 } 898 899 for svcname, paths := range svcmap { 900 genGoHTTP(paths, svcname, clientDir, env, pkg) 901 } 902 903 vofile = filepath.Join(clientDir, "vo.go") 904 fi, err = os.Stat(vofile) 905 if err != nil && !os.IsNotExist(err) { 906 panic(err) 907 } 908 if fi != nil { 909 logrus.Warningln("file vo.go will be overwritten") 910 } 911 if f, err = os.Create(vofile); err != nil { 912 panic(err) 913 } 914 defer f.Close() 915 genGoVo(api.Components.Schemas, vofile, pkg) 916 } 917 918 func loadAPI(file string) v3.API { 919 var ( 920 docfile *os.File 921 err error 922 docraw []byte 923 api v3.API 924 ) 925 if strings.HasPrefix(file, "http") { 926 link := file 927 client := resty.New() 928 client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) 929 root, _ := os.Getwd() 930 client.SetOutputDirectory(root) 931 filename := ".openapi3" 932 _, err := client.R(). 933 SetOutput(filename). 934 Get(link) 935 if err != nil { 936 panic(err) 937 } 938 file = filepath.Join(root, filename) 939 defer os.Remove(file) 940 } 941 if docfile, err = os.Open(file); err != nil { 942 panic(err) 943 } 944 defer func(docfile *os.File) { 945 _ = docfile.Close() 946 }(docfile) 947 if docraw, err = ioutil.ReadAll(docfile); err != nil { 948 panic(err) 949 } 950 if err = json.Unmarshal(docraw, &api); err != nil { 951 panic(err) 952 } 953 return api 954 } 955 956 func toOptional(t string) string { 957 if !strings.HasPrefix(t, "*") { 958 return "*" + t 959 } 960 return t 961 }