-
Notifications
You must be signed in to change notification settings - Fork 67
/
client.go
196 lines (161 loc) 路 4.46 KB
/
client.go
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
package resourcemanager
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"go.uber.org/zap"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Verb string
type Client struct {
client *HTTPClient
resourceName string
resourceNamePlural string
logger *zap.Logger
options options
}
type HTTPClient struct {
client http.Client
baseURL string
extraHeaders http.Header
}
func NewHTTPClient(baseURL string, extraHeaders http.Header) *HTTPClient {
return &HTTPClient{
client: http.Client{
// this function avoids blindly followin redirects.
// the problem with redirects is that they don't guarantee to preserve the method, body, headers, etc.
// This can hide issues when developing, because the client will follow the redirect and the request
// will succeed, but the server will not receive the request that the user intended to send.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
},
baseURL: baseURL,
extraHeaders: extraHeaders,
}
}
func (c HTTPClient) url(resourceName, prefix string, extra ...string) *url.URL {
urlStr := c.baseURL + path.Join("/", prefix, resourceName, strings.Join(extra, "/"))
url, _ := url.Parse(urlStr)
return url
}
type ContextOption string
const (
ContextHeadersKey ContextOption = "headers"
)
func SetRequestContextHeaders(ctx context.Context, headers map[string]string) context.Context {
httpHeaders := http.Header{}
for k, v := range headers {
httpHeaders.Set(k, v)
}
return context.WithValue(ctx, ContextHeadersKey, httpHeaders)
}
func (c HTTPClient) do(req *http.Request) (*http.Response, error) {
for k, v := range c.extraHeaders {
req.Header[k] = v
}
contextHeaders := req.Context().Value(ContextHeadersKey)
if contextHeaders != nil {
for k, v := range contextHeaders.(http.Header) {
req.Header[k] = v
}
}
return c.client.Do(req)
}
// NewClient creates a new client for a resource managed by the resourceamanger.
// The tableConfig parameter configures how the table view should be rendered.
// This configuration work both for a single resource from a Get, or a ResourceList from a List
func NewClient(
httpClient *HTTPClient,
logger *zap.Logger,
resourceName, resourceNamePlural string,
opts ...option) Client {
c := Client{
client: httpClient,
resourceName: resourceName,
resourceNamePlural: resourceNamePlural,
logger: logger,
}
for _, opt := range opts {
opt(&c.options)
}
return c
}
func (c Client) WithHttpClient(HTTPClient *HTTPClient) Client {
c.client = HTTPClient
return c
}
func (c Client) WithOptions(opts ...option) Client {
for _, opt := range opts {
opt(&c.options)
}
return c
}
func (c Client) resourceType() string {
if c.options.resourceType != "" {
return c.options.resourceType
}
// language.Und means Undefined
caser := cases.Title(language.Und, cases.NoLower)
return caser.String(c.resourceName)
}
var ErrNotFound = RequestError{
Code: http.StatusNotFound,
Message: "Resource not found",
}
type RequestError struct {
Code int `json:"code"`
Message string `json:"error"`
}
type alternateRequestError struct {
Status int `json:"status"`
Detail string `json:"detail"`
}
func (e RequestError) Error() string {
return e.Message
}
func (e RequestError) Is(target error) bool {
t, ok := target.(RequestError)
return ok && t.Code == e.Code
}
func isSuccessResponse(resp *http.Response) bool {
// successfull http status codes are 2xx
return resp.StatusCode >= 200 && resp.StatusCode < 300
}
func parseRequestError(resp *http.Response, format Format) error {
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("cannot read response body: %w", err)
}
if len(body) == 0 {
return RequestError{
Code: resp.StatusCode,
Message: resp.Status,
}
}
var reqErr RequestError
err = format.Unmarshal(body, &reqErr)
if err != nil {
return fmt.Errorf("cannot parse response body: %w", err)
}
emptyRequestError := reqErr.Code == 0 && reqErr.Message == ""
if !emptyRequestError {
// Success, parsed error message
return reqErr
}
// Fallback, try to parse message in other format
var alternateReqError alternateRequestError
err = format.Unmarshal(body, &alternateReqError)
if err != nil {
return fmt.Errorf("cannot parse response body: %w", err)
}
return RequestError{
Code: alternateReqError.Status,
Message: alternateReqError.Detail,
}
}