Skip to content

Commit bf7b4e6

Browse files
authored
feat: Add retry to API requests (#141)
Looks like this is the only way to inject the retry client, there's also oapi-codegen/oapi-codegen#150 which is in progress via oapi-codegen/oapi-codegen#156
1 parent 0eb7391 commit bf7b4e6

File tree

5 files changed

+327
-0
lines changed

5 files changed

+327
-0
lines changed

client.gen.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ package: cloudquery_api
22
generate:
33
client: true
44
output: client.gen.go
5+
output-options:
6+
user-templates:
7+
# Based on https://github.com/deepmap/oapi-codegen/blob/v1.15.0/pkg/codegen/templates/client.tmpl
8+
client.tmpl: templates/client.tmpl

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.21.0
55
require (
66
github.com/adrg/xdg v0.4.0
77
github.com/deepmap/oapi-codegen v1.15.0
8+
github.com/hashicorp/go-retryablehttp v0.7.5
89
github.com/stretchr/testify v1.8.4
910
)
1011

@@ -33,6 +34,7 @@ require (
3334
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 // indirect
3435
github.com/google/uuid v1.3.1 // indirect
3536
github.com/gorilla/css v1.0.0 // indirect
37+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
3638
github.com/iris-contrib/schema v0.0.6 // indirect
3739
github.com/josharian/intern v1.0.0 // indirect
3840
github.com/json-iterator/go v1.1.12 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
7373
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
7474
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
7575
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
76+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
77+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
78+
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
79+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
80+
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
81+
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
7682
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
7783
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
7884
github.com/iris-contrib/httpexpect/v2 v2.15.2 h1:T9THsdP1woyAqKHwjkEsbCnMefsAFvk8iJJKokcJ3Go=

templates/client.tmpl

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
// RequestEditorFn is the function signature for the RequestEditor callback function
2+
type RequestEditorFn func(ctx context.Context, req *http.Request) error
3+
4+
// Doer performs HTTP requests.
5+
//
6+
// The standard http.Client implements this interface.
7+
type HttpRequestDoer interface {
8+
Do(req *http.Request) (*http.Response, error)
9+
}
10+
11+
{{$clientTypeName := opts.OutputOptions.ClientTypeName -}}
12+
13+
// {{ $clientTypeName }} which conforms to the OpenAPI3 specification for this service.
14+
type {{ $clientTypeName }} struct {
15+
// The endpoint of the server conforming to this interface, with scheme,
16+
// https://api.deepmap.com for example. This can contain a path relative
17+
// to the server, such as https://api.deepmap.com/dev-test, and all the
18+
// paths in the swagger spec will be appended to the server.
19+
Server string
20+
21+
// Doer for performing requests, typically a *http.Client with any
22+
// customized settings, such as certificate chains.
23+
Client HttpRequestDoer
24+
25+
// A list of callbacks for modifying requests which are generated before sending over
26+
// the network.
27+
RequestEditors []RequestEditorFn
28+
}
29+
30+
// ClientOption allows setting custom parameters during construction
31+
type ClientOption func(*{{ $clientTypeName }}) error
32+
33+
// Creates a new {{ $clientTypeName }}, with reasonable defaults
34+
func NewClient(server string, opts ...ClientOption) (*{{ $clientTypeName }}, error) {
35+
opts = append([]ClientOption{WithHTTPClient(retryablehttp.NewClient().StandardClient())}, opts...)
36+
// create a client with sane default values
37+
client := {{ $clientTypeName }}{
38+
Server: server,
39+
}
40+
// mutate client and add all optional params
41+
for _, o := range opts {
42+
if err := o(&client); err != nil {
43+
return nil, err
44+
}
45+
}
46+
// ensure the server URL always has a trailing slash
47+
if !strings.HasSuffix(client.Server, "/") {
48+
client.Server += "/"
49+
}
50+
// create httpClient, if not already present
51+
if client.Client == nil {
52+
client.Client = &http.Client{}
53+
}
54+
return &client, nil
55+
}
56+
57+
// WithHTTPClient allows overriding the default Doer, which is
58+
// automatically created using http.Client. This is useful for tests.
59+
func WithHTTPClient(doer HttpRequestDoer) ClientOption {
60+
return func(c *{{ $clientTypeName }}) error {
61+
c.Client = doer
62+
return nil
63+
}
64+
}
65+
66+
// WithRequestEditorFn allows setting up a callback function, which will be
67+
// called right before sending the request. This can be used to mutate the request.
68+
func WithRequestEditorFn(fn RequestEditorFn) ClientOption {
69+
return func(c *{{ $clientTypeName }}) error {
70+
c.RequestEditors = append(c.RequestEditors, fn)
71+
return nil
72+
}
73+
}
74+
75+
// The interface specification for the client above.
76+
type ClientInterface interface {
77+
{{range . -}}
78+
{{$hasParams := .RequiresParamObject -}}
79+
{{$pathParams := .PathParams -}}
80+
{{$opid := .OperationId -}}
81+
// {{$opid}}{{if .HasBody}}WithBody{{end}} request{{if .HasBody}} with any body{{end}}
82+
{{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error)
83+
{{range .Bodies}}
84+
{{if .IsSupportedByClient -}}
85+
{{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error)
86+
{{end -}}
87+
{{end}}{{/* range .Bodies */}}
88+
{{end}}{{/* range . $opid := .OperationId */}}
89+
}
90+
91+
92+
{{/* Generate client methods */}}
93+
{{range . -}}
94+
{{$hasParams := .RequiresParamObject -}}
95+
{{$pathParams := .PathParams -}}
96+
{{$opid := .OperationId -}}
97+
98+
func (c *{{ $clientTypeName }}) {{$opid}}{{if .HasBody}}WithBody{{end}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}, reqEditors... RequestEditorFn) (*http.Response, error) {
99+
req, err := New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(c.Server{{genParamNames .PathParams}}{{if $hasParams}}, params{{end}}{{if .HasBody}}, contentType, body{{end}})
100+
if err != nil {
101+
return nil, err
102+
}
103+
req = req.WithContext(ctx)
104+
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
105+
return nil, err
106+
}
107+
return c.Client.Do(req)
108+
}
109+
110+
{{range .Bodies}}
111+
{{if .IsSupportedByClient -}}
112+
func (c *{{ $clientTypeName }}) {{$opid}}{{.Suffix}}(ctx context.Context{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody, reqEditors... RequestEditorFn) (*http.Response, error) {
113+
req, err := New{{$opid}}Request{{.Suffix}}(c.Server{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, body)
114+
if err != nil {
115+
return nil, err
116+
}
117+
req = req.WithContext(ctx)
118+
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
119+
return nil, err
120+
}
121+
return c.Client.Do(req)
122+
}
123+
{{end -}}{{/* if .IsSupported */}}
124+
{{end}}{{/* range .Bodies */}}
125+
{{end}}
126+
127+
{{/* Generate request builders */}}
128+
{{range .}}
129+
{{$hasParams := .RequiresParamObject -}}
130+
{{$pathParams := .PathParams -}}
131+
{{$bodyRequired := .BodyRequired -}}
132+
{{$opid := .OperationId -}}
133+
134+
{{range .Bodies}}
135+
{{if .IsSupportedByClient -}}
136+
// New{{$opid}}Request{{.Suffix}} calls the generic {{$opid}} builder with {{.ContentType}} body
137+
func New{{$opid}}Request{{.Suffix}}(server string{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}, body {{$opid}}{{.NameTag}}RequestBody) (*http.Request, error) {
138+
var bodyReader io.Reader
139+
{{if .IsJSON -}}
140+
buf, err := json.Marshal(body)
141+
if err != nil {
142+
return nil, err
143+
}
144+
bodyReader = bytes.NewReader(buf)
145+
{{else if eq .NameTag "Formdata" -}}
146+
bodyStr, err := runtime.MarshalForm(body, nil)
147+
if err != nil {
148+
return nil, err
149+
}
150+
bodyReader = strings.NewReader(bodyStr.Encode())
151+
{{else if eq .NameTag "Text" -}}
152+
bodyReader = strings.NewReader(string(body))
153+
{{end -}}
154+
return New{{$opid}}RequestWithBody(server{{genParamNames $pathParams}}{{if $hasParams}}, params{{end}}, "{{.ContentType}}", bodyReader)
155+
}
156+
{{end -}}
157+
{{end}}
158+
159+
// New{{$opid}}Request{{if .HasBody}}WithBody{{end}} generates requests for {{$opid}}{{if .HasBody}} with any type of body{{end}}
160+
func New{{$opid}}Request{{if .HasBody}}WithBody{{end}}(server string{{genParamArgs $pathParams}}{{if $hasParams}}, params *{{$opid}}Params{{end}}{{if .HasBody}}, contentType string, body io.Reader{{end}}) (*http.Request, error) {
161+
var err error
162+
{{range $paramIdx, $param := .PathParams}}
163+
var pathParam{{$paramIdx}} string
164+
{{if .IsPassThrough}}
165+
pathParam{{$paramIdx}} = {{.GoVariableName}}
166+
{{end}}
167+
{{if .IsJson}}
168+
var pathParamBuf{{$paramIdx}} []byte
169+
pathParamBuf{{$paramIdx}}, err = json.Marshal({{.GoVariableName}})
170+
if err != nil {
171+
return nil, err
172+
}
173+
pathParam{{$paramIdx}} = string(pathParamBuf{{$paramIdx}})
174+
{{end}}
175+
{{if .IsStyled}}
176+
pathParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationPath, {{.GoVariableName}})
177+
if err != nil {
178+
return nil, err
179+
}
180+
{{end}}
181+
{{end}}
182+
serverURL, err := url.Parse(server)
183+
if err != nil {
184+
return nil, err
185+
}
186+
187+
operationPath := fmt.Sprintf("{{genParamFmtString .Path}}"{{range $paramIdx, $param := .PathParams}}, pathParam{{$paramIdx}}{{end}})
188+
if operationPath[0] == '/' {
189+
operationPath = "." + operationPath
190+
}
191+
192+
queryURL, err := serverURL.Parse(operationPath)
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
{{if .QueryParams}}
198+
if params != nil {
199+
queryValues := queryURL.Query()
200+
{{range $paramIdx, $param := .QueryParams}}
201+
{{if not .Required}} if params.{{.GoName}} != nil { {{end}}
202+
{{if .IsPassThrough}}
203+
queryValues.Add("{{.ParamName}}", {{if not .Required}}*{{end}}params.{{.GoName}})
204+
{{end}}
205+
{{if .IsJson}}
206+
if queryParamBuf, err := json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}}); err != nil {
207+
return nil, err
208+
} else {
209+
queryValues.Add("{{.ParamName}}", string(queryParamBuf))
210+
}
211+
212+
{{end}}
213+
{{if .IsStyled}}
214+
if queryFrag, err := runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationQuery, {{if not .Required}}*{{end}}params.{{.GoName}}); err != nil {
215+
return nil, err
216+
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
217+
return nil, err
218+
} else {
219+
for k, v := range parsed {
220+
for _, v2 := range v {
221+
queryValues.Add(k, v2)
222+
}
223+
}
224+
}
225+
{{end}}
226+
{{if not .Required}}}{{end}}
227+
{{end}}
228+
queryURL.RawQuery = queryValues.Encode()
229+
}
230+
{{end}}{{/* if .QueryParams */}}
231+
req, err := http.NewRequest("{{.Method}}", queryURL.String(), {{if .HasBody}}body{{else}}nil{{end}})
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
{{if .HasBody}}req.Header.Add("Content-Type", contentType){{end}}
237+
{{ if .HeaderParams }}
238+
if params != nil {
239+
{{range $paramIdx, $param := .HeaderParams}}
240+
{{if not .Required}} if params.{{.GoName}} != nil { {{end}}
241+
var headerParam{{$paramIdx}} string
242+
{{if .IsPassThrough}}
243+
headerParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}}
244+
{{end}}
245+
{{if .IsJson}}
246+
var headerParamBuf{{$paramIdx}} []byte
247+
headerParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}})
248+
if err != nil {
249+
return nil, err
250+
}
251+
headerParam{{$paramIdx}} = string(headerParamBuf{{$paramIdx}})
252+
{{end}}
253+
{{if .IsStyled}}
254+
headerParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("{{.Style}}", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationHeader, {{if not .Required}}*{{end}}params.{{.GoName}})
255+
if err != nil {
256+
return nil, err
257+
}
258+
{{end}}
259+
req.Header.Set("{{.ParamName}}", headerParam{{$paramIdx}})
260+
{{if not .Required}}}{{end}}
261+
{{end}}
262+
}
263+
{{- end }}{{/* if .HeaderParams */}}
264+
265+
{{ if .CookieParams }}
266+
if params != nil {
267+
{{range $paramIdx, $param := .CookieParams}}
268+
{{if not .Required}} if params.{{.GoName}} != nil { {{end}}
269+
var cookieParam{{$paramIdx}} string
270+
{{if .IsPassThrough}}
271+
cookieParam{{$paramIdx}} = {{if not .Required}}*{{end}}params.{{.GoName}}
272+
{{end}}
273+
{{if .IsJson}}
274+
var cookieParamBuf{{$paramIdx}} []byte
275+
cookieParamBuf{{$paramIdx}}, err = json.Marshal({{if not .Required}}*{{end}}params.{{.GoName}})
276+
if err != nil {
277+
return nil, err
278+
}
279+
cookieParam{{$paramIdx}} = url.QueryEscape(string(cookieParamBuf{{$paramIdx}}))
280+
{{end}}
281+
{{if .IsStyled}}
282+
cookieParam{{$paramIdx}}, err = runtime.StyleParamWithLocation("simple", {{.Explode}}, "{{.ParamName}}", runtime.ParamLocationCookie, {{if not .Required}}*{{end}}params.{{.GoName}})
283+
if err != nil {
284+
return nil, err
285+
}
286+
{{end}}
287+
cookie{{$paramIdx}} := &http.Cookie{
288+
Name:"{{.ParamName}}",
289+
Value:cookieParam{{$paramIdx}},
290+
}
291+
req.AddCookie(cookie{{$paramIdx}})
292+
{{if not .Required}}}{{end}}
293+
{{ end -}}
294+
}
295+
{{- end }}{{/* if .CookieParams */}}
296+
return req, nil
297+
}
298+
299+
{{end}}{{/* Range */}}
300+
301+
func (c *{{ $clientTypeName }}) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
302+
for _, r := range c.RequestEditors {
303+
if err := r(ctx, req); err != nil {
304+
return err
305+
}
306+
}
307+
for _, r := range additionalEditors {
308+
if err := r(ctx, req); err != nil {
309+
return err
310+
}
311+
}
312+
return nil
313+
}

0 commit comments

Comments
 (0)