-
Notifications
You must be signed in to change notification settings - Fork 1
/
request.go
355 lines (294 loc) · 10.7 KB
/
request.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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package cex
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"github.com/go-resty/resty/v2"
)
// =========================================================== \\
// +++ +++ \\
// +++ Cex REST Core: Request +++ \\
// +++ +++ \\
// =========================================================== \\
// ===========================================================
// Request
// -----------------------------------------------------------
// Request is the core of cex REST.
// This method is convenient for developers and callers.
//
// Developers just need to implement a simple ReqMaker to custom request creating,
// and implement a HTTPStatusCodeChecker and some simple RespBodyUnmarshaler -s
// to custom response analysing.
// After above works, whenever add new REST api, developer could just add
// new ReqConfig, and some very simple REST functions in exchange packages.
// Can see code in bnc(binance) package.
//
// Callers just need to find target ReqConfig in the target exchange package,
// and add opts appending needs, Request will do other things.
//
// If wanting to quickly familiarize with this frame, go directly to cex/bnc/user.go.
//
// Structured request error, RequestError, will help callers to check more
// details of error occurred during resting.
func Request[ReqDataType, RespDataType any](
reqMaker ReqMaker,
config ReqConfig[ReqDataType, RespDataType],
reqData ReqDataType,
opts ...CltOpt,
) (*resty.Response, RespDataType, RequestError) {
var resp *resty.Response
var data RespDataType
var err RequestError
for i := 0; i < 3; i++ {
resp, data, err = request(reqMaker, config, reqData, opts...)
if err.Is(ErrInvalidTimestamp) {
continue
}
break
}
return resp, data, err
}
func request[ReqDataType, RespDataType any](
reqMaker ReqMaker,
config ReqConfig[ReqDataType, RespDataType],
reqData ReqDataType,
opts ...CltOpt,
) (*resty.Response, RespDataType, RequestError) {
reqErr := RequestError{ReqBaseConfig: config.ReqBaseConfig}
var respData RespDataType
req, err := reqMaker.Make(config.ReqBaseConfig, reqData, opts...)
if err != nil {
return nil, respData, *reqErr.SetErr(fmt.Errorf("cex: make request, %w", err))
}
// here sets empty url
// request maker should compose the whole url
var resp *resty.Response
switch config.Method {
case http.MethodGet:
resp, err = req.Get("")
case http.MethodPost:
resp, err = req.Post("")
case http.MethodPut:
resp, err = req.Put("")
case http.MethodDelete:
resp, err = req.Delete("")
default:
return resp, respData, *reqErr.SetErr(fmt.Errorf("cex: http method %v is not supported", config.Method))
}
// Ignore resty error, if response is not nil.
// Resty will return err if status code > 399.
// But the request with a response status code that bigger than 399
// may not be failed.
// ex. For binance, response status code that bigger than 500 means
// that the status is unknown, and users can ignore.
// https://binance-docs.github.io/apidocs/spot/en/#general-api-information
// So if one err is returned, check that resp is nil or not.
// If resp is nil, return directly.
// Otherwise, go on.
if err != nil && resp == nil {
return resp, respData, *reqErr.SetErr(fmt.Errorf("cex: request err: %w", err))
}
if resp == nil {
// Should not get here.
// If getting here, err and resp are all nil.
// Resty may have bugs.
return resp, respData, *reqErr.SetErr(fmt.Errorf("cex: resp and err are all nil, resty may have bugs"))
}
var errResty error
if err != nil {
errResty = err
}
if config.HTTPStatusCodeChecker == nil {
return resp, respData, *reqErr.SetErr(fmt.Errorf("cex: config http status code checker is nil"))
}
if config.RespBodyUnmarshaler == nil {
return resp, respData, *reqErr.SetErr(fmt.Errorf("cex: config resp body unmarshaler is nil"))
}
errHttp := config.HTTPStatusCodeChecker(resp.StatusCode())
respData, errBodyUnmarshal := config.RespBodyUnmarshaler(resp.Body())
if errHttp == nil && errBodyUnmarshal == nil {
return resp, respData, reqErr
}
// some cex may set detailed error msg in body, while request failed
// so, collect http status and body data together
if errHttp != nil {
reqErr.HTTPError = &HTTPError{
StatusCode: resp.StatusCode(),
Status: resp.Status(),
Err: errHttp,
}
}
if errBodyUnmarshal != nil {
reqErr.RespBodyUnmarshalerError = errBodyUnmarshal
}
reqErr.Err = fmt.Errorf("cex: request, resty err: %w, http err: %w, body unmarshal err: %w", errResty, errHttp, errBodyUnmarshal)
return resp, respData, reqErr
}
// -----------------------------------------------------------
// Request
// ===========================================================
// ===========================================================
// Core Types
// -----------------------------------------------------------
// ReqBaseConfig save some read-only info.
// This struct is the real contain of ReqConfig.
type ReqBaseConfig struct {
// ex. https://www.example.com
BaseUrl string `json:"baseUrl" bson:"baseUrl"`
// ex. /path/to/service
Path string `json:"path" bson:"path"`
// http method, GET, POST...
// better to use const method value in http package directly
Method string `json:"method" bson:"method"`
// if true, should use api key
IsUserData bool `json:"isUserData" bson:"isUserData"`
// one user can rest every UserTimeInterval.
// unit is millisecond
UserTimeInterval int64 `json:"userTimeInterval" bson:"userTimeInterval"`
// one ip can reset every IpTimeInterval
// unit is millisecond
IpTimeInterval int64 `json:"ipTimeInterval" bson:"ipTimeInterval"`
}
// HTTPStatusCodeChecker checks HTTP status code.
// If request is failed, return error.
type HTTPStatusCodeChecker func(int) error
// RespBodyUnmarshaler unmarshal HTTP response body.
// Cex may have its own diy error code and msg.
// Generally, these infos are contained in body,
// so should get these infos by unmarshalling.
type RespBodyUnmarshaler[D any] func([]byte) (D, *RespBodyUnmarshalerError)
// NilReqData means that no request data.
// If a ReqConfig ReqDataType is this,
// reqData should be nil.
type NilReqData any
// ReqConfig is wrapper of ReqBaseConfig.
// This struct makes it convenient to call Request.
// ReqDataType and RespDataType are not used in ReqConfig,
// but in practice, it is very useful.
// In practice, we call Request to query cex data,
// but we should know ReqBaseConfig, ReqDataType and RespDataType simultaneously.
// We have many config implementations in all cex packages.
// These config with patterns bind ReqBaseConfig,
// ReqDataType and RespDataType together.
// Set a ReqBaseConfig instance as input of Request,
// all Request patterns are defined.
type ReqConfig[ReqDataType, RespDataType any] struct {
ReqBaseConfig
// status code and its status message
HTTPStatusCodeChecker HTTPStatusCodeChecker
RespBodyUnmarshaler RespBodyUnmarshaler[RespDataType]
}
// CltOpt is function option that can custom request.
type CltOpt func(*resty.Client)
// ReqMaker should be implemented in all cex package
type ReqMaker interface {
Make(config ReqBaseConfig, reqData any, opts ...CltOpt) (*resty.Request, error)
//HandleResp(*resty.Response, *resty.Request) error
}
// -----------------------------------------------------------
// Core Types
// ===========================================================
// ===========================================================
// Custom Errors
// -----------------------------------------------------------
// RespBodyUnmarshalerError contains cex own diy error code and msg.
// Why should specific this struct? See RespBodyUnmarshaler.
type RespBodyUnmarshalerError struct {
CexErrCode int `json:"cexErrCode,omitempty"`
CexErrMsg string `json:"cexErrMsg,omitempty"`
// Err is unmarshal error or cex err.
Err error `json:"err,omitempty"`
}
func (e *RespBodyUnmarshalerError) Error() string {
return fmt.Sprintf("code: %v, msg: %v, err: %v", e.CexErrCode, e.CexErrMsg, e.Err)
}
func (e *RespBodyUnmarshalerError) Is(target error) bool {
return e.Err != nil && errors.Is(e.Err, target)
}
func (e *RespBodyUnmarshalerError) SetErr(err error) *RespBodyUnmarshalerError {
e.Err = err
return e
}
// HTTPError contains raw info and cex package custom http error.
type HTTPError struct {
StatusCode int `json:"statusCode"`
Status string `json:"status"`
Err error `json:"err"`
}
func (e *HTTPError) Error() string {
return fmt.Sprintf(
"code: %v, status: %v, httperr: %v",
e.StatusCode, e.Status, e.Err,
)
}
func (e *HTTPError) Is(target error) bool {
return e.Err != nil && errors.Is(e.Err, target)
}
// RequestError
// Structured error info is better.
type RequestError struct {
ReqBaseConfig ReqBaseConfig `json:"reqBaseConfig"`
HTTPError *HTTPError `json:"HTTPError"`
RespBodyUnmarshalerError *RespBodyUnmarshalerError `json:"respBodyUnmarshalerError"`
Err error `json:"err"`
}
func (e *RequestError) Error() string {
return e.String()
}
func (e *RequestError) String() string {
return fmt.Sprintf(
"%v %v%v, %v",
e.ReqBaseConfig.Method,
e.ReqBaseConfig.BaseUrl,
e.ReqBaseConfig.Path,
e.Err,
)
}
func (e *RequestError) Is(target error) bool {
return e.Err != nil && errors.Is(e.Err, target)
}
func (e *RequestError) SetErr(err error) *RequestError {
e.Err = err
return e
}
func (e *RequestError) IsNotNil() bool {
return e.Err != nil
}
func (e *RequestError) IsNil() bool {
return e.Err == nil
}
// -----------------------------------------------------------
// Custom Errors
// ===========================================================
// ===========================================================
// Resp Data Unmarshaler
// -----------------------------------------------------------
func StdBodyUnmarshaler[D any](data []byte) (D, *RespBodyUnmarshalerError) {
errUnmar := new(RespBodyUnmarshalerError)
respData := new(D)
respType := reflect.TypeOf(respData).Elem()
var anyRes any
switch respType.Kind() {
case reflect.String:
anyRes = any(string(data))
case reflect.Slice, reflect.Struct, reflect.Map:
if err := json.Unmarshal(data, respData); err != nil {
return *respData, errUnmar.SetErr(fmt.Errorf("%w: unmarshal response body, %w", ErrJsonUnmarshal, err))
}
anyRes = any(*respData)
default:
return *respData, errUnmar.SetErr(fmt.Errorf("response data type %v is not supported", respType.Kind()))
}
res, ok := anyRes.(D)
if !ok {
errUnmar.Err = fmt.Errorf("cex: cannot convert to %T", res)
} else {
errUnmar = nil
}
return res, errUnmar
}
// -----------------------------------------------------------
// Resp Data Unmarshaler
// ===========================================================