From 3e21610c4a84d97c4719ee40ef985d66976f3926 Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Wed, 19 Jul 2023 15:20:28 -0700 Subject: [PATCH 1/4] feat: provide the options to set arbitrary json encoder and decoder options --- lambda/handler.go | 71 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/lambda/handler.go b/lambda/handler.go index e4cfaf7a..ec8c4280 100644 --- a/lambda/handler.go +++ b/lambda/handler.go @@ -22,12 +22,11 @@ type Handler interface { type handlerOptions struct { handlerFunc - baseContext context.Context - jsonResponseEscapeHTML bool - jsonResponseIndentPrefix string - jsonResponseIndentValue string - enableSIGTERM bool - sigtermCallbacks []func() + baseContext context.Context + jsonEncoderOptions []func(encoder *json.Encoder) + jsonDecoderOptions []func(decoder *json.Decoder) + enableSIGTERM bool + sigtermCallbacks []func() } type Option func(*handlerOptions) @@ -48,6 +47,44 @@ func WithContext(ctx context.Context) Option { }) } +// WithJSONEncoderOption allows setting arbitrary options on the underlying json encoder +// +// Usage: +// +// lambda.StartWithOptions( +// func () (string, error) { +// return "hello!>", nil +// }, +// lambda.WithJSONEncoderOption(func(encoder *json.Encoder) { +// encoder.SetEscapeHTML(true) +// encoder.SetIndent(">", " ") +// }), +// ) +func WithJSONEncoderOption(opt func(encoder *json.Encoder)) Option { + return Option(func(h *handlerOptions) { + h.jsonEncoderOptions = append(h.jsonEncoderOptions, opt) + }) +} + +// WithJSONDecoderOption allows setting arbitrary options on the underlying json decoder +// +// Usage: +// +// lambda.StartWithOptions( +// func (event any) (any, error) { +// return event, nil +// }, +// lambda.WithJSONEncoderOption(func(decoder *json.Decoder) { +// decoder.UseNumber() +// decoder.DisallowUnknownFields() +// }), +// ) +func WithJSONDecoderOption(opt func(decoder *json.Decoder)) Option { + return Option(func(h *handlerOptions) { + h.jsonDecoderOptions = append(h.jsonDecoderOptions, opt) + }) +} + // WithSetEscapeHTML sets the SetEscapeHTML argument on the underlying json encoder // // Usage: @@ -59,8 +96,8 @@ func WithContext(ctx context.Context) Option { // lambda.WithSetEscapeHTML(true), // ) func WithSetEscapeHTML(escapeHTML bool) Option { - return Option(func(h *handlerOptions) { - h.jsonResponseEscapeHTML = escapeHTML + return WithJSONEncoderOption(func(encoder *json.Encoder) { + encoder.SetEscapeHTML(escapeHTML) }) } @@ -75,9 +112,8 @@ func WithSetEscapeHTML(escapeHTML bool) Option { // lambda.WithSetIndent(">"," "), // ) func WithSetIndent(prefix, indent string) Option { - return Option(func(h *handlerOptions) { - h.jsonResponseIndentPrefix = prefix - h.jsonResponseIndentValue = indent + return WithJSONEncoderOption(func(encoder *json.Encoder) { + encoder.SetIndent(prefix, indent) }) } @@ -176,10 +212,7 @@ func newHandler(handlerFunc interface{}, options ...Option) *handlerOptions { return h } h := &handlerOptions{ - baseContext: context.Background(), - jsonResponseEscapeHTML: false, - jsonResponseIndentPrefix: "", - jsonResponseIndentValue: "", + baseContext: context.Background(), } for _, option := range options { option(h) @@ -267,9 +300,13 @@ func reflectHandler(f interface{}, h *handlerOptions) handlerFunc { out.Reset() in := bytes.NewBuffer(payload) decoder := json.NewDecoder(in) + for _, opt := range h.jsonDecoderOptions { + opt(decoder) + } encoder := json.NewEncoder(out) - encoder.SetEscapeHTML(h.jsonResponseEscapeHTML) - encoder.SetIndent(h.jsonResponseIndentPrefix, h.jsonResponseIndentValue) + for _, opt := range h.jsonEncoderOptions { + opt(encoder) + } trace := handlertrace.FromContext(ctx) From f755a864a866e861edce57cf8cc7de17adfcf48a Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Wed, 19 Jul 2023 15:33:12 -0700 Subject: [PATCH 2/4] fix: retain backward compatibility of newline stripping --- lambda/handler.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lambda/handler.go b/lambda/handler.go index ec8c4280..088ad5a5 100644 --- a/lambda/handler.go +++ b/lambda/handler.go @@ -25,6 +25,7 @@ type handlerOptions struct { baseContext context.Context jsonEncoderOptions []func(encoder *json.Encoder) jsonDecoderOptions []func(decoder *json.Decoder) + setIndentUsed bool enableSIGTERM bool sigtermCallbacks []func() } @@ -112,8 +113,14 @@ func WithSetEscapeHTML(escapeHTML bool) Option { // lambda.WithSetIndent(">"," "), // ) func WithSetIndent(prefix, indent string) Option { - return WithJSONEncoderOption(func(encoder *json.Encoder) { - encoder.SetIndent(prefix, indent) + return Option(func(h *handlerOptions) { + // back-compat, the encoder's trailing newline is stripped unless WithSetIndent was used + if prefix != "" || indent != "" { + h.setIndentUsed = true + } + h.jsonEncoderOptions = append(h.jsonEncoderOptions, func(encoder *json.Encoder) { + encoder.SetIndent(prefix, indent) + }) }) } @@ -362,7 +369,7 @@ func reflectHandler(f interface{}, h *handlerOptions) handlerFunc { } // back-compat, strip the encoder's trailing newline unless WithSetIndent was used - if h.jsonResponseIndentValue == "" && h.jsonResponseIndentPrefix == "" { + if !h.setIndentUsed { out.Truncate(out.Len() - 1) } return out, nil From c81332f654c5fbd162fe451e19791b5b94e67f07 Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Wed, 19 Jul 2023 15:48:55 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20test=20(=F0=9F=A7=AA)=20`UseNumber()?= =?UTF-8?q?`=20option=20as=20an=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lambda/handler_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lambda/handler_test.go b/lambda/handler_test.go index 87942900..204565e5 100644 --- a/lambda/handler_test.go +++ b/lambda/handler_test.go @@ -5,6 +5,7 @@ package lambda import ( "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -285,6 +286,50 @@ func TestInvokes(t *testing.T) { return nil, messages.InvokeResponse_Error{Message: "message", Type: "type"} }, }, + { + name: "WithJSONDecoderOption(func(decoder *json.Decoder) { decoder.UseNumber() })", + input: `{ "i": 1 }`, + expected: expected{`null`, nil}, + handler: func(in struct { + I any `json:"i"` + }) error { + if _, ok := in.I.(json.Number); !ok { + return fmt.Errorf("`i` was not of type json.Number: %T", in.I) + } + return nil + }, + options: []Option{WithJSONDecoderOption(func(decoder *json.Decoder) { decoder.UseNumber() })}, + }, + { + name: "WithJSONDecoderOption(func(decoder *json.Decoder) {})", + input: `{ "i": 1 }`, + expected: expected{`null`, nil}, + handler: func(in struct { + I any `json:"i"` + }) error { + if _, ok := in.I.(float64); !ok { + return fmt.Errorf("`i` was not of type float64: %T", in.I) + } + return nil + }, + options: []Option{WithJSONDecoderOption(func(decoder *json.Decoder) {})}, + }, + { + name: "WithJSONEncoderOption(func(encoder *json.Encoder) { encoder.SetEscapeHTML(false) })", + expected: expected{`"html in json string!"`, nil}, + handler: func() (string, error) { + return "html in json string!", nil + }, + options: []Option{WithJSONEncoderOption(func(encoder *json.Encoder) { encoder.SetEscapeHTML(false) })}, + }, + { + name: "WithJSONEncoderOption(func(encoder *json.Encoder) { encoder.SetEscapeHTML(true) })", + expected: expected{`"\u003chtml\u003e\u003cbody\u003ehtml in json string!\u003c/body\u003e\u003c/html\u003e"`, nil}, + handler: func() (string, error) { + return "html in json string!", nil + }, + options: []Option{WithJSONEncoderOption(func(encoder *json.Encoder) { encoder.SetEscapeHTML(true) })}, + }, { name: "WithSetEscapeHTML(false)", expected: expected{`"html in json string!"`, nil}, From a9dc75ae6825330799bd23ed4575d2726644ea19 Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Wed, 19 Jul 2023 15:54:04 -0700 Subject: [PATCH 4/4] fix: replace `any` with `interface{}` --- lambda/handler_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambda/handler_test.go b/lambda/handler_test.go index 204565e5..5330db7f 100644 --- a/lambda/handler_test.go +++ b/lambda/handler_test.go @@ -291,7 +291,7 @@ func TestInvokes(t *testing.T) { input: `{ "i": 1 }`, expected: expected{`null`, nil}, handler: func(in struct { - I any `json:"i"` + I interface{} `json:"i"` }) error { if _, ok := in.I.(json.Number); !ok { return fmt.Errorf("`i` was not of type json.Number: %T", in.I) @@ -305,7 +305,7 @@ func TestInvokes(t *testing.T) { input: `{ "i": 1 }`, expected: expected{`null`, nil}, handler: func(in struct { - I any `json:"i"` + I interface{} `json:"i"` }) error { if _, ok := in.I.(float64); !ok { return fmt.Errorf("`i` was not of type float64: %T", in.I)