-
Notifications
You must be signed in to change notification settings - Fork 18
/
client.go
233 lines (203 loc) · 5.93 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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package helpers
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"github.com/PuerkitoBio/goquery"
"github.com/hetiansu5/urlquery"
"github.com/rotisserie/eris"
"github.com/G-Research/fasttrackml/pkg/server"
)
// ResponseType represents HTTP response type.
type ResponseType string
// Supported list of HTTP response types.
// TODO:dsuhinin - add another type `stream`. For this type return `io.ReadCloser`.
const (
ResponseTypeJSON ResponseType = "json"
ResponseTypeBuffer ResponseType = "buffer"
ResponseTypeHTML ResponseType = "html"
)
// HttpClient represents HTTP client.
type HttpClient struct {
server server.Server
basePath string
namespace string
method string
params any
headers map[string]string
request any
response any
responseType ResponseType
statusCode int
}
// NewClient creates new preconfigured HTTP client.
func NewClient(server server.Server, basePath string) *HttpClient {
return &HttpClient{
server: server,
basePath: basePath,
method: http.MethodGet,
headers: map[string]string{
"Content-Type": "application/json",
},
responseType: ResponseTypeJSON,
}
}
// NewMlflowApiClient creates new HTTP client for the mlflow api
func NewMlflowApiClient(server server.Server) *HttpClient {
return NewClient(server, "/api/2.0/mlflow")
}
// NewAimApiClient creates new HTTP client for the aim api
func NewAimApiClient(server server.Server) *HttpClient {
return NewClient(server, "/aim/api")
}
// NewAdminApiClient creates new HTTP client for the admin api
func NewAdminApiClient(server server.Server) *HttpClient {
return NewClient(server, "/admin")
}
// WithMethod sets the HTTP method.
func (c *HttpClient) WithMethod(method string) *HttpClient {
c.method = method
return c
}
// WithQuery adds query parameters to the HTTP request.
func (c *HttpClient) WithQuery(params any) *HttpClient {
c.params = params
return c
}
// WithRequest sets request object.
func (c *HttpClient) WithRequest(request any) *HttpClient {
c.request = request
return c
}
// WithNamespace sets the namespace path.
func (c *HttpClient) WithNamespace(namespace string) *HttpClient {
c.namespace = namespace
return c
}
// WithHeaders adds headers to the HTTP request.
func (c *HttpClient) WithHeaders(headers map[string]string) *HttpClient {
c.headers = headers
return c
}
// WithResponse sets the response object where HTTP response will be deserialized.
func (c *HttpClient) WithResponse(response any) *HttpClient {
c.response = response
return c
}
// WithResponseType sets the response object type.
func (c *HttpClient) WithResponseType(responseType ResponseType) *HttpClient {
c.responseType = responseType
return c
}
// GetStatusCode returns HTTP status code of the last response, if available.
func (c *HttpClient) GetStatusCode() int {
return c.statusCode
}
// DoRequest do actual HTTP request based on provided parameters.
// nolint:gocyclo
func (c *HttpClient) DoRequest(uri string, values ...any) error {
// 1. check if request object were provided. if provided then marshal it.
var requestBody io.Reader
if c.request != nil {
data, err := json.Marshal(c.request)
if err != nil {
return eris.Wrap(err, "error marshaling request object")
}
requestBody = bytes.NewBuffer(data)
}
// 2. build path with namespace.
path := c.basePath
if c.namespace != "" {
path = fmt.Sprintf("/ns/%s%s", c.namespace, c.basePath)
}
// 3. build actual URL.
u, err := url.Parse(fmt.Sprintf("%s%s", path, fmt.Sprintf(uri, values...)))
if err != nil {
return eris.Wrap(err, "error building url")
}
// 4. if params were provided then add params to actual url.
if c.params != nil {
switch reflect.ValueOf(c.params).Kind() {
case reflect.Struct:
query, err := urlquery.Marshal(c.params)
if err != nil {
return eris.New("error marshaling params")
}
u.RawQuery = string(query)
case reflect.Map:
query := u.Query()
for key, value := range c.params.(map[any]any) {
query.Set(fmt.Sprintf("%v", key), fmt.Sprintf("%v", value))
}
u.RawQuery = query.Encode()
default:
return eris.New("unsupported type of params. should be struct or map[any]any")
}
}
// 5. create actual request object.
// if HttpMethod was not provided, then by default use HttpMethodGet.
req := httptest.NewRequest(
c.method, u.String(), requestBody,
)
// 6. if headers were provided, then attach them.
// by default attach `"Content-Type", "application/json"`
if c.headers != nil {
for key, value := range c.headers {
req.Header.Set(key, value)
}
}
// 7. send request data.
resp, err := c.server.Test(req, 60000)
if err != nil {
return eris.Wrap(err, "error doing request")
}
c.statusCode = resp.StatusCode
// 8. read and check response data.
if c.response != nil {
switch c.responseType {
case ResponseTypeJSON:
body, err := io.ReadAll(resp.Body)
if err != nil {
return eris.Wrap(err, "error reading response data")
}
//nolint:errcheck
defer resp.Body.Close()
if err := json.Unmarshal(body, c.response); err != nil {
return eris.Wrap(err, "error unmarshaling response data")
}
case ResponseTypeBuffer:
buffer, ok := c.response.(io.Writer)
if !ok {
return eris.New("response object has no implementation of a io.Writer")
}
_, err := io.Copy(buffer, resp.Body)
//nolint:errcheck
defer resp.Body.Close()
if err != nil {
return eris.Wrap(err, "error reading streaming response")
}
case ResponseTypeHTML:
body, err := io.ReadAll(resp.Body)
if err != nil {
return eris.Wrap(err, "error reading response data")
}
//nolint:errcheck
defer resp.Body.Close()
response, ok := c.response.(*goquery.Document)
if !ok {
return eris.New("response object is not a *goquery.Document")
}
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
if err != nil {
return eris.Wrap(err, "error creating goquery document")
}
*response = *doc
}
}
return nil
}