From ce5a59091babaa9d515b89923b6e0fa28362e082 Mon Sep 17 00:00:00 2001 From: Karitham Date: Fri, 25 Aug 2023 20:38:00 +0200 Subject: [PATCH 1/9] add client --- codegen/codegen.go | 26 ++ codegen/template_helpers.go | 20 +- examples/petstore-expanded/api/client.go | 292 ++++++++++++++++++++++ examples/petstore-expanded/client/main.go | 78 ++++++ go.mod | 5 +- go.sum | 9 +- main.go | 2 + templates/client.tmpl | 144 +++++++++++ templates/imports.tmpl | 1 + 9 files changed, 571 insertions(+), 6 deletions(-) create mode 100644 examples/petstore-expanded/api/client.go create mode 100644 examples/petstore-expanded/client/main.go create mode 100644 templates/client.tmpl diff --git a/codegen/codegen.go b/codegen/codegen.go index e01edeb8..78ef2386 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -36,6 +36,7 @@ import ( // Most callers to this package will use Generate. type Options struct { GenerateServer bool // GenerateChiServer specifies whether to generate chi server boilerplate + GenerateClient bool // GenerateClient specifies whether to generate client boilerplate GenerateTypes bool // GenerateTypes specifies whether to generate type definitions EmbedSpec bool // Whether to embed the swagger spec in the generated code SkipFmt bool // Whether to skip go imports on the generated code @@ -207,6 +208,18 @@ func Generate(swagger *openapi3.T, packageName string, opts Options) (string, er } } + if opts.GenerateClient { + clientOut, err := GenerateClient(t, ops) + if err != nil { + return "", fmt.Errorf("error writing client: %w", err) + } + + _, err = w.WriteString(clientOut) + if err != nil { + return "", fmt.Errorf("error writing client: %w", err) + } + } + if opts.EmbedSpec { _, err = w.WriteString(inlinedSpec) if err != nil { @@ -610,6 +623,19 @@ func GenerateAdditionalPropertyBoilerplate(t *template.Template, typeDefs []Type return GenerateTemplates([]string{"additional-properties.tmpl"}, t, context) } +func GenerateClient( + t *template.Template, + ops []OperationDefinition, +) (string, error) { + context := struct { + Operations []OperationDefinition + }{ + Operations: ops, + } + + return GenerateTemplates([]string{"client.tmpl"}, t, context) +} + // SanitizeCode runs sanitizers across the generated Go code to ensure the // generated code will be able to compile. func SanitizeCode(goCode string) string { diff --git a/codegen/template_helpers.go b/codegen/template_helpers.go index 6c90297e..fbaeee18 100644 --- a/codegen/template_helpers.go +++ b/codegen/template_helpers.go @@ -101,6 +101,16 @@ func responseNameToStatusCode(responseName string) string { } } +func responseToStatusRangeString(responseName string) string { + switch strings.ToUpper(responseName) { + case "DEFAULT": + return "!= 0" + case "1XX", "2XX", "3XX", "4XX", "5XX": + return fmt.Sprintf(">= %s00 && < %s99", responseName[:1], responseName[:1]) + } + return fmt.Sprintf("== %s", responseName) +} + // TitleWord converts a single worded string to title case. // This is a replacement to `strings.Title` which we used previously. // We didn't need strings.Title word boundary rules, and just want to Title the words directly, @@ -123,9 +133,11 @@ var TemplateFunctions = template.FuncMap{ "swaggerURIToChiURI": SwaggerURIToChiURI, - "statusCode": responseNameToStatusCode, + "statusCode": responseNameToStatusCode, + "statusCodeRange": responseToStatusRangeString, - "ucFirst": snaker.ForceCamelIdentifier, - "lower": strings.ToLower, - "title": TitleWord, + "hasPrefix": strings.HasPrefix, + "ucFirst": snaker.ForceCamelIdentifier, + "lower": strings.ToLower, + "title": TitleWord, } diff --git a/examples/petstore-expanded/api/client.go b/examples/petstore-expanded/api/client.go new file mode 100644 index 00000000..310e7e75 --- /dev/null +++ b/examples/petstore-expanded/api/client.go @@ -0,0 +1,292 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/discord-gophers/goapi-gen version (devel) DO NOT EDIT. +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/carlmjohnson/requests" +) + +type Client struct { + BaseURL string + Client *http.Client +} + +// FindPets Returns all pets +func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error, error) { + + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method("GET") + req = req.BaseURL(c.BaseURL) + req = req.Path("/pets") + + if p.Tags != nil { + req = req.Param("tags", strings.Join(p.Tags, ",")) + } + if p.Limit != nil { + req = req.Param("limit", fmt.Sprint(p.Limit)) + } + + // define out handlers + read := false // flag such that empty responses are kept nil + is200 := func(resp *http.Response) bool { + return resp.StatusCode == 200 + } + var _200 *[]Pet + handle200 := func(resp *http.Response) error { + if !is200(resp) { + return nil + } + + if read { + return nil + } + read = true + + _200 = new([]Pet) + err := json.NewDecoder(resp.Body).Decode(_200) + switch err { + case nil: + return nil + case io.EOF: + _200 = nil + return nil + } + return err + } + + isdefault := func(resp *http.Response) bool { + return resp.StatusCode != 0 + } + var _default *Error + handledefault := func(resp *http.Response) error { + if !isdefault(resp) { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + + req = req.Handle(requests.ChainHandlers(handle200, handledefault)) + err := req.Fetch(ctx) + if err != nil { + return nil, nil, err + } + + return _200, _default, nil +} + +// AddPet Creates a new pet +func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) { + + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method("POST") + req = req.BaseURL(c.BaseURL) + req = req.Path("/pets") + + req = req.BodyJSON(body) + req = req.ContentType("application/json") + + // define out handlers + read := false // flag such that empty responses are kept nil + is201 := func(resp *http.Response) bool { + return resp.StatusCode == 201 + } + var _201 *Pet + handle201 := func(resp *http.Response) error { + if !is201(resp) { + return nil + } + + if read { + return nil + } + read = true + + _201 = new(Pet) + err := json.NewDecoder(resp.Body).Decode(_201) + switch err { + case nil: + return nil + case io.EOF: + _201 = nil + return nil + } + return err + } + + isdefault := func(resp *http.Response) bool { + return resp.StatusCode != 0 + } + var _default *Error + handledefault := func(resp *http.Response) error { + if !isdefault(resp) { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + + req = req.Handle(requests.ChainHandlers(handle201, handledefault)) + err := req.Fetch(ctx) + if err != nil { + return nil, nil, err + } + + return _201, _default, nil +} + +// DeletePet Deletes a pet by ID +func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { + + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method("DELETE") + req = req.BaseURL(c.BaseURL) + req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) + + // define out handlers + read := false // flag such that empty responses are kept nil + isdefault := func(resp *http.Response) bool { + return resp.StatusCode != 0 + } + var _default *Error + handledefault := func(resp *http.Response) error { + if !isdefault(resp) { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + + req = req.Handle(requests.ChainHandlers(handledefault)) + err := req.Fetch(ctx) + if err != nil { + return nil, err + } + + return _default, nil +} + +// FindPetByID Returns a pet by ID +func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error) { + + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method("GET") + req = req.BaseURL(c.BaseURL) + req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) + + // define out handlers + read := false // flag such that empty responses are kept nil + is200 := func(resp *http.Response) bool { + return resp.StatusCode == 200 + } + var _200 *Pet + handle200 := func(resp *http.Response) error { + if !is200(resp) { + return nil + } + + if read { + return nil + } + read = true + + _200 = new(Pet) + err := json.NewDecoder(resp.Body).Decode(_200) + switch err { + case nil: + return nil + case io.EOF: + _200 = nil + return nil + } + return err + } + + isdefault := func(resp *http.Response) bool { + return resp.StatusCode != 0 + } + var _default *Error + handledefault := func(resp *http.Response) error { + if !isdefault(resp) { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + + req = req.Handle(requests.ChainHandlers(handle200, handledefault)) + err := req.Fetch(ctx) + if err != nil { + return nil, nil, err + } + + return _200, _default, nil +} diff --git a/examples/petstore-expanded/client/main.go b/examples/petstore-expanded/client/main.go new file mode 100644 index 00000000..3db7216a --- /dev/null +++ b/examples/petstore-expanded/client/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "flag" + "log" + "net/http" + "strconv" + + "github.com/discord-gophers/goapi-gen/examples/petstore-expanded/api" +) + +func main() { + port := flag.Int("port", 8080, "Port for test HTTP server") + flag.Parse() + + client := api.Client{ + BaseURL: "http://localhost:" + strconv.Itoa(*port), + Client: http.DefaultClient, // in case we need to do authentication, we can have an oauth2.Client for example + } + + spotTag := "TagOfSpot" + // Create a new pet + newPet := api.NewPet{ + Name: "Spot", + Tag: &spotTag, + } + + // Add the pet + pet, apiErr, err := client.AddPet(context.Background(), newPet) + if err != nil { + panic(err) + } + if apiErr != nil { + panic(apiErr) + } + + log.Println("Added pet", pet.ID) + + // Get the pet + pet, apiErr, err = client.FindPetByID(context.Background(), pet.ID) + if err != nil { + panic(err) + } + if apiErr != nil { + panic(apiErr) + } + + log.Println("Found pet", pet.ID, "named", pet.Name) + + pets, apiErr, err := client.FindPets(context.Background(), api.FindPetsParams{ + Tags: []string{spotTag}, + }) + if err != nil { + panic(err) + } + if apiErr != nil { + panic(apiErr) + } + + log.Println("Found", len(*pets), "pets") + + for _, pet := range *pets { + log.Println("Pet", pet.ID, "named", pet.Name) + + // Delete the pet + apiErr, err = client.DeletePet(context.Background(), pet.ID) + if err != nil { + panic(err) + } + + if apiErr != nil { + panic(apiErr) + } + + log.Println("Deleted pet", pet.ID) + } +} diff --git a/go.mod b/go.mod index e0489ae4..1edf7f32 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/discord-gophers/goapi-gen require ( + github.com/carlmjohnson/requests v0.23.4 github.com/getkin/kin-openapi v0.80.0 github.com/go-chi/chi/v5 v5.0.4 github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 @@ -12,6 +13,8 @@ require ( gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) +require golang.org/x/net v0.7.0 // indirect + require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -25,7 +28,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect golang.org/x/mod v0.7.0 // indirect - golang.org/x/sys v0.2.0 // indirect + golang.org/x/sys v0.5.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 718cf0bf..32d4b592 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/carlmjohnson/requests v0.23.4 h1:AxcvapfB9RPXLSyvAHk9YJoodQ43ZjzNHj6Ft3tQGdg= +github.com/carlmjohnson/requests v0.23.4/go.mod h1:Qzp6tW4DQyainPP+tGwiJTzwxvElTIKm0B191TgTtOA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -65,6 +67,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -74,15 +78,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/main.go b/main.go index d217440b..d79fa102 100644 --- a/main.go +++ b/main.go @@ -90,6 +90,8 @@ func run(c *cli.Context, cfg *config) error { opts.SkipFmt = true case "skip-prune": opts.SkipPrune = true + case "client": + opts.GenerateClient = true default: return fmt.Errorf("unknown generation option: %s", tgt) } diff --git a/templates/client.tmpl b/templates/client.tmpl new file mode 100644 index 00000000..3e83cab3 --- /dev/null +++ b/templates/client.tmpl @@ -0,0 +1,144 @@ +type Client struct { + BaseURL string + Client *http.Client +} + +{{define "rt" -}} +{{if or (not (hasPrefix .Schema.GoType "*")) .Schema.SkipOptionalPointer -}} + p.{{.ParamName | ucFirst}} +{{- else -}} + *p.{{.ParamName | ucFirst}} +{{- end -}} +{{- end -}} + +{{define "typeToString" -}} +{{- if eq .Schema.GoType "string" -}} +{{template "rt" .}} +{{- else if eq .Schema.GoType "int32" -}} +fmt.Sprint({{template "rt" .}}) +{{- else if eq .Schema.GoType "int64" -}} +fmt.Sprint({{template "rt" .}}) +{{- else if eq .Schema.GoType "float32" -}} +fmt.Sprint({{template "rt" .}}) +{{- else if eq .Schema.GoType "float64" -}} +fmt.Sprint({{template "rt" .}}) +{{- else if eq .Schema.GoType "bool" -}} +fmt.Sprint({{template "rt" .}}) +{{- else if eq .Schema.GoType "[]string" -}} +strings.Join({{template "rt" .}}, ",") +{{- else if eq .Schema.GoType "[]int32" -}} +strings.Join({{template "rt" .}}, ",") +{{- else if eq .Schema.GoType "[]int64" -}} +strings.Join({{template "rt" .}}, ",") +{{- else if eq .Schema.GoType "[]float32" -}} +strings.Join({{template "rt" .}}, ",") +{{- else if eq .Schema.GoType "[]float64" -}} +strings.Join({{template "rt" .}}, ",") +{{- else if eq .Schema.GoType "[]bool" -}} +strings.Join({{template "rt" .}}, ",") +{{- else -}} +fmt.Sprint({{template "rt" .}}) +{{- end -}} +{{- end}} + + +{{range .Operations -}} + +// {{.OperationID}} {{.Summary}} +func (c *Client) {{.OperationID}}(ctx context.Context + {{- if not (eq (len .Params) 0) -}}, p {{.OperationID}}Params {{- end -}} + {{- range .PathParams}}, {{.ParamName}} {{.Schema.TypeDecl}}{{end}} + {{- if not (eq (len .Bodies) 0) -}}, body {{(index .Bodies 0 ).Schema.GoType}} {{- end -}} + ) ({{- range .GetResponseTypeDefinitions -}} + *{{ .Schema.TypeDecl }}, + {{- end -}} + error) { + + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method("{{.Method}}") + req = req.BaseURL(c.BaseURL) + req = req.Path( + {{- if not (eq (.PathParams | len) 0) -}} + strings.NewReplacer({{range .PathParams}} "{ {{- .ParamName -}} }", fmt.Sprint({{.ParamName}}),{{end}}).Replace("{{.Path}}") + {{- else -}} + "{{.Path}}" + {{- end -}} + ) + + {{range .QueryParams -}} + {{if .Required -}} + req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}} ) + {{else -}} + if p.{{.ParamName | ucFirst}} != nil { + req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}} ) + } + {{end -}} + {{end -}} + + {{range .HeaderParams -}} + {{if .Required -}} + req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}} ) + {{else -}} + if p.{{.ParamName | ucFirst}} != nil { + req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}} ) + } + {{end -}} + {{end -}} + + {{range .CookieParams -}} + {{if .Required -}} + req = req.Cookie("{{.ParamName}}", fmt.Sprint(p.{{.ParamName | ucFirst}})) + {{else -}} + if p.{{.ParamName | ucFirst}} != nil { + req = req.Cookie("{{.ParamName}}", fmt.Sprint(p.{{.ParamName | ucFirst}})) + } + {{end -}} + {{end -}} + + {{if not (eq (len .Bodies) 0) -}} + req = req.BodyJSON(body) + req = req.ContentType({{ range .Bodies}}"{{.ContentType}}",{{end}}) + {{end}} + + // define out handlers + read := false // flag such that empty responses are kept nil + {{range .GetResponseTypeDefinitions -}} + is{{.ResponseName}} := func(resp *http.Response) bool { + return resp.StatusCode {{.ResponseName | statusCodeRange}} + } + var _{{.ResponseName}} *{{.Schema.TypeDecl}} + handle{{.ResponseName}} := func(resp *http.Response) error { + if !is{{.ResponseName}}(resp) { + return nil + } + + if read { + return nil + } + read = true + + _{{.ResponseName}} = new({{.Schema.TypeDecl}}) + err := json.NewDecoder(resp.Body).Decode(_{{.ResponseName}}) + switch err { + case nil: + return nil + case io.EOF: + _{{.ResponseName}} = nil + return nil + } + return err + } + + {{end -}} + + req = req.Handle(requests.ChainHandlers( {{- range .GetResponseTypeDefinitions -}} handle{{.ResponseName}}, {{- end -}})) + err := req.Fetch(ctx) + if err != nil { + return {{range .GetResponseTypeDefinitions }}nil, {{ end }} err + } + + return {{range .GetResponseTypeDefinitions }}_{{.ResponseName}}, {{ end }} nil +} + +{{ end -}} \ No newline at end of file diff --git a/templates/imports.tmpl b/templates/imports.tmpl index 1b43ff35..bfa19787 100644 --- a/templates/imports.tmpl +++ b/templates/imports.tmpl @@ -24,6 +24,7 @@ import ( openapi_types "github.com/discord-gophers/goapi-gen/types" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" + "github.com/carlmjohnson/requests" "github.com/go-chi/render" {{- range .ExternalImports}} {{ . }} From cf03382dbc54d389f9be3b27043fc88e5ab450ff Mon Sep 17 00:00:00 2001 From: Karitham Date: Fri, 25 Aug 2023 21:17:17 +0200 Subject: [PATCH 2/9] fix generation for clients with array types --- templates/client.tmpl | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/templates/client.tmpl b/templates/client.tmpl index 3e83cab3..95979ca1 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -27,15 +27,45 @@ fmt.Sprint({{template "rt" .}}) {{- else if eq .Schema.GoType "[]string" -}} strings.Join({{template "rt" .}}, ",") {{- else if eq .Schema.GoType "[]int32" -}} -strings.Join({{template "rt" .}}, ",") +func() string { + var s []string + for _, v := range {{template "rt" .}} { + s = append(s, fmt.Sprint(v)) + } + return strings.Join(s, ",") +}() {{- else if eq .Schema.GoType "[]int64" -}} -strings.Join({{template "rt" .}}, ",") +func() string { + var s []string + for _, v := range {{template "rt" .}} { + s = append(s, fmt.Sprint(v)) + } + return strings.Join(s, ",") +}() {{- else if eq .Schema.GoType "[]float32" -}} -strings.Join({{template "rt" .}}, ",") +func() string { + var s []string + for _, v := range {{template "rt" .}} { + s = append(s, fmt.Sprint(v)) + } + return strings.Join(s, ",") +}() {{- else if eq .Schema.GoType "[]float64" -}} -strings.Join({{template "rt" .}}, ",") +func() string { + var s []string + for _, v := range {{template "rt" .}} { + s = append(s, fmt.Sprint(v)) + } + return strings.Join(s, ",") +}() {{- else if eq .Schema.GoType "[]bool" -}} -strings.Join({{template "rt" .}}, ",") +func() string { + var s []string + for _, v := range {{template "rt" .}} { + s = append(s, fmt.Sprint(v)) + } + return strings.Join(s, ",") +}() {{- else -}} fmt.Sprint({{template "rt" .}}) {{- end -}} From 0ac89ab732046a83a303b37f46b0cb4333899a6f Mon Sep 17 00:00:00 2001 From: Karitham Date: Fri, 25 Aug 2023 21:45:35 +0200 Subject: [PATCH 3/9] add interceptors and improve quality of generated code --- examples/petstore-expanded/api/client.go | 71 +++++++++++++++++++----- templates/client.tmpl | 42 +++++++++----- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/examples/petstore-expanded/api/client.go b/examples/petstore-expanded/api/client.go index 310e7e75..2283a4c5 100644 --- a/examples/petstore-expanded/api/client.go +++ b/examples/petstore-expanded/api/client.go @@ -15,16 +15,17 @@ import ( ) type Client struct { - BaseURL string - Client *http.Client + BaseURL string + Client *http.Client + ResponseInterceptor func(*http.Response) error + RequestOptions func(*requests.Builder) *requests.Builder } // FindPets Returns all pets func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error, error) { - req := &requests.Builder{} req = req.Client(c.Client) - req = req.Method("GET") + req = req.Method(http.MethodGet) req = req.BaseURL(c.BaseURL) req = req.Path("/pets") @@ -32,7 +33,7 @@ func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error req = req.Param("tags", strings.Join(p.Tags, ",")) } if p.Limit != nil { - req = req.Param("limit", fmt.Sprint(p.Limit)) + req = req.Param("limit", fmt.Sprint(*p.Limit)) } // define out handlers @@ -89,7 +90,18 @@ func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error return err } - req = req.Handle(requests.ChainHandlers(handle200, handledefault)) + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle200) + handlers = append(handlers, handledefault) + req = req.Handle(requests.ChainHandlers(handlers...)) + + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } err := req.Fetch(ctx) if err != nil { return nil, nil, err @@ -100,10 +112,9 @@ func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error // AddPet Creates a new pet func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) { - req := &requests.Builder{} req = req.Client(c.Client) - req = req.Method("POST") + req = req.Method(http.MethodPost) req = req.BaseURL(c.BaseURL) req = req.Path("/pets") @@ -164,7 +175,18 @@ func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) return err } - req = req.Handle(requests.ChainHandlers(handle201, handledefault)) + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle201) + handlers = append(handlers, handledefault) + req = req.Handle(requests.ChainHandlers(handlers...)) + + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } err := req.Fetch(ctx) if err != nil { return nil, nil, err @@ -175,10 +197,9 @@ func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) // DeletePet Deletes a pet by ID func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { - req := &requests.Builder{} req = req.Client(c.Client) - req = req.Method("DELETE") + req = req.Method(http.MethodDelete) req = req.BaseURL(c.BaseURL) req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) @@ -210,7 +231,17 @@ func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { return err } - req = req.Handle(requests.ChainHandlers(handledefault)) + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handledefault) + req = req.Handle(requests.ChainHandlers(handlers...)) + + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } err := req.Fetch(ctx) if err != nil { return nil, err @@ -221,10 +252,9 @@ func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { // FindPetByID Returns a pet by ID func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error) { - req := &requests.Builder{} req = req.Client(c.Client) - req = req.Method("GET") + req = req.Method(http.MethodGet) req = req.BaseURL(c.BaseURL) req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) @@ -282,7 +312,18 @@ func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error return err } - req = req.Handle(requests.ChainHandlers(handle200, handledefault)) + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle200) + handlers = append(handlers, handledefault) + req = req.Handle(requests.ChainHandlers(handlers...)) + + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } err := req.Fetch(ctx) if err != nil { return nil, nil, err diff --git a/templates/client.tmpl b/templates/client.tmpl index 95979ca1..0ab5ef0b 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -1,13 +1,15 @@ type Client struct { BaseURL string Client *http.Client + ResponseInterceptor func(*http.Response) error + RequestOptions func(*requests.Builder) *requests.Builder } {{define "rt" -}} -{{if or (not (hasPrefix .Schema.GoType "*")) .Schema.SkipOptionalPointer -}} - p.{{.ParamName | ucFirst}} -{{- else -}} +{{if .IndirectOptional -}} *p.{{.ParamName | ucFirst}} +{{- else -}} + p.{{.ParamName | ucFirst}} {{- end -}} {{- end -}} @@ -83,14 +85,13 @@ func (c *Client) {{.OperationID}}(ctx context.Context *{{ .Schema.TypeDecl }}, {{- end -}} error) { - req := &requests.Builder{} req = req.Client(c.Client) - req = req.Method("{{.Method}}") + req = req.Method(http.Method{{.Method | lower | title}}) req = req.BaseURL(c.BaseURL) req = req.Path( {{- if not (eq (.PathParams | len) 0) -}} - strings.NewReplacer({{range .PathParams}} "{ {{- .ParamName -}} }", fmt.Sprint({{.ParamName}}),{{end}}).Replace("{{.Path}}") + strings.NewReplacer({{range .PathParams}} "{ {{- .ParamName -}} }", {{if eq .Schema.GoType "string"}}{{.ParamName}}, {{else}}fmt.Sprint({{.ParamName}}),{{- end -}}{{end}}).Replace("{{.Path}}") {{- else -}} "{{.Path}}" {{- end -}} @@ -98,30 +99,30 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{range .QueryParams -}} {{if .Required -}} - req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}} ) + req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}}) {{else -}} if p.{{.ParamName | ucFirst}} != nil { - req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}} ) + req = req.Param("{{.ParamName}}", {{- template "typeToString" . -}}) } {{end -}} {{end -}} {{range .HeaderParams -}} {{if .Required -}} - req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}} ) + req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}}) {{else -}} if p.{{.ParamName | ucFirst}} != nil { - req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}} ) + req = req.Header("{{.ParamName}}", {{- template "typeToString" . -}}) } {{end -}} {{end -}} {{range .CookieParams -}} {{if .Required -}} - req = req.Cookie("{{.ParamName}}", fmt.Sprint(p.{{.ParamName | ucFirst}})) + req = req.Cookie("{{.ParamName}}", {{- template "typeToString" . -}}) {{else -}} if p.{{.ParamName | ucFirst}} != nil { - req = req.Cookie("{{.ParamName}}", fmt.Sprint(p.{{.ParamName | ucFirst}})) + req = req.Cookie("{{.ParamName}}", {{- template "typeToString" . -}})) } {{end -}} {{end -}} @@ -132,7 +133,9 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{end}} // define out handlers + {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} read := false // flag such that empty responses are kept nil + {{end -}} {{range .GetResponseTypeDefinitions -}} is{{.ResponseName}} := func(resp *http.Response) bool { return resp.StatusCode {{.ResponseName | statusCodeRange}} @@ -162,7 +165,20 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{end -}} - req = req.Handle(requests.ChainHandlers( {{- range .GetResponseTypeDefinitions -}} handle{{.ResponseName}}, {{- end -}})) + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} + {{range .GetResponseTypeDefinitions -}} handlers = append(handlers, handle{{.ResponseName}}) + {{ end -}} + {{end -}} + req = req.Handle(requests.ChainHandlers(handlers...)) + + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } err := req.Fetch(ctx) if err != nil { return {{range .GetResponseTypeDefinitions }}nil, {{ end }} err From 93ed632c168419985bfe132e24ba84487dc89943 Mon Sep 17 00:00:00 2001 From: Karitham Date: Fri, 25 Aug 2023 21:49:24 +0200 Subject: [PATCH 4/9] simplify generated code again --- examples/petstore-expanded/api/client.go | 35 +++++------------------- templates/client.tmpl | 5 +--- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/examples/petstore-expanded/api/client.go b/examples/petstore-expanded/api/client.go index 2283a4c5..e93dea09 100644 --- a/examples/petstore-expanded/api/client.go +++ b/examples/petstore-expanded/api/client.go @@ -38,12 +38,9 @@ func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error // define out handlers read := false // flag such that empty responses are kept nil - is200 := func(resp *http.Response) bool { - return resp.StatusCode == 200 - } var _200 *[]Pet handle200 := func(resp *http.Response) error { - if !is200(resp) { + if resp.StatusCode == 200 { return nil } @@ -64,12 +61,9 @@ func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error return err } - isdefault := func(resp *http.Response) bool { - return resp.StatusCode != 0 - } var _default *Error handledefault := func(resp *http.Response) error { - if !isdefault(resp) { + if resp.StatusCode != 0 { return nil } @@ -123,12 +117,9 @@ func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) // define out handlers read := false // flag such that empty responses are kept nil - is201 := func(resp *http.Response) bool { - return resp.StatusCode == 201 - } var _201 *Pet handle201 := func(resp *http.Response) error { - if !is201(resp) { + if resp.StatusCode == 201 { return nil } @@ -149,12 +140,9 @@ func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) return err } - isdefault := func(resp *http.Response) bool { - return resp.StatusCode != 0 - } var _default *Error handledefault := func(resp *http.Response) error { - if !isdefault(resp) { + if resp.StatusCode != 0 { return nil } @@ -205,12 +193,9 @@ func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { // define out handlers read := false // flag such that empty responses are kept nil - isdefault := func(resp *http.Response) bool { - return resp.StatusCode != 0 - } var _default *Error handledefault := func(resp *http.Response) error { - if !isdefault(resp) { + if resp.StatusCode != 0 { return nil } @@ -260,12 +245,9 @@ func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error // define out handlers read := false // flag such that empty responses are kept nil - is200 := func(resp *http.Response) bool { - return resp.StatusCode == 200 - } var _200 *Pet handle200 := func(resp *http.Response) error { - if !is200(resp) { + if resp.StatusCode == 200 { return nil } @@ -286,12 +268,9 @@ func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error return err } - isdefault := func(resp *http.Response) bool { - return resp.StatusCode != 0 - } var _default *Error handledefault := func(resp *http.Response) error { - if !isdefault(resp) { + if resp.StatusCode != 0 { return nil } diff --git a/templates/client.tmpl b/templates/client.tmpl index 0ab5ef0b..129f1cc9 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -137,12 +137,9 @@ func (c *Client) {{.OperationID}}(ctx context.Context read := false // flag such that empty responses are kept nil {{end -}} {{range .GetResponseTypeDefinitions -}} - is{{.ResponseName}} := func(resp *http.Response) bool { - return resp.StatusCode {{.ResponseName | statusCodeRange}} - } var _{{.ResponseName}} *{{.Schema.TypeDecl}} handle{{.ResponseName}} := func(resp *http.Response) error { - if !is{{.ResponseName}}(resp) { + if resp.StatusCode {{.ResponseName | statusCodeRange}} { return nil } From 9705cfe73349380e2916f486053399a46d2a7edf Mon Sep 17 00:00:00 2001 From: Karitham Date: Fri, 25 Aug 2023 21:53:53 +0200 Subject: [PATCH 5/9] fix handling of status code ranges --- codegen/template_helpers.go | 13 ++++++------- templates/client.tmpl | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/codegen/template_helpers.go b/codegen/template_helpers.go index fbaeee18..beef80ed 100644 --- a/codegen/template_helpers.go +++ b/codegen/template_helpers.go @@ -104,11 +104,11 @@ func responseNameToStatusCode(responseName string) string { func responseToStatusRangeString(responseName string) string { switch strings.ToUpper(responseName) { case "DEFAULT": - return "!= 0" + return "resp.StatusCode != 0" case "1XX", "2XX", "3XX", "4XX", "5XX": - return fmt.Sprintf(">= %s00 && < %s99", responseName[:1], responseName[:1]) + return fmt.Sprintf("resp.StatusCode >= %s00 && resp.StatusCode <= %s99", responseName[:1], responseName[:1]) } - return fmt.Sprintf("== %s", responseName) + return fmt.Sprintf("resp.StatusCode == %s", responseName) } // TitleWord converts a single worded string to title case. @@ -136,8 +136,7 @@ var TemplateFunctions = template.FuncMap{ "statusCode": responseNameToStatusCode, "statusCodeRange": responseToStatusRangeString, - "hasPrefix": strings.HasPrefix, - "ucFirst": snaker.ForceCamelIdentifier, - "lower": strings.ToLower, - "title": TitleWord, + "ucFirst": snaker.ForceCamelIdentifier, + "lower": strings.ToLower, + "title": TitleWord, } diff --git a/templates/client.tmpl b/templates/client.tmpl index 129f1cc9..232214fe 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -139,7 +139,7 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{range .GetResponseTypeDefinitions -}} var _{{.ResponseName}} *{{.Schema.TypeDecl}} handle{{.ResponseName}} := func(resp *http.Response) error { - if resp.StatusCode {{.ResponseName | statusCodeRange}} { + if {{.ResponseName | statusCodeRange}} { return nil } From 179337d331ff8555787770e5cb7c27e117afeffd Mon Sep 17 00:00:00 2001 From: Karitham Date: Sat, 26 Aug 2023 11:08:40 +0000 Subject: [PATCH 6/9] typecheck generated client in tests --- codegen/codegen_test.go | 95 +++++++++++++++++++++++++++++++++++++++++ go.mod | 8 ++-- go.sum | 8 ++++ templates/client.tmpl | 18 ++++---- 4 files changed, 116 insertions(+), 13 deletions(-) diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 2516f390..461c3c44 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -2,10 +2,14 @@ package codegen import ( "go/format" + "go/types" + "net/url" + "os" "testing" "text/template" examplePetstore "github.com/discord-gophers/goapi-gen/examples/petstore-expanded/api" + "golang.org/x/tools/go/loader" "github.com/discord-gophers/goapi-gen/templates" "github.com/getkin/kin-openapi/openapi3" @@ -439,3 +443,94 @@ func (t *MyType) FromValue(value int64) error { }) } } + +func TestGenerateClientFormat(t *testing.T) { + // Input vars for code generation: + opts := Options{ + GenerateClient: true, + GenerateTypes: true, + } + + // Get a spec from the example PetStore definition: + swagger, err := examplePetstore.GetSwagger() + assert.NoError(t, err) + + // Run our code generation: + code, err := Generate(swagger, "petstore", opts) + assert.NoError(t, err) + assert.NotEmpty(t, code) + + // Check that we have valid (formattable) code: + // Check that we have valid (formattable) code: + _, err = format.Source([]byte(code)) + assert.NoError(t, err) + + // Check that we have a package: + assert.Contains(t, code, "package petstore") + assert.Contains(t, code, `"github.com/carlmjohnson/requests"`) + assert.Contains(t, code, "type Client struct") + + c := loader.Config{} + as, err := c.ParseFile("petstore.gen.go", code) + assert.NoError(t, err) + c.CreateFromFiles("petstore", as) + + p, err := c.Load() + assert.NoError(t, err) + + pkg := p.Package("petstore") + assert.NotNil(t, pkg) + + client := pkg.Pkg.Scope().Lookup("Client") + assert.NotNil(t, client) + + assert.True(t, client.Exported()) + assert.Equal(t, client.Type().Underlying().(*types.Struct).NumFields(), 4) +} + +func TestGenerateK8sClient(t *testing.T) { + uri, err := url.Parse("https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/v3/api__v1_openapi.json") + assert.NoError(t, err) + swagger, err := openapi3.NewLoader().LoadFromURI(uri) + assert.NoError(t, err) + + // Run our code generation: + code, err := Generate(swagger, "k8s", Options{ + GenerateClient: true, + GenerateTypes: true, + }) + + assert.NoError(t, err) + assert.NotEmpty(t, code) + + // Check that we have valid (formattable) code: + _, err = format.Source([]byte(code)) + assert.NoError(t, err) + + // Check that we have a package: + assert.Contains(t, code, "package k8s") + assert.Contains(t, code, `"github.com/carlmjohnson/requests"`) + assert.Contains(t, code, "type Client struct") + + c := loader.Config{} + as, err := c.ParseFile("k8s.gen.go", code) + assert.NoError(t, err) + c.CreateFromFiles("k8s", as) + + p, err := c.Load() + assert.NoError(t, err) + + pkg := p.Package("k8s") + assert.NotNil(t, pkg) + + client := pkg.Pkg.Scope().Lookup("Client") + assert.NotNil(t, client) + + assert.True(t, client.Exported()) + assert.Equal(t, client.Type().Underlying().(*types.Struct).NumFields(), 4) + + if of := os.Getenv("OUT_FILE"); of != "" { + assert.NoError(t, os.WriteFile(of, []byte(code), 0644)) + } + +} diff --git a/go.mod b/go.mod index 1edf7f32..0e4a687a 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,11 @@ require ( github.com/matryer/moq v0.3.1 github.com/stretchr/testify v1.7.0 github.com/urfave/cli/v2 v2.3.0 - golang.org/x/tools v0.3.0 + golang.org/x/tools v0.12.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) -require golang.org/x/net v0.7.0 // indirect +require golang.org/x/net v0.14.0 // indirect require ( github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect @@ -27,8 +27,8 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.11.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 32d4b592..a6b134c4 100644 --- a/go.sum +++ b/go.sum @@ -63,12 +63,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -81,6 +85,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -95,6 +101,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/templates/client.tmpl b/templates/client.tmpl index 232214fe..a42c07c4 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -136,10 +136,10 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} read := false // flag such that empty responses are kept nil {{end -}} - {{range .GetResponseTypeDefinitions -}} - var _{{.ResponseName}} *{{.Schema.TypeDecl}} - handle{{.ResponseName}} := func(resp *http.Response) error { - if {{.ResponseName | statusCodeRange}} { + {{range $i, $s := .GetResponseTypeDefinitions -}} + var _{{$s.ResponseName}}_{{$i}} *{{$s.Schema.TypeDecl}} + handle{{$s.ResponseName}}_{{$i}} := func(resp *http.Response) error { + if {{$s.ResponseName | statusCodeRange}} { return nil } @@ -148,13 +148,13 @@ func (c *Client) {{.OperationID}}(ctx context.Context } read = true - _{{.ResponseName}} = new({{.Schema.TypeDecl}}) - err := json.NewDecoder(resp.Body).Decode(_{{.ResponseName}}) + _{{$s.ResponseName}}_{{$i}} = new({{$s.Schema.TypeDecl}}) + err := json.NewDecoder(resp.Body).Decode(_{{$s.ResponseName}}_{{$i}}) switch err { case nil: return nil case io.EOF: - _{{.ResponseName}} = nil + _{{$s.ResponseName}}_{{$i}} = nil return nil } return err @@ -168,7 +168,7 @@ func (c *Client) {{.OperationID}}(ctx context.Context } {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} - {{range .GetResponseTypeDefinitions -}} handlers = append(handlers, handle{{.ResponseName}}) + {{range $i, $s := .GetResponseTypeDefinitions -}} handlers = append(handlers, handle{{$s.ResponseName}}_{{$i}}) {{ end -}} {{end -}} req = req.Handle(requests.ChainHandlers(handlers...)) @@ -181,7 +181,7 @@ func (c *Client) {{.OperationID}}(ctx context.Context return {{range .GetResponseTypeDefinitions }}nil, {{ end }} err } - return {{range .GetResponseTypeDefinitions }}_{{.ResponseName}}, {{ end }} nil + return {{range $i, $s := .GetResponseTypeDefinitions }}_{{.ResponseName}}_{{$i}}, {{ end }} nil } {{ end -}} \ No newline at end of file From ce31127aa644c5e16beebe866b1c5014de7466cb Mon Sep 17 00:00:00 2001 From: Karitham Date: Sat, 26 Aug 2023 11:40:09 +0000 Subject: [PATCH 7/9] use non deprecated type-checking --- codegen/codegen_test.go | 78 +++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 461c3c44..11fdfe5e 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -2,6 +2,7 @@ package codegen import ( "go/format" + "go/token" "go/types" "net/url" "os" @@ -9,7 +10,7 @@ import ( "text/template" examplePetstore "github.com/discord-gophers/goapi-gen/examples/petstore-expanded/api" - "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" "github.com/discord-gophers/goapi-gen/templates" "github.com/getkin/kin-openapi/openapi3" @@ -458,34 +459,31 @@ func TestGenerateClientFormat(t *testing.T) { // Run our code generation: code, err := Generate(swagger, "petstore", opts) assert.NoError(t, err) - assert.NotEmpty(t, code) - - // Check that we have valid (formattable) code: - // Check that we have valid (formattable) code: - _, err = format.Source([]byte(code)) - assert.NoError(t, err) - // Check that we have a package: - assert.Contains(t, code, "package petstore") - assert.Contains(t, code, `"github.com/carlmjohnson/requests"`) - assert.Contains(t, code, "type Client struct") - - c := loader.Config{} - as, err := c.ParseFile("petstore.gen.go", code) - assert.NoError(t, err) - c.CreateFromFiles("petstore", as) - - p, err := c.Load() + tfs := token.NewFileSet() + tfs.AddFile("petstore.gen.go", tfs.Base(), len(code)) + cwd, _ := os.Getwd() + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedImports, + Fset: tfs, + Overlay: map[string][]byte{ + cwd + "/petstore/petstore.gen.go": []byte(code), + }, + }, "./petstore") assert.NoError(t, err) + assert.Len(t, pkgs, 1) + pkg := pkgs[0] + assert.Empty(t, pkg.Errors) - pkg := p.Package("petstore") - assert.NotNil(t, pkg) + // check we import the requests package + _, ok := pkg.Imports["github.com/carlmjohnson/requests"] + assert.True(t, ok) - client := pkg.Pkg.Scope().Lookup("Client") + client := pkg.Types.Scope().Lookup("Client") assert.NotNil(t, client) assert.True(t, client.Exported()) - assert.Equal(t, client.Type().Underlying().(*types.Struct).NumFields(), 4) + assert.Equal(t, 4, client.Type().Underlying().(*types.Struct).NumFields()) } func TestGenerateK8sClient(t *testing.T) { @@ -503,27 +501,26 @@ func TestGenerateK8sClient(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, code) - // Check that we have valid (formattable) code: - _, err = format.Source([]byte(code)) - assert.NoError(t, err) - - // Check that we have a package: - assert.Contains(t, code, "package k8s") - assert.Contains(t, code, `"github.com/carlmjohnson/requests"`) - assert.Contains(t, code, "type Client struct") - - c := loader.Config{} - as, err := c.ParseFile("k8s.gen.go", code) - assert.NoError(t, err) - c.CreateFromFiles("k8s", as) - - p, err := c.Load() + tfs := token.NewFileSet() + tfs.AddFile("petstore.gen.go", tfs.Base(), len(code)) + cwd, _ := os.Getwd() + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedImports, + Fset: tfs, + Overlay: map[string][]byte{ + cwd + "/petstore/petstore.gen.go": []byte(code), + }, + }, "./petstore") assert.NoError(t, err) + assert.Len(t, pkgs, 1) + pkg := pkgs[0] + assert.Empty(t, pkg.Errors) - pkg := p.Package("k8s") - assert.NotNil(t, pkg) + // check we import the requests package + _, ok := pkg.Imports["github.com/carlmjohnson/requests"] + assert.True(t, ok) - client := pkg.Pkg.Scope().Lookup("Client") + client := pkg.Types.Scope().Lookup("Client") assert.NotNil(t, client) assert.True(t, client.Exported()) @@ -532,5 +529,4 @@ func TestGenerateK8sClient(t *testing.T) { if of := os.Getenv("OUT_FILE"); of != "" { assert.NoError(t, os.WriteFile(of, []byte(code), 0644)) } - } From 3a80c3c52a0720de99b649616c486e29592017fb Mon Sep 17 00:00:00 2001 From: Karitham Date: Sat, 26 Aug 2023 11:49:33 +0000 Subject: [PATCH 8/9] fix go mod tidy --- go.sum | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/go.sum b/go.sum index a6b134c4..de920a2f 100644 --- a/go.sum +++ b/go.sum @@ -60,47 +60,54 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 25f151589d5e894c86261fba1685b64a6a948b2f Mon Sep 17 00:00:00 2001 From: Karitham Date: Sat, 26 Aug 2023 14:03:24 +0000 Subject: [PATCH 9/9] add comments and fix multiple generation for different content-types we only handle json. users can handle other content types with custom response handlers --- codegen/codegen_test.go | 7 +- codegen/template_helpers.go | 10 +- examples/petstore-expanded/api/client.go | 312 ---------------- .../petstore-expanded/api/petstore.gen.go | 332 ++++++++++++++++++ examples/petstore-expanded/api/petstore.go | 2 +- templates/client.tmpl | 60 +++- 6 files changed, 389 insertions(+), 334 deletions(-) delete mode 100644 examples/petstore-expanded/api/client.go diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index 11fdfe5e..e9ed8f28 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -497,6 +497,9 @@ func TestGenerateK8sClient(t *testing.T) { GenerateClient: true, GenerateTypes: true, }) + if of := os.Getenv("OUT_FILE"); of != "" { + assert.NoError(t, os.WriteFile(of, []byte(code), 0644)) + } assert.NoError(t, err) assert.NotEmpty(t, code) @@ -525,8 +528,4 @@ func TestGenerateK8sClient(t *testing.T) { assert.True(t, client.Exported()) assert.Equal(t, client.Type().Underlying().(*types.Struct).NumFields(), 4) - - if of := os.Getenv("OUT_FILE"); of != "" { - assert.NoError(t, os.WriteFile(of, []byte(code), 0644)) - } } diff --git a/codegen/template_helpers.go b/codegen/template_helpers.go index beef80ed..766fde0f 100644 --- a/codegen/template_helpers.go +++ b/codegen/template_helpers.go @@ -106,9 +106,9 @@ func responseToStatusRangeString(responseName string) string { case "DEFAULT": return "resp.StatusCode != 0" case "1XX", "2XX", "3XX", "4XX", "5XX": - return fmt.Sprintf("resp.StatusCode >= %s00 && resp.StatusCode <= %s99", responseName[:1], responseName[:1]) + return fmt.Sprintf("resp.StatusCode < %s00 && resp.StatusCode > %s99", responseName[:1], responseName[:1]) } - return fmt.Sprintf("resp.StatusCode == %s", responseName) + return fmt.Sprintf("resp.StatusCode != %s", responseName) } // TitleWord converts a single worded string to title case. @@ -122,6 +122,11 @@ func TitleWord(s string) string { return string(r) } +// ToComment converts a string to a comment. +func ToComment(s string) string { + return "// " + strings.ReplaceAll(s, "\n", "\n// ") +} + // TemplateFunctions generates the list of utlity and helpfer functions used by // the templates. var TemplateFunctions = template.FuncMap{ @@ -130,6 +135,7 @@ var TemplateFunctions = template.FuncMap{ "getResponseTypeDefinitions": getResponseTypeDefinitions, "genTaggedMiddleware": getTaggedMiddlewares, "toStringArray": toStringArray, + "toComment": ToComment, "swaggerURIToChiURI": SwaggerURIToChiURI, diff --git a/examples/petstore-expanded/api/client.go b/examples/petstore-expanded/api/client.go deleted file mode 100644 index e93dea09..00000000 --- a/examples/petstore-expanded/api/client.go +++ /dev/null @@ -1,312 +0,0 @@ -// Package api provides primitives to interact with the openapi HTTP API. -// -// Code generated by github.com/discord-gophers/goapi-gen version (devel) DO NOT EDIT. -package api - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - - "github.com/carlmjohnson/requests" -) - -type Client struct { - BaseURL string - Client *http.Client - ResponseInterceptor func(*http.Response) error - RequestOptions func(*requests.Builder) *requests.Builder -} - -// FindPets Returns all pets -func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error, error) { - req := &requests.Builder{} - req = req.Client(c.Client) - req = req.Method(http.MethodGet) - req = req.BaseURL(c.BaseURL) - req = req.Path("/pets") - - if p.Tags != nil { - req = req.Param("tags", strings.Join(p.Tags, ",")) - } - if p.Limit != nil { - req = req.Param("limit", fmt.Sprint(*p.Limit)) - } - - // define out handlers - read := false // flag such that empty responses are kept nil - var _200 *[]Pet - handle200 := func(resp *http.Response) error { - if resp.StatusCode == 200 { - return nil - } - - if read { - return nil - } - read = true - - _200 = new([]Pet) - err := json.NewDecoder(resp.Body).Decode(_200) - switch err { - case nil: - return nil - case io.EOF: - _200 = nil - return nil - } - return err - } - - var _default *Error - handledefault := func(resp *http.Response) error { - if resp.StatusCode != 0 { - return nil - } - - if read { - return nil - } - read = true - - _default = new(Error) - err := json.NewDecoder(resp.Body).Decode(_default) - switch err { - case nil: - return nil - case io.EOF: - _default = nil - return nil - } - return err - } - - handlers := []func(*http.Response) error{} - if c.ResponseInterceptor != nil { - handlers = append(handlers, c.ResponseInterceptor) - } - - handlers = append(handlers, handle200) - handlers = append(handlers, handledefault) - req = req.Handle(requests.ChainHandlers(handlers...)) - - if c.RequestOptions != nil { - req = c.RequestOptions(req) - } - err := req.Fetch(ctx) - if err != nil { - return nil, nil, err - } - - return _200, _default, nil -} - -// AddPet Creates a new pet -func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) { - req := &requests.Builder{} - req = req.Client(c.Client) - req = req.Method(http.MethodPost) - req = req.BaseURL(c.BaseURL) - req = req.Path("/pets") - - req = req.BodyJSON(body) - req = req.ContentType("application/json") - - // define out handlers - read := false // flag such that empty responses are kept nil - var _201 *Pet - handle201 := func(resp *http.Response) error { - if resp.StatusCode == 201 { - return nil - } - - if read { - return nil - } - read = true - - _201 = new(Pet) - err := json.NewDecoder(resp.Body).Decode(_201) - switch err { - case nil: - return nil - case io.EOF: - _201 = nil - return nil - } - return err - } - - var _default *Error - handledefault := func(resp *http.Response) error { - if resp.StatusCode != 0 { - return nil - } - - if read { - return nil - } - read = true - - _default = new(Error) - err := json.NewDecoder(resp.Body).Decode(_default) - switch err { - case nil: - return nil - case io.EOF: - _default = nil - return nil - } - return err - } - - handlers := []func(*http.Response) error{} - if c.ResponseInterceptor != nil { - handlers = append(handlers, c.ResponseInterceptor) - } - - handlers = append(handlers, handle201) - handlers = append(handlers, handledefault) - req = req.Handle(requests.ChainHandlers(handlers...)) - - if c.RequestOptions != nil { - req = c.RequestOptions(req) - } - err := req.Fetch(ctx) - if err != nil { - return nil, nil, err - } - - return _201, _default, nil -} - -// DeletePet Deletes a pet by ID -func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { - req := &requests.Builder{} - req = req.Client(c.Client) - req = req.Method(http.MethodDelete) - req = req.BaseURL(c.BaseURL) - req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) - - // define out handlers - read := false // flag such that empty responses are kept nil - var _default *Error - handledefault := func(resp *http.Response) error { - if resp.StatusCode != 0 { - return nil - } - - if read { - return nil - } - read = true - - _default = new(Error) - err := json.NewDecoder(resp.Body).Decode(_default) - switch err { - case nil: - return nil - case io.EOF: - _default = nil - return nil - } - return err - } - - handlers := []func(*http.Response) error{} - if c.ResponseInterceptor != nil { - handlers = append(handlers, c.ResponseInterceptor) - } - - handlers = append(handlers, handledefault) - req = req.Handle(requests.ChainHandlers(handlers...)) - - if c.RequestOptions != nil { - req = c.RequestOptions(req) - } - err := req.Fetch(ctx) - if err != nil { - return nil, err - } - - return _default, nil -} - -// FindPetByID Returns a pet by ID -func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error) { - req := &requests.Builder{} - req = req.Client(c.Client) - req = req.Method(http.MethodGet) - req = req.BaseURL(c.BaseURL) - req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) - - // define out handlers - read := false // flag such that empty responses are kept nil - var _200 *Pet - handle200 := func(resp *http.Response) error { - if resp.StatusCode == 200 { - return nil - } - - if read { - return nil - } - read = true - - _200 = new(Pet) - err := json.NewDecoder(resp.Body).Decode(_200) - switch err { - case nil: - return nil - case io.EOF: - _200 = nil - return nil - } - return err - } - - var _default *Error - handledefault := func(resp *http.Response) error { - if resp.StatusCode != 0 { - return nil - } - - if read { - return nil - } - read = true - - _default = new(Error) - err := json.NewDecoder(resp.Body).Decode(_default) - switch err { - case nil: - return nil - case io.EOF: - _default = nil - return nil - } - return err - } - - handlers := []func(*http.Response) error{} - if c.ResponseInterceptor != nil { - handlers = append(handlers, c.ResponseInterceptor) - } - - handlers = append(handlers, handle200) - handlers = append(handlers, handledefault) - req = req.Handle(requests.ChainHandlers(handlers...)) - - if c.RequestOptions != nil { - req = c.RequestOptions(req) - } - err := req.Fetch(ctx) - if err != nil { - return nil, nil, err - } - - return _200, _default, nil -} diff --git a/examples/petstore-expanded/api/petstore.gen.go b/examples/petstore-expanded/api/petstore.gen.go index 7496c30c..2fdbfc84 100644 --- a/examples/petstore-expanded/api/petstore.gen.go +++ b/examples/petstore-expanded/api/petstore.gen.go @@ -6,15 +6,18 @@ package api import ( "bytes" "compress/gzip" + "context" "encoding/base64" "encoding/json" "encoding/xml" "fmt" + "io" "net/http" "net/url" "path" "strings" + "github.com/carlmjohnson/requests" "github.com/discord-gophers/goapi-gen/runtime" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" @@ -446,6 +449,335 @@ func WithErrorHandler(handler func(w http.ResponseWriter, r *http.Request, err e } } +// Client is an auto-generated client +type Client struct { + // BaseURL is the base URL of the API (e.g. https://api.example.com/v1) + BaseURL string + + // Client is an http client used to communicate with the API + // If nil, http.DefaultClient will be used + // To handle authorization to a remote API, use an http client with a custom transport. + // See https://pkg.go.dev/net/http#RoundTripper and https://pkg.go.dev/golang.org/x/oauth2#NewClient + Client *http.Client + + // ResponseInterceptor is a function that will be called on every response made by this client + // in the case where the API implicitly returns an error not defined in the spec, you can handle it here. + ResponseInterceptor func(*http.Response) error + + // RequestOptions allows you to set custom options on each http request before it's sent. + // This is another way to set authorization headers, for example. + RequestOptions func(*requests.Builder) *requests.Builder +} + +// FindPets +// Returns all pets from the system that the user has access to +// Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia. +// +// Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien. +func (c *Client) FindPets(ctx context.Context, p FindPetsParams) (*[]Pet, *Error, error) { + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method(http.MethodGet) + req = req.BaseURL(c.BaseURL) + req = req.Path("/pets") + + if p.Tags != nil { + req = req.Param("tags", strings.Join(p.Tags, ",")) + } + if p.Limit != nil { + req = req.Param("limit", fmt.Sprint(*p.Limit)) + } + + // define out handlers + req = req.Accept("application/json") + + read := false // flag such that empty responses are kept nil + var _200 *[]Pet + handle200 := func(resp *http.Response) error { + if resp.StatusCode != 200 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _200 = new([]Pet) + err := json.NewDecoder(resp.Body).Decode(_200) + switch err { + case nil: + return nil + case io.EOF: + _200 = nil + return nil + } + return err + } + var _default *Error + handledefault := func(resp *http.Response) error { + if resp.StatusCode != 0 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle200) + handlers = append(handlers, handledefault) + + req = req.Handle(requests.ChainHandlers(handlers...)) + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } + + err := req.Fetch(ctx) + if err != nil { + return _200, _default, err + } + + return _200, _default, nil +} + +// AddPet +// Creates a new pet in the store. Duplicates are allowed +func (c *Client) AddPet(ctx context.Context, body NewPet) (*Pet, *Error, error) { + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method(http.MethodPost) + req = req.BaseURL(c.BaseURL) + req = req.Path("/pets") + + req = req.BodyJSON(body) + req = req.ContentType("application/json") + + // define out handlers + req = req.Accept("application/json") + + read := false // flag such that empty responses are kept nil + var _201 *Pet + handle201 := func(resp *http.Response) error { + if resp.StatusCode != 201 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _201 = new(Pet) + err := json.NewDecoder(resp.Body).Decode(_201) + switch err { + case nil: + return nil + case io.EOF: + _201 = nil + return nil + } + return err + } + var _default *Error + handledefault := func(resp *http.Response) error { + if resp.StatusCode != 0 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle201) + handlers = append(handlers, handledefault) + + req = req.Handle(requests.ChainHandlers(handlers...)) + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } + + err := req.Fetch(ctx) + if err != nil { + return _201, _default, err + } + + return _201, _default, nil +} + +// DeletePet +// deletes a single pet based on the ID supplied +func (c *Client) DeletePet(ctx context.Context, id int64) (*Error, error) { + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method(http.MethodDelete) + req = req.BaseURL(c.BaseURL) + req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) + + // define out handlers + req = req.Accept("application/json") + + read := false // flag such that empty responses are kept nil + var _default *Error + handledefault := func(resp *http.Response) error { + if resp.StatusCode != 0 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handledefault) + + req = req.Handle(requests.ChainHandlers(handlers...)) + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } + + err := req.Fetch(ctx) + if err != nil { + return _default, err + } + + return _default, nil +} + +// FindPetByID +// Returns a pet based on a single ID +func (c *Client) FindPetByID(ctx context.Context, id int64) (*Pet, *Error, error) { + req := &requests.Builder{} + req = req.Client(c.Client) + req = req.Method(http.MethodGet) + req = req.BaseURL(c.BaseURL) + req = req.Path(strings.NewReplacer("{id}", fmt.Sprint(id)).Replace("/pets/{id}")) + + // define out handlers + req = req.Accept("application/json") + + read := false // flag such that empty responses are kept nil + var _200 *Pet + handle200 := func(resp *http.Response) error { + if resp.StatusCode != 200 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _200 = new(Pet) + err := json.NewDecoder(resp.Body).Decode(_200) + switch err { + case nil: + return nil + case io.EOF: + _200 = nil + return nil + } + return err + } + var _default *Error + handledefault := func(resp *http.Response) error { + if resp.StatusCode != 0 || + !strings.Contains(resp.Header.Get("Content-Type"), "application/json") { + return nil + } + + if read { + return nil + } + read = true + + _default = new(Error) + err := json.NewDecoder(resp.Body).Decode(_default) + switch err { + case nil: + return nil + case io.EOF: + _default = nil + return nil + } + return err + } + handlers := []func(*http.Response) error{} + if c.ResponseInterceptor != nil { + handlers = append(handlers, c.ResponseInterceptor) + } + + handlers = append(handlers, handle200) + handlers = append(handlers, handledefault) + + req = req.Handle(requests.ChainHandlers(handlers...)) + if c.RequestOptions != nil { + req = c.RequestOptions(req) + } + + err := req.Fetch(ctx) + if err != nil { + return _200, _default, err + } + + return _200, _default, nil +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ diff --git a/examples/petstore-expanded/api/petstore.go b/examples/petstore-expanded/api/petstore.go index fb6c2810..4230e16c 100644 --- a/examples/petstore-expanded/api/petstore.go +++ b/examples/petstore-expanded/api/petstore.go @@ -1,4 +1,4 @@ -//go:generate go run github.com/discord-gophers/goapi-gen --package=api --generate types,server,spec -o petstore.gen.go ../petstore-expanded.yaml +//go:generate go run github.com/discord-gophers/goapi-gen --package=api --generate client,types,server,spec -o petstore.gen.go ../petstore-expanded.yaml package api diff --git a/templates/client.tmpl b/templates/client.tmpl index a42c07c4..19736f13 100644 --- a/templates/client.tmpl +++ b/templates/client.tmpl @@ -1,7 +1,20 @@ +// Client is an auto-generated client type Client struct { + // BaseURL is the base URL of the API (e.g. https://api.example.com/v1) BaseURL string + + // Client is an http client used to communicate with the API + // If nil, http.DefaultClient will be used + // To handle authorization to a remote API, use an http client with a custom transport. + // See https://pkg.go.dev/net/http#RoundTripper and https://pkg.go.dev/golang.org/x/oauth2#NewClient Client *http.Client + + // ResponseInterceptor is a function that will be called on every response made by this client + // in the case where the API implicitly returns an error not defined in the spec, you can handle it here. ResponseInterceptor func(*http.Response) error + + // RequestOptions allows you to set custom options on each http request before it's sent. + // This is another way to set authorization headers, for example. RequestOptions func(*requests.Builder) *requests.Builder } @@ -76,14 +89,19 @@ fmt.Sprint({{template "rt" .}}) {{range .Operations -}} -// {{.OperationID}} {{.Summary}} +{{.OperationID | toComment}} +{{.Spec.Description | toComment}} +{{- if .Spec.Deprecated }} +// Deprectated: this route is marked deprecated.{{end}} func (c *Client) {{.OperationID}}(ctx context.Context - {{- if not (eq (len .Params) 0) -}}, p {{.OperationID}}Params {{- end -}} {{- range .PathParams}}, {{.ParamName}} {{.Schema.TypeDecl}}{{end}} {{- if not (eq (len .Bodies) 0) -}}, body {{(index .Bodies 0 ).Schema.GoType}} {{- end -}} + {{- if not (eq (len .Params) 0) -}}, p {{.OperationID}}Params {{- end -}} ) ({{- range .GetResponseTypeDefinitions -}} + {{- if eq .ContentTypeName "application/json" -}} *{{ .Schema.TypeDecl }}, {{- end -}} + {{- end -}} error) { req := &requests.Builder{} req = req.Client(c.Client) @@ -129,17 +147,21 @@ func (c *Client) {{.OperationID}}(ctx context.Context {{if not (eq (len .Bodies) 0) -}} req = req.BodyJSON(body) - req = req.ContentType({{ range .Bodies}}"{{.ContentType}}",{{end}}) + req = req.ContentType("application/json") {{end}} // define out handlers {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} + req = req.Accept("application/json") + read := false // flag such that empty responses are kept nil {{end -}} - {{range $i, $s := .GetResponseTypeDefinitions -}} - var _{{$s.ResponseName}}_{{$i}} *{{$s.Schema.TypeDecl}} - handle{{$s.ResponseName}}_{{$i}} := func(resp *http.Response) error { - if {{$s.ResponseName | statusCodeRange}} { + {{range .GetResponseTypeDefinitions -}} + {{- if eq .ContentTypeName "application/json" -}} + var _{{.ResponseName}} *{{.Schema.TypeDecl}} + handle{{.ResponseName}} := func(resp *http.Response) error { + if {{.ResponseName | statusCodeRange}} || + !strings.Contains(resp.Header.Get("Content-Type"), "{{.ContentTypeName}}") { return nil } @@ -148,18 +170,18 @@ func (c *Client) {{.OperationID}}(ctx context.Context } read = true - _{{$s.ResponseName}}_{{$i}} = new({{$s.Schema.TypeDecl}}) - err := json.NewDecoder(resp.Body).Decode(_{{$s.ResponseName}}_{{$i}}) + _{{.ResponseName}} = new({{.Schema.TypeDecl}}) + err := json.NewDecoder(resp.Body).Decode(_{{.ResponseName}}) switch err { case nil: return nil case io.EOF: - _{{$s.ResponseName}}_{{$i}} = nil + _{{.ResponseName}} = nil return nil } return err } - + {{end -}} {{end -}} handlers := []func(*http.Response) error{} @@ -168,20 +190,28 @@ func (c *Client) {{.OperationID}}(ctx context.Context } {{if not (eq (len .GetResponseTypeDefinitions) 0) -}} - {{range $i, $s := .GetResponseTypeDefinitions -}} handlers = append(handlers, handle{{$s.ResponseName}}_{{$i}}) + {{range .GetResponseTypeDefinitions -}} + {{- if eq .ContentTypeName "application/json" -}} + handlers = append(handlers, handle{{.ResponseName}}) {{ end -}} {{end -}} - req = req.Handle(requests.ChainHandlers(handlers...)) + {{end}} + req = req.Handle(requests.ChainHandlers(handlers...)) if c.RequestOptions != nil { req = c.RequestOptions(req) } + err := req.Fetch(ctx) if err != nil { - return {{range .GetResponseTypeDefinitions }}nil, {{ end }} err + return {{range .GetResponseTypeDefinitions }} + {{- if eq .ContentTypeName "application/json" -}} + _{{.ResponseName}},{{end}}{{ end }} err } - return {{range $i, $s := .GetResponseTypeDefinitions }}_{{.ResponseName}}_{{$i}}, {{ end }} nil + return {{range .GetResponseTypeDefinitions }} + {{- if eq .ContentTypeName "application/json" -}} + _{{.ResponseName}},{{end}}{{ end }} nil } {{ end -}} \ No newline at end of file