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  }