Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide the options to set arbitrary json encoder and decoder options #513

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 61 additions & 17 deletions lambda/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ 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)
setIndentUsed bool
enableSIGTERM bool
sigtermCallbacks []func()
}

type Option func(*handlerOptions)
Expand All @@ -48,6 +48,44 @@ func WithContext(ctx context.Context) Option {
})
}

// WithJSONEncoderOption allows setting arbitrary options on the underlying json encoder
//
// Usage:
//
// lambda.StartWithOptions(
// func () (string, error) {
// return "<html><body>hello!></body></html>", 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:
Expand All @@ -59,8 +97,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)
})
}

Expand All @@ -76,8 +114,13 @@ func WithSetEscapeHTML(escapeHTML bool) Option {
// )
func WithSetIndent(prefix, indent string) Option {
return Option(func(h *handlerOptions) {
h.jsonResponseIndentPrefix = prefix
h.jsonResponseIndentValue = indent
// 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)
})
})
}

Expand Down Expand Up @@ -176,10 +219,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)
Expand Down Expand Up @@ -267,9 +307,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)

Expand Down Expand Up @@ -325,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
Expand Down
45 changes: 45 additions & 0 deletions lambda/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package lambda
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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 interface{} `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 interface{} `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><body>html in json string!</body></html>"`, nil},
handler: func() (string, error) {
return "<html><body>html in json string!</body></html>", 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><body>html in json string!</body></html>", nil
},
options: []Option{WithJSONEncoderOption(func(encoder *json.Encoder) { encoder.SetEscapeHTML(true) })},
},
{
name: "WithSetEscapeHTML(false)",
expected: expected{`"<html><body>html in json string!</body></html>"`, nil},
Expand Down