Codebase list golang-github-go-kit-kit / 048aff0
#814 Add Transport Layer: AWS Lambda (#815) * base serving, encode, decode functions * logger * add before encode options * add ServerAfter functions * add error encoder * add finalizer * add basic happy flow * add ServerBefore tests * test error flow on decoding stage * complete the test scenarios * refactor into more generic support by implementing lambda.Handler * testing: ErrorEncoder to reflect behavior of encoding error as payload for AWS APIGateway event handling, as if we return error not nil to lambda it shall treated as error, and the API response would not be pretty * add wrapper * capitalize Lambda * add particle an * tidy up doc words as per suggested * Update transport/awslambda/request_response_funcs.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * Update transport/awslambda/request_response_funcs.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * Update transport/awslambda/server.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * Update transport/awslambda/request_response_funcs.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * refactor multiline to keep them 1 per line * remove \n from test * defines a DefaultErrorEncoder, refactor the handling of errorEncoder during Invoke * refactor Server into Handler * remove wrapper * refactor variable s into h * simplify the return of errorEncoder * Update transport/awslambda/handler.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * Update transport/awslambda/handler.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * Update transport/awslambda/handler.go Co-Authored-By: suekto-andreas <suekto.andreas@gmail.com> * DefaultErrorEncoder unit test Andreas authored 4 years ago Peter Bourgon committed 4 years ago
5 changed file(s) with 509 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 // Package awslambda provides an AWS Lambda transport layer.
1 package awslambda
0 package awslambda
1
2 import (
3 "context"
4 )
5
6 // DecodeRequestFunc extracts a user-domain request object from an
7 // AWS Lambda payload.
8 type DecodeRequestFunc func(context.Context, []byte) (interface{}, error)
9
10 // EncodeResponseFunc encodes the passed response object into []byte,
11 // ready to be sent as AWS Lambda response.
12 type EncodeResponseFunc func(context.Context, interface{}) ([]byte, error)
13
14 // ErrorEncoder is responsible for encoding an error.
15 type ErrorEncoder func(ctx context.Context, err error) ([]byte, error)
0 package awslambda
1
2 import (
3 "context"
4
5 "github.com/go-kit/kit/endpoint"
6 "github.com/go-kit/kit/log"
7 )
8
9 // Handler wraps an endpoint.
10 type Handler struct {
11 e endpoint.Endpoint
12 dec DecodeRequestFunc
13 enc EncodeResponseFunc
14 before []HandlerRequestFunc
15 after []HandlerResponseFunc
16 errorEncoder ErrorEncoder
17 finalizer []HandlerFinalizerFunc
18 logger log.Logger
19 }
20
21 // NewHandler constructs a new handler, which implements
22 // the AWS lambda.Handler interface.
23 func NewHandler(
24 e endpoint.Endpoint,
25 dec DecodeRequestFunc,
26 enc EncodeResponseFunc,
27 options ...HandlerOption,
28 ) *Handler {
29 h := &Handler{
30 e: e,
31 dec: dec,
32 enc: enc,
33 logger: log.NewNopLogger(),
34 errorEncoder: DefaultErrorEncoder,
35 }
36 for _, option := range options {
37 option(h)
38 }
39 return h
40 }
41
42 // HandlerOption sets an optional parameter for handlers.
43 type HandlerOption func(*Handler)
44
45 // HandlerBefore functions are executed on the payload byte,
46 // before the request is decoded.
47 func HandlerBefore(before ...HandlerRequestFunc) HandlerOption {
48 return func(h *Handler) { h.before = append(h.before, before...) }
49 }
50
51 // HandlerAfter functions are only executed after invoking the endpoint
52 // but prior to returning a response.
53 func HandlerAfter(after ...HandlerResponseFunc) HandlerOption {
54 return func(h *Handler) { h.after = append(h.after, after...) }
55 }
56
57 // HandlerErrorLogger is used to log non-terminal errors.
58 // By default, no errors are logged.
59 func HandlerErrorLogger(logger log.Logger) HandlerOption {
60 return func(h *Handler) { h.logger = logger }
61 }
62
63 // HandlerErrorEncoder is used to encode errors.
64 func HandlerErrorEncoder(ee ErrorEncoder) HandlerOption {
65 return func(h *Handler) { h.errorEncoder = ee }
66 }
67
68 // HandlerFinalizer sets finalizer which are called at the end of
69 // request. By default no finalizer is registered.
70 func HandlerFinalizer(f ...HandlerFinalizerFunc) HandlerOption {
71 return func(h *Handler) { h.finalizer = append(h.finalizer, f...) }
72 }
73
74 // DefaultErrorEncoder defines the default behavior of encoding an error response,
75 // where it returns nil, and the error itself.
76 func DefaultErrorEncoder(ctx context.Context, err error) ([]byte, error) {
77 return nil, err
78 }
79
80 // Invoke represents implementation of the AWS lambda.Handler interface.
81 func (h *Handler) Invoke(
82 ctx context.Context,
83 payload []byte,
84 ) (resp []byte, err error) {
85 if len(h.finalizer) > 0 {
86 defer func() {
87 for _, f := range h.finalizer {
88 f(ctx, resp, err)
89 }
90 }()
91 }
92
93 for _, f := range h.before {
94 ctx = f(ctx, payload)
95 }
96
97 request, err := h.dec(ctx, payload)
98 if err != nil {
99 h.logger.Log("err", err)
100 return h.errorEncoder(ctx, err)
101 }
102
103 response, err := h.e(ctx, request)
104 if err != nil {
105 h.logger.Log("err", err)
106 return h.errorEncoder(ctx, err)
107 }
108
109 for _, f := range h.after {
110 ctx = f(ctx, response)
111 }
112
113 if resp, err = h.enc(ctx, response); err != nil {
114 h.logger.Log("err", err)
115 return h.errorEncoder(ctx, err)
116 }
117
118 return resp, err
119 }
0 package awslambda
1
2 import (
3 "context"
4 "encoding/json"
5 "fmt"
6 "testing"
7
8 "github.com/aws/aws-lambda-go/events"
9 "github.com/go-kit/kit/endpoint"
10 "github.com/go-kit/kit/log"
11 )
12
13 type key int
14
15 const (
16 KeyBeforeOne key = iota
17 KeyBeforeTwo key = iota
18 KeyAfterOne key = iota
19 KeyEncMode key = iota
20 )
21
22 func TestDefaultErrorEncoder(t *testing.T) {
23 ctx := context.Background()
24 rootErr := fmt.Errorf("root")
25 b, err := DefaultErrorEncoder(ctx, rootErr)
26 if b != nil {
27 t.Fatalf("DefaultErrorEncoder should return nil as []byte")
28 }
29 if err != rootErr {
30 t.Fatalf("DefaultErrorEncoder expects return back the given error.")
31 }
32 }
33
34 func TestInvokeHappyPath(t *testing.T) {
35 svc := serviceTest01{}
36
37 helloHandler := NewHandler(
38 makeTest01HelloEndpoint(svc),
39 decodeHelloRequestWithTwoBefores,
40 encodeResponse,
41 HandlerErrorLogger(log.NewNopLogger()),
42 HandlerBefore(func(
43 ctx context.Context,
44 payload []byte,
45 ) context.Context {
46 ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
47 return ctx
48 }),
49 HandlerBefore(func(
50 ctx context.Context,
51 payload []byte,
52 ) context.Context {
53 ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
54 return ctx
55 }),
56 HandlerAfter(func(
57 ctx context.Context,
58 response interface{},
59 ) context.Context {
60 ctx = context.WithValue(ctx, KeyAfterOne, "af1")
61 return ctx
62 }),
63 HandlerAfter(func(
64 ctx context.Context,
65 response interface{},
66 ) context.Context {
67 if _, ok := ctx.Value(KeyAfterOne).(string); !ok {
68 t.Fatalf("Value was not set properly during multi HandlerAfter")
69 }
70 return ctx
71 }),
72 HandlerFinalizer(func(
73 _ context.Context,
74 resp []byte,
75 _ error,
76 ) {
77 apigwResp := events.APIGatewayProxyResponse{}
78 err := json.Unmarshal(resp, &apigwResp)
79 if err != nil {
80 t.Fatalf("Should have no error, but got: %+v", err)
81 }
82
83 response := helloResponse{}
84 err = json.Unmarshal([]byte(apigwResp.Body), &response)
85 if err != nil {
86 t.Fatalf("Should have no error, but got: %+v", err)
87 }
88
89 expectedGreeting := "hello john doe bef1 bef2"
90 if response.Greeting != expectedGreeting {
91 t.Fatalf(
92 "Expect: %s, Actual: %s", expectedGreeting, response.Greeting)
93 }
94 }),
95 )
96
97 ctx := context.Background()
98 req, _ := json.Marshal(events.APIGatewayProxyRequest{
99 Body: `{"name":"john doe"}`,
100 })
101 resp, err := helloHandler.Invoke(ctx, req)
102
103 if err != nil {
104 t.Fatalf("Should have no error, but got: %+v", err)
105 }
106
107 apigwResp := events.APIGatewayProxyResponse{}
108 err = json.Unmarshal(resp, &apigwResp)
109 if err != nil {
110 t.Fatalf("Should have no error, but got: %+v", err)
111 }
112
113 response := helloResponse{}
114 err = json.Unmarshal([]byte(apigwResp.Body), &response)
115 if err != nil {
116 t.Fatalf("Should have no error, but got: %+v", err)
117 }
118
119 expectedGreeting := "hello john doe bef1 bef2"
120 if response.Greeting != expectedGreeting {
121 t.Fatalf(
122 "Expect: %s, Actual: %s", expectedGreeting, response.Greeting)
123 }
124 }
125
126 func TestInvokeFailDecode(t *testing.T) {
127 svc := serviceTest01{}
128
129 helloHandler := NewHandler(
130 makeTest01HelloEndpoint(svc),
131 decodeHelloRequestWithTwoBefores,
132 encodeResponse,
133 HandlerErrorEncoder(func(
134 ctx context.Context,
135 err error,
136 ) ([]byte, error) {
137 apigwResp := events.APIGatewayProxyResponse{}
138 apigwResp.Body = `{"error":"yes"}`
139 apigwResp.StatusCode = 500
140 resp, err := json.Marshal(apigwResp)
141 return resp, err
142 }),
143 )
144
145 ctx := context.Background()
146 req, _ := json.Marshal(events.APIGatewayProxyRequest{
147 Body: `{"name":"john doe"}`,
148 })
149 resp, err := helloHandler.Invoke(ctx, req)
150
151 if err != nil {
152 t.Fatalf("Should have no error, but got: %+v", err)
153 }
154
155 apigwResp := events.APIGatewayProxyResponse{}
156 json.Unmarshal(resp, &apigwResp)
157 if apigwResp.StatusCode != 500 {
158 t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
159 }
160 }
161
162 func TestInvokeFailEndpoint(t *testing.T) {
163 svc := serviceTest01{}
164
165 helloHandler := NewHandler(
166 makeTest01FailEndpoint(svc),
167 decodeHelloRequestWithTwoBefores,
168 encodeResponse,
169 HandlerBefore(func(
170 ctx context.Context,
171 payload []byte,
172 ) context.Context {
173 ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
174 return ctx
175 }),
176 HandlerBefore(func(
177 ctx context.Context,
178 payload []byte,
179 ) context.Context {
180 ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
181 return ctx
182 }),
183 HandlerErrorEncoder(func(
184 ctx context.Context,
185 err error,
186 ) ([]byte, error) {
187 apigwResp := events.APIGatewayProxyResponse{}
188 apigwResp.Body = `{"error":"yes"}`
189 apigwResp.StatusCode = 500
190 resp, err := json.Marshal(apigwResp)
191 return resp, err
192 }),
193 )
194
195 ctx := context.Background()
196 req, _ := json.Marshal(events.APIGatewayProxyRequest{
197 Body: `{"name":"john doe"}`,
198 })
199 resp, err := helloHandler.Invoke(ctx, req)
200
201 if err != nil {
202 t.Fatalf("Should have no error, but got: %+v", err)
203 }
204
205 apigwResp := events.APIGatewayProxyResponse{}
206 json.Unmarshal(resp, &apigwResp)
207 if apigwResp.StatusCode != 500 {
208 t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
209 }
210 }
211
212 func TestInvokeFailEncode(t *testing.T) {
213 svc := serviceTest01{}
214
215 helloHandler := NewHandler(
216 makeTest01HelloEndpoint(svc),
217 decodeHelloRequestWithTwoBefores,
218 encodeResponse,
219 HandlerBefore(func(
220 ctx context.Context,
221 payload []byte,
222 ) context.Context {
223 ctx = context.WithValue(ctx, KeyBeforeOne, "bef1")
224 return ctx
225 }),
226 HandlerBefore(func(
227 ctx context.Context,
228 payload []byte,
229 ) context.Context {
230 ctx = context.WithValue(ctx, KeyBeforeTwo, "bef2")
231 return ctx
232 }),
233 HandlerAfter(func(
234 ctx context.Context,
235 response interface{},
236 ) context.Context {
237 ctx = context.WithValue(ctx, KeyEncMode, "fail_encode")
238 return ctx
239 }),
240 HandlerErrorEncoder(func(
241 ctx context.Context,
242 err error,
243 ) ([]byte, error) {
244 // convert error into proper APIGateway response.
245 apigwResp := events.APIGatewayProxyResponse{}
246 apigwResp.Body = `{"error":"yes"}`
247 apigwResp.StatusCode = 500
248 resp, err := json.Marshal(apigwResp)
249 return resp, err
250 }),
251 )
252
253 ctx := context.Background()
254 req, _ := json.Marshal(events.APIGatewayProxyRequest{
255 Body: `{"name":"john doe"}`,
256 })
257 resp, err := helloHandler.Invoke(ctx, req)
258
259 if err != nil {
260 t.Fatalf("Should have no error, but got: %+v", err)
261 }
262
263 apigwResp := events.APIGatewayProxyResponse{}
264 json.Unmarshal(resp, &apigwResp)
265 if apigwResp.StatusCode != 500 {
266 t.Fatalf("Expect status code of 500, instead of %d", apigwResp.StatusCode)
267 }
268 }
269
270 func decodeHelloRequestWithTwoBefores(
271 ctx context.Context, req []byte,
272 ) (interface{}, error) {
273 apigwReq := events.APIGatewayProxyRequest{}
274 err := json.Unmarshal([]byte(req), &apigwReq)
275 if err != nil {
276 return apigwReq, err
277 }
278
279 request := helloRequest{}
280 err = json.Unmarshal([]byte(apigwReq.Body), &request)
281 if err != nil {
282 return request, err
283 }
284
285 valOne, ok := ctx.Value(KeyBeforeOne).(string)
286 if !ok {
287 return request, fmt.Errorf(
288 "Value was not set properly when multiple HandlerBefores are used")
289 }
290
291 valTwo, ok := ctx.Value(KeyBeforeTwo).(string)
292 if !ok {
293 return request, fmt.Errorf(
294 "Value was not set properly when multiple HandlerBefores are used")
295 }
296
297 request.Name += " " + valOne + " " + valTwo
298 return request, err
299 }
300
301 func encodeResponse(
302 ctx context.Context, response interface{},
303 ) ([]byte, error) {
304 apigwResp := events.APIGatewayProxyResponse{}
305
306 mode, ok := ctx.Value(KeyEncMode).(string)
307 if ok && mode == "fail_encode" {
308 return nil, fmt.Errorf("fail encoding")
309 }
310
311 respByte, err := json.Marshal(response)
312 if err != nil {
313 return nil, err
314 }
315
316 apigwResp.Body = string(respByte)
317 apigwResp.StatusCode = 200
318
319 resp, err := json.Marshal(apigwResp)
320 return resp, err
321 }
322
323 type helloRequest struct {
324 Name string `json:"name"`
325 }
326
327 type helloResponse struct {
328 Greeting string `json:"greeting"`
329 }
330
331 func makeTest01HelloEndpoint(svc serviceTest01) endpoint.Endpoint {
332 return func(_ context.Context, request interface{}) (interface{}, error) {
333 req := request.(helloRequest)
334 greeting := svc.hello(req.Name)
335 return helloResponse{greeting}, nil
336 }
337 }
338
339 func makeTest01FailEndpoint(_ serviceTest01) endpoint.Endpoint {
340 return func(_ context.Context, request interface{}) (interface{}, error) {
341 return nil, fmt.Errorf("test error endpoint")
342 }
343 }
344
345 type serviceTest01 struct{}
346
347 func (ts *serviceTest01) hello(name string) string {
348 return fmt.Sprintf("hello %s", name)
349 }
0 package awslambda
1
2 import (
3 "context"
4 )
5
6 // HandlerRequestFunc may take information from the received
7 // payload and use it to place items in the request scoped context.
8 // HandlerRequestFuncs are executed prior to invoking the endpoint and
9 // decoding of the payload.
10 type HandlerRequestFunc func(ctx context.Context, payload []byte) context.Context
11
12 // HandlerResponseFunc may take information from a request context
13 // and use it to manipulate the response before it's marshaled.
14 // HandlerResponseFunc are executed after invoking the endpoint
15 // but prior to returning a response.
16 type HandlerResponseFunc func(ctx context.Context, response interface{}) context.Context
17
18 // HandlerFinalizerFunc is executed at the end of Invoke.
19 // This can be used for logging purposes.
20 type HandlerFinalizerFunc func(ctx context.Context, resp []byte, err error)