-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathencode.go
259 lines (235 loc) · 7.61 KB
/
encode.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
package nvelope
import (
"encoding/json"
"encoding/xml"
"net/http"
"github.com/muir/nject"
"github.com/golang/gddo/httputil"
"github.com/pkg/errors"
)
// InjectWriter injects a DeferredWriter
var InjectWriter = nject.Provide("writer", NewDeferredWriter)
// AutoFlushWriter calls Flush on the deferred writer if it hasn't
// already been done
var AutoFlushWriter = nject.Provide("autoflush-writer", func(inner func(), w *DeferredWriter) {
inner()
_ = w.FlushIfNotFlushed()
})
// Response is an empty interface that is the expected return value
// from endpoints.
type Response interface{}
// EncodeJSON is a JSON encoder manufactured by MakeResponseEncoder with default options.
var EncodeJSON = MakeResponseEncoder("JSON",
WithEncoder("application/json", json.Marshal,
WithEncoderErrorTransform(func(err error) (interface{}, bool) {
var jm json.Marshaler
if errors.As(err, &jm) {
return jm, true
}
return nil, false
}),
))
// EncodeXML is a XML encoder manufactured by MakeResponseEncoder with default options.
var EncodeXML = MakeResponseEncoder("XML",
WithEncoder("application/xml", xml.Marshal,
WithEncoderErrorTransform(func(err error) (interface{}, bool) {
var me xml.Marshaler
if errors.As(err, &me) {
return me, true
}
return nil, false
}),
))
type encoderOptions struct {
encoders map[string]specificEncoder
contentOffers []string
defaultEncoder string
errorTransformer ErrorTranformer
}
type specificEncoder struct {
apiEnforcer func(httpCode int, enc []byte, header http.Header, r *http.Request) error
errorTransformer ErrorTranformer
encode func(interface{}) ([]byte, error)
}
// ResponseEncoderFuncArg is a function argument for MakeResponseEncoder
type ResponseEncoderFuncArg func(*encoderOptions)
// EncoderSpecificFuncArg is a functional arguemnt for WithEncoder
type EncoderSpecificFuncArg func(*specificEncoder)
// ErrorTranformer transforms an error into a model that can be logged.
type ErrorTranformer func(error) (replacementModel interface{}, useReplacement bool)
// WithEncoder adds an model encoder to what MakeResponseEncoder will support.
// The first encoder added becomes the default encoder that is used if there
// is no match between the client's Accept header and the encoders that
// MakeResponseEncoder knows about.
func WithEncoder(contentType string, encode func(interface{}) ([]byte, error), encoderOpts ...EncoderSpecificFuncArg) ResponseEncoderFuncArg {
return func(o *encoderOptions) {
if o.defaultEncoder == "" {
o.defaultEncoder = contentType
}
se := specificEncoder{
encode: encode,
apiEnforcer: func(_ int, _ []byte, _ http.Header, _ *http.Request) error { return nil },
}
for _, eo := range encoderOpts {
eo(&se)
}
if _, ok := o.encoders[contentType]; !ok {
o.contentOffers = append(o.contentOffers, contentType)
}
o.encoders[contentType] = se
}
}
// WithErrorModel provides a function to transform errors before
// encoding them using the normal encoder. The return values are the model
// to use instead of the error and a boolean to indicate that the replacement
// should be used. If the boolean is false, then a plain text error
// message will be generated using err.Error().
func WithErrorModel(errorTransformer ErrorTranformer) ResponseEncoderFuncArg {
return func(o *encoderOptions) {
o.errorTransformer = errorTransformer
}
}
// WithEncoderErrorTransform provides an encoder-specific function to
// transform errors before
// encoding them using the normal encoder. The return values are the model
// to use instead of the error and a boolean to indicate that the replacement
// should be used. If the boolean is false, then a plain text error
// message will be generated using err.Error().
func WithEncoderErrorTransform(errorTransformer ErrorTranformer) EncoderSpecificFuncArg {
return func(o *specificEncoder) {
o.errorTransformer = errorTransformer
}
}
type APIEnforcerFunc func(httpCode int, enc []byte, header http.Header, r *http.Request) error
// WithAPIEnforcer specifies
// a function that can check if the encoded API response is valid
// for the endpoint that is generating the response. This is where
// swagger enforcement could be added. The default is not not verify
// API conformance.
//
// https://github.com/muir/nvalid provides a function to generate an
// APIEnforcerFunc from swagger.
func WithAPIEnforcer(apiEnforcer APIEnforcerFunc) EncoderSpecificFuncArg {
return func(o *specificEncoder) {
o.apiEnforcer = apiEnforcer
}
}
// MakeResponseEncoder generates an nject Provider to encode API responses.
//
// The generated provider is a wrapper that invokes the rest of the
// handler injection chain and expect to receive as return values
// an Response and and error. If the error is not nil, then the response
// becomes the error.
//
// If more than one encoder is configurured, then MakeResponseEncoder will default to
// the first one specified in its functional arguments.
func MakeResponseEncoder(
name string,
encoderFuncArgs ...ResponseEncoderFuncArg,
) nject.Provider {
o := encoderOptions{
errorTransformer: func(_ error) (interface{}, bool) { return nil, false },
encoders: make(map[string]specificEncoder),
}
for _, fa := range encoderFuncArgs {
fa(&o)
}
if o.defaultEncoder == "" {
// oops, the user should have done something!
WithEncoder("application/json", json.Marshal)(&o)
}
return nject.Provide("marshal-"+name,
func(
inner func() (Response, error),
w *DeferredWriter,
log BasicLogger,
r *http.Request,
) {
model, err := inner()
if w.Done() {
return
}
contentType := httputil.NegotiateContentType(r, o.contentOffers, o.defaultEncoder)
encoder := o.encoders[contentType]
w.Header().Set("Content-Type", contentType)
var code int
var enc []byte
// handleError will always set enc
var handleError func(recurseOkay bool)
handleError = func(recurseOkay bool) {
code = GetReturnCode(err)
et := encoder.errorTransformer
if et == nil {
et = o.errorTransformer
}
logDetails := map[string]interface{}{
"httpCode": code,
"error": err.Error(),
"method": r.Method,
"uri": r.URL.String(),
}
if code < 500 {
log.Warn("returning user error", logDetails)
} else {
log.Error("returning server error", logDetails)
}
if rm, ok := et(err); ok {
enc, err = encoder.encode(rm)
if err != nil {
err = errors.Wrapf(err, "encode %s response", contentType)
if recurseOkay {
handleError(false)
} else {
enc = []byte(err.Error())
}
}
} else {
enc = []byte(err.Error())
}
}
if err != nil {
handleError(true)
}
if len(enc) == 0 {
enc, err = encoder.encode(model)
if err != nil {
handleError(true)
}
}
if code == 0 {
code = 200
}
err = encoder.apiEnforcer(code, enc, w.Header(), r)
if err != nil {
handleError(true)
}
w.WriteHeader(code)
_, err = w.Write(enc)
e2 := w.Flush()
if err == nil {
err = e2
}
if err != nil {
log.Warn("Cannot write response",
map[string]interface{}{
"error": err.Error(),
"method": r.Method,
"uri": r.URL.String(),
})
}
})
}
// Nil204 is a wrapper that causes looks for return values of Response and error
// and if both are nil, writes a 204 header and no data. It is mean to be used
// downstream from a response encocder.
var Nil204 = nject.Desired(nject.Provide("nil-204", nil204))
func nil204(inner func() (Response, error), w *DeferredWriter) {
model, err := inner()
if w.Done() {
return
}
if err == nil && model == nil {
w.WriteHeader(204)
_ = w.Flush()
}
}