-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapi_response.go
297 lines (248 loc) · 7.96 KB
/
api_response.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
package bigdog
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"strconv"
"sync"
"time"
)
var (
ErrResponseBeingRead = errors.New("response bytes have been either partially or fully read")
ErrResponseClosed = errors.New("response has been closed")
ErrResponseHydrated = errors.New("response bytes have been read via Hydrate call")
ErrResponseEmpty = errors.New("response body empty")
)
type APIResponseMeta struct {
RequestMethod string
RequestURI string
SuccessCode int
ResponseCode int
ResponseStatus string
ResponseHeader http.Header
ExecutionTime time.Duration
}
func newAPIResponseMeta(req *APIRequest, successCode int, httpResp *http.Response, executionTime time.Duration) APIResponseMeta {
rm := APIResponseMeta{
RequestMethod: req.Method,
RequestURI: req.URI,
SuccessCode: successCode,
ExecutionTime: executionTime,
}
if httpResp != nil {
rm.ResponseCode = httpResp.StatusCode
rm.ResponseStatus = http.StatusText(httpResp.StatusCode)
// make copy of response headers so gc can clean up response after body has been read
// todo: always read the body, dingus.
rm.ResponseHeader = make(http.Header, len(httpResp.Header))
for k, vs := range httpResp.Header {
l := len(vs)
rm.ResponseHeader[k] = make([]string, l, l)
copy(rm.ResponseHeader[k], vs)
}
}
return rm
}
func newAPIResponseMetaWithCode(req *APIRequest, successCode, errHTTPCode int, executionTime time.Duration) APIResponseMeta {
rm := newAPIResponseMeta(req, successCode, nil, executionTime)
rm.ResponseCode = errHTTPCode
return rm
}
func (rm APIResponseMeta) ContentType() string {
return rm.ResponseHeader.Get(headerKeyContentType)
}
func (rm APIResponseMeta) ContentEncoding() string {
return rm.ResponseHeader.Get(headerKeyContentEncoding)
}
func (rm APIResponseMeta) ContentLength() int {
if v := rm.ResponseHeader.Get(headerKeyContentLength); v != "" {
l, _ := strconv.Atoi(v)
return l
}
return 0
}
func (rm APIResponseMeta) ContentDisposition() string {
return rm.ResponseHeader.Get(headerKeyContentDisposition)
}
// Completed indicates whether the request described by this meta type completed, but does not indicate success
func (rm APIResponseMeta) Completed() bool {
return rm.ResponseCode != 0
}
func (rm APIResponseMeta) String() string {
var msg string
if rm.SuccessCode == rm.ResponseCode {
msg = "Success"
} else {
msg = "Error"
}
return fmt.Sprintf("%s response from request %s %s", msg, rm.RequestMethod, rm.RequestURI)
}
// APIResponseMetaContainer describes any type containing metadata about an API call
type APIResponseMetaContainer interface {
ResponseMeta() APIResponseMeta
}
// APIResponse is implemented by each response model, allowing you to either unmarshal the resulting bytes into a model
// or receive the raw bytes themselves.
//
// This can be useful for middlewares that perform proxy duties, where there is nothing to be gained by unmarshalling
// and marshalling the returned data and it is fine to simply ship the bytes on along to the receiver.
type APIResponse interface {
APIResponseMetaContainer
// Source must return the source of this API response
Source() APISource
// Err must return the current state error, or nil if beginning state.
Err() error
// Read returns the raw bytes from the HTTP response body. Once called
//
// This action is mutually exclusive with Hydrate.
Read(p []byte) (n int, err error)
// Close renders the internal response body defunct. Once called, further calls to Read or Hydrate will
Close() error
}
type ModeledAPIResponse interface {
APIResponse
// Hydrate must attempt to perform whatever action is necessary to ingest any returned bytes into a specific model
// This action is mutually exclusive with Hydrate, and any attempt to use this as a reader after having called
// Hydrate will result in an error
Hydrate() (interface{}, error)
}
// apiResponseFactory is used by the internal response handling mechanism to construct each response type
type apiResponseFactory func(source APISource, meta APIResponseMeta, body io.ReadCloser) APIResponse
// RawAPIResponse is the base implementation of APIResponse. Each api either returns this or returns a type that embeds
// this type.
type RawAPIResponse struct {
mu sync.Mutex
src APISource
body io.ReadCloser
err error
meta APIResponseMeta
}
func newRawAPIResponse(source APISource, meta APIResponseMeta, body io.ReadCloser) APIResponse {
b := new(RawAPIResponse)
b.src = source
b.meta = meta
if body != nil {
b.body = body
}
return b
}
func (b *RawAPIResponse) cleanupBody() error {
if b.body == nil {
return nil
}
_, _ = io.Copy(ioutil.Discard, b.body)
err := b.body.Close()
b.body = nil
return err
}
// ResponseMeta returns a portable meta type
func (b *RawAPIResponse) ResponseMeta() APIResponseMeta {
return b.meta
}
// Source returns the upstream source of this response (VSZ or SCI)
func (b *RawAPIResponse) Source() APISource {
return b.src
}
// Err returns the current state error, if there is one.
func (b *RawAPIResponse) Err() error {
b.mu.Lock()
err := b.err
b.mu.Unlock()
return err
}
// Read performs a read from the response body. Once this has been called, any Hydrate implementation will fail with an
// error
func (b *RawAPIResponse) Read(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
// test for prior action
if b.err != nil {
// if the response body was hydrated into a model, return an EOF to hint to consumers there is nothing left
// to read
if errors.Is(b.err, ErrResponseHydrated) {
return 0, io.EOF
}
// if the state error is anything other than "in the middle of a read", return it
if !errors.Is(b.err, ErrResponseBeingRead) {
return 0, b.err
}
// otherwise, allow read to continue
}
// check for body being nil
if b.body == nil {
// todo: is a simple EOF sufficient here? should it be io.ErrUnexpectedEOF?
b.err = io.EOF
return 0, b.err
}
var (
n int
err error
)
// always set state error
b.err = ErrResponseBeingRead
// perform read
if n, err = b.body.Read(p); err != nil {
// if any error is encountered, set state error before returning
b.err = err
}
// if no error encountered, simply return
return n, err
}
// Close closes the upstream http response body and removes its reference from the local type, enabling response gc
func (b *RawAPIResponse) Close() error {
b.mu.Lock()
defer b.mu.Unlock()
// if this response was previous hydrated, move on
if b.err != nil && errors.Is(b.err, ErrResponseHydrated) {
return nil
}
// in all other cases, perform full cleanupBody and return any close error
b.err = ErrResponseClosed
return b.cleanupBody()
}
func (b *RawAPIResponse) doHydrate(ptr interface{}) error {
// test for an action already having been taken
if b.err != nil {
// test for a prior Hydrate action
if errors.Is(b.err, ErrResponseHydrated) {
return nil
}
// if err is anything else, return directly
return b.err
}
// queue up body cleanup
defer func() { _ = b.cleanupBody() }()
// unmarshall into model
if err := json.NewDecoder(b.body).Decode(ptr); err != nil {
b.err = err
return err
}
// specify Hydrate error for subsequent action attempts
b.err = ErrResponseHydrated
return nil
}
// EmptyAPIResponse is returned by API calls that do not return a body, i.e. those with an http response code of 204
// or 201. Calls to Read on this type will immediately return an io.EOF
type EmptyAPIResponse struct {
*RawAPIResponse
}
func newEmptyAPIResponse(source APISource, meta APIResponseMeta, body io.ReadCloser) APIResponse {
r := new(EmptyAPIResponse)
cleanupReadCloser(body)
r.RawAPIResponse = newRawAPIResponse(source, meta, nil).(*RawAPIResponse)
return r
}
func (*EmptyAPIResponse) Read(_ []byte) (int, error) {
return 0, io.EOF
}
type FileAPIResponse struct {
*RawAPIResponse
}
func newFileAPIResponse(source APISource, meta APIResponseMeta, body io.ReadCloser) APIResponse {
r := new(FileAPIResponse)
r.RawAPIResponse = newRawAPIResponse(source, meta, body).(*RawAPIResponse)
return r
}