-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathclient.go
More file actions
209 lines (174 loc) · 5.97 KB
/
client.go
File metadata and controls
209 lines (174 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package sourcify
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// RetryOptions represents options for configuring retry settings.
type RetryOptions struct {
MaxRetries int // The maximum number of retries.
Delay time.Duration // The delay between retries.
}
// RetryOption sets a configuration option for retry settings.
type RetryOption func(*RetryOptions)
// WithMaxRetries sets the maximum number of retries.
func WithMaxRetries(maxRetries int) RetryOption {
return func(options *RetryOptions) {
options.MaxRetries = maxRetries
}
}
// WithDelay sets the delay between retries.
func WithDelay(delay time.Duration) RetryOption {
return func(options *RetryOptions) {
options.Delay = delay
}
}
type ClientOption func(*Client)
type Client struct {
BaseURL string // The base URL of the Sourcify API.
HTTPClient *http.Client // The HTTP client to use for making requests.
RetryOptions RetryOptions // The retry options for the client.
RateLimiter *RateLimiter // The rate limiter for the client.
}
// WithHTTPClient allows you to provide your own http.Client for the Sourcify client.
func WithHTTPClient(client *http.Client) ClientOption {
return func(c *Client) {
c.HTTPClient = client
}
}
// WithBaseURL allows you to provide your own base URL for the Sourcify client.
func WithBaseURL(baseURL string) ClientOption {
return func(c *Client) {
c.BaseURL = baseURL
}
}
// WithRetryOptions allows you to configure retry settings for the Sourcify client.
func WithRetryOptions(options ...RetryOption) ClientOption {
return func(c *Client) {
for _, opt := range options {
opt(&c.RetryOptions)
}
}
}
// WithRateLimit allows you to configure rate limits for the Sourcify client.
func WithRateLimit(max int, duration time.Duration) ClientOption {
return func(c *Client) {
c.RateLimiter = NewRateLimiter(max, duration)
}
}
// NewClient initializes a new Sourcify client with optional configurations.
// By default, it uses the Sourcify API's base URL (https://sourcify.dev/server),
// the default http.Client, and no retry options.
func NewClient(options ...ClientOption) *Client {
c := &Client{
BaseURL: "https://sourcify.dev/server",
HTTPClient: http.DefaultClient,
RetryOptions: RetryOptions{},
}
for _, option := range options {
option(c)
}
return c
}
// CallMethod calls the specified method function with the provided parameters.
// It returns the response body as a byte slice and an error if any.
func (c *Client) CallMethod(method Method) (io.ReadCloser, int, error) {
switch method.ParamType {
case MethodParamTypeUri:
return c.callURIMethod(method)
case MethodParamTypeQueryString:
return c.callQueryMethod(method)
case MethodParamTypeUriAndQueryString:
return c.callUriAndQueryMethod(method)
default:
return nil, 0, fmt.Errorf("invalid MethodParamType: %v", method.ParamType)
}
}
// callURIMethod calls the URI-based method function with the provided parameters.
func (c *Client) callURIMethod(method Method) (io.ReadCloser, int, error) {
requestUrl, err := url.Parse(c.BaseURL)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse API base URL: %w", err)
}
uri, err := method.ParseUri()
if err != nil {
return nil, 0, fmt.Errorf("failed to parse method parameters: %w", err)
}
requestPath, err := url.JoinPath(requestUrl.Path, uri)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse full API URL: %w", err)
}
requestUrl.Path = requestPath
req, err := http.NewRequest(method.Method, requestUrl.String(), nil)
if err != nil {
return nil, 0, fmt.Errorf("failed to create HTTP request: %w", err)
}
return c.doRequestWithRetry(req)
}
// callQueryMethod calls the query-based method function with the provided parameters.
func (c *Client) callQueryMethod(method Method) (io.ReadCloser, int, error) {
requestUrl, err := url.Parse(c.BaseURL)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse API base URL: %w", err)
}
requestPath, err := url.JoinPath(requestUrl.Path, method.URI)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse full API URL: %w", err)
}
requestUrl.Path = requestPath
queryParams := method.GetQueryParams()
requestUrl.RawQuery = queryParams.Encode()
req, err := http.NewRequest(method.Method, requestUrl.String(), nil)
if err != nil {
return nil, 0, fmt.Errorf("failed to create HTTP request: %w", err)
}
return c.doRequestWithRetry(req)
}
func (c *Client) callUriAndQueryMethod(method Method) (io.ReadCloser, int, error) {
requestUrl, err := url.Parse(c.BaseURL)
if err != nil {
return nil, 0, fmt.Errorf("failed to parse API base URL: %w", err)
}
uri, err := method.ParseUri()
if err != nil {
return nil, 0, fmt.Errorf("failed to parse method parameters: %w", err)
}
req, err := http.NewRequest(method.Method, strings.Join([]string{requestUrl.String(), uri}, ""), nil)
if err != nil {
return nil, 0, fmt.Errorf("failed to create HTTP request: %w", err)
}
return c.doRequestWithRetry(req)
}
// doRequestWithRetry sends the HTTP request with retry according to the configured retry options.
func (c *Client) doRequestWithRetry(req *http.Request) (io.ReadCloser, int, error) {
attempt := 0
for {
if c.RateLimiter != nil {
c.RateLimiter.Wait()
}
attempt++
resp, err := c.HTTPClient.Do(req)
if err != nil {
if attempt <= c.RetryOptions.MaxRetries {
time.Sleep(c.RetryOptions.Delay)
continue
}
return nil, 0, fmt.Errorf("failed to send HTTP request: %w", err)
}
// We do not want to retry on status codes less than 500 as those are not temporary errors
if resp.StatusCode >= 500 {
if attempt <= c.RetryOptions.MaxRetries {
time.Sleep(c.RetryOptions.Delay)
continue
}
if err == nil {
return nil, resp.StatusCode, fmt.Errorf("unexpected response (status: %s) (attempt: %d): no error details available", resp.Status, attempt)
}
return nil, resp.StatusCode, fmt.Errorf("unexpected response (status: %s) (attempt: %d): %w", resp.Status, attempt, err)
}
return resp.Body, resp.StatusCode, nil
}
}