Skip to content

Commit 35a023f

Browse files
committed
feat(encoding)!: simplify encoding interface and reduce allocs
1 parent 3f53fd0 commit 35a023f

File tree

21 files changed

+145
-202
lines changed

21 files changed

+145
-202
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ as your request & response structs.
181181

182182
### Adding custom encoding
183183

184-
Adding your own is easy. See [encoding/json/json.go](./encoding/json/json.go).
184+
Adding your own is easy. See [encoding/xml/xml.go](./encoding/xml/xml.go) for an example.
185185

186186
## Request parsing
187187

encoding/decode.go

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,28 @@
11
package encoding
22

33
import (
4-
"context"
4+
"bytes"
55

6+
"github.com/abemedia/go-don/internal/byteconv"
67
"github.com/valyala/fasthttp"
78
)
89

9-
type (
10-
Unmarshaler = func(data []byte, v any) error
11-
ContextUnmarshaler = func(ctx context.Context, data []byte, v any) error
12-
RequestParser = func(ctx *fasthttp.RequestCtx, v any) error
13-
)
14-
15-
type DecoderConstraint interface {
16-
Unmarshaler | ContextUnmarshaler | RequestParser
17-
}
10+
type RequestDecoder = func(ctx *fasthttp.RequestCtx, v any) error
1811

1912
// RegisterDecoder registers a request decoder for a given media type.
20-
func RegisterDecoder[T DecoderConstraint](dec T, mime string, aliases ...string) {
21-
switch d := any(dec).(type) {
22-
case Unmarshaler:
23-
decoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error {
24-
return d(ctx.Request.Body(), v)
25-
}
26-
27-
case ContextUnmarshaler:
28-
decoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error {
29-
return d(ctx, ctx.Request.Body(), v)
30-
}
31-
32-
case RequestParser:
33-
decoders[mime] = d
34-
}
35-
13+
func RegisterDecoder(dec RequestDecoder, mime string, aliases ...string) {
14+
decoders[mime] = dec
3615
for _, alias := range aliases {
37-
decoders[alias] = decoders[mime]
16+
decoders[alias] = dec
3817
}
3918
}
4019

4120
// GetDecoder returns the request decoder for a given media type.
42-
func GetDecoder(mime string) RequestParser {
43-
return decoders[mime]
21+
func GetDecoder(mime []byte) RequestDecoder {
22+
if i := bytes.IndexByte(mime, ';'); i > 0 {
23+
mime = mime[:i]
24+
}
25+
return decoders[byteconv.Btoa(bytes.TrimSpace(mime))]
4426
}
4527

46-
var decoders = map[string]RequestParser{}
28+
var decoders = map[string]RequestDecoder{}

encoding/decode_test.go

Lines changed: 11 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package encoding_test
22

33
import (
4-
"context"
54
"io"
65
"reflect"
76
"testing"
@@ -12,45 +11,19 @@ import (
1211
)
1312

1413
func TestRegisterDecoder(t *testing.T) {
15-
t.Run("Unmarshaler", func(t *testing.T) {
16-
testRegisterDecoder(t, func(data []byte, v any) error {
17-
if len(data) == 0 {
18-
return io.EOF
19-
}
20-
reflect.ValueOf(v).Elem().SetBytes(data)
21-
return nil
22-
}, "unmarshaler", "unmarshaler-alias")
23-
})
24-
25-
t.Run("ContextUnmarshaler", func(t *testing.T) {
26-
testRegisterDecoder(t, func(ctx context.Context, data []byte, v any) error {
27-
if len(data) == 0 {
28-
return io.EOF
29-
}
30-
reflect.ValueOf(v).Elem().SetBytes(data)
31-
return nil
32-
}, "context-unmarshaler", "context-unmarshaler-alias")
33-
})
34-
35-
t.Run("RequestParser", func(t *testing.T) {
36-
testRegisterDecoder(t, func(ctx *fasthttp.RequestCtx, v any) error {
37-
b := ctx.Request.Body()
38-
if len(b) == 0 {
39-
return io.EOF
40-
}
41-
reflect.ValueOf(v).Elem().SetBytes(b)
42-
return nil
43-
}, "request-parser", "request-parser-alias")
44-
})
45-
}
46-
47-
func testRegisterDecoder[T encoding.DecoderConstraint](t *testing.T, dec T, contentType, alias string) {
48-
t.Helper()
14+
dec := func(ctx *fasthttp.RequestCtx, v any) error {
15+
b := ctx.Request.Body()
16+
if len(b) == 0 {
17+
return io.EOF
18+
}
19+
reflect.ValueOf(v).Elem().SetBytes(b)
20+
return nil
21+
}
4922

50-
encoding.RegisterDecoder(dec, contentType, alias)
23+
encoding.RegisterDecoder(dec, "mime", "alias")
5124

52-
for _, v := range []string{contentType, alias} {
53-
decode := encoding.GetDecoder(v)
25+
for _, v := range []string{"mime", "alias", "mime; charset=utf-8", "alias; charset=utf-8"} {
26+
decode := encoding.GetDecoder([]byte(v))
5427
if decode == nil {
5528
t.Error("decoder not found")
5629
continue

encoding/encode.go

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,37 @@
11
package encoding
22

33
import (
4-
"context"
5-
"strings"
4+
"bytes"
65

6+
"github.com/abemedia/go-don/internal/byteconv"
77
"github.com/valyala/fasthttp"
88
)
99

10-
type (
11-
Marshaler = func(v any) ([]byte, error)
12-
ContextMarshaler = func(ctx context.Context, v any) ([]byte, error)
13-
ResponseEncoder = func(ctx *fasthttp.RequestCtx, v any) error
14-
)
15-
16-
type EncoderConstraint interface {
17-
Marshaler | ContextMarshaler | ResponseEncoder
18-
}
10+
type ResponseEncoder = func(ctx *fasthttp.RequestCtx, v any) error
1911

2012
// RegisterEncoder registers a response encoder on a given media type.
21-
func RegisterEncoder[T EncoderConstraint](enc T, mime string, aliases ...string) {
22-
switch e := any(enc).(type) {
23-
case Marshaler:
24-
encoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error {
25-
b, err := e(v)
26-
if err != nil {
27-
return err
28-
}
29-
ctx.Response.SetBodyRaw(b)
30-
return nil
31-
}
32-
33-
case ContextMarshaler:
34-
encoders[mime] = func(ctx *fasthttp.RequestCtx, v any) error {
35-
b, err := e(ctx, v)
36-
if err != nil {
37-
return err
38-
}
39-
ctx.Response.SetBodyRaw(b)
40-
return nil
41-
}
42-
43-
case ResponseEncoder:
44-
encoders[mime] = e
45-
}
46-
13+
func RegisterEncoder(enc ResponseEncoder, mime string, aliases ...string) {
14+
encoders[mime] = enc
4715
for _, alias := range aliases {
48-
encoders[alias] = encoders[mime]
16+
encoders[alias] = enc
4917
}
5018
}
5119

5220
// GetEncoder returns the response encoder for a given media type.
53-
func GetEncoder(mime string) ResponseEncoder {
54-
mimeParts := strings.Split(mime, ",")
55-
for _, part := range mimeParts {
56-
if enc, ok := encoders[part]; ok {
21+
func GetEncoder(mime []byte) ResponseEncoder {
22+
var contentType []byte
23+
for len(mime) > 0 {
24+
if i := bytes.IndexByte(mime, ','); i >= 0 {
25+
contentType = mime[:i]
26+
mime = mime[i+1:]
27+
} else {
28+
contentType = mime
29+
mime = nil
30+
}
31+
if i := bytes.IndexByte(contentType, ';'); i > 0 {
32+
contentType = contentType[:i]
33+
}
34+
if enc := encoders[byteconv.Btoa(bytes.TrimSpace(contentType))]; enc != nil {
5735
return enc
5836
}
5937
}

encoding/encode_test.go

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package encoding_test
22

33
import (
4-
"context"
54
"io"
65
"testing"
76

@@ -11,45 +10,19 @@ import (
1110
)
1211

1312
func TestRegisterEncoder(t *testing.T) {
14-
t.Run("Marshaler", func(t *testing.T) {
15-
testRegisterEncoder(t, func(v any) ([]byte, error) {
16-
b := v.([]byte)
17-
if len(b) == 0 {
18-
return nil, io.EOF
19-
}
20-
return b, nil
21-
}, "unmarshaler", "marshaler-alias")
22-
})
23-
24-
t.Run("ContextMarshaler", func(t *testing.T) {
25-
testRegisterEncoder(t, func(ctx context.Context, v any) ([]byte, error) {
26-
b := v.([]byte)
27-
if len(b) == 0 {
28-
return nil, io.EOF
29-
}
30-
return b, nil
31-
}, "context-marshaler", "context-marshaler-alias")
32-
})
33-
34-
t.Run("ResponseEncoder", func(t *testing.T) {
35-
testRegisterEncoder(t, func(ctx *fasthttp.RequestCtx, v any) error {
36-
b := v.([]byte)
37-
if len(b) == 0 {
38-
return io.EOF
39-
}
40-
ctx.Response.SetBodyRaw(b)
41-
return nil
42-
}, "response-encoder", "response-encoder-alias")
43-
})
44-
}
45-
46-
func testRegisterEncoder[T encoding.EncoderConstraint](t *testing.T, dec T, contentType, alias string) {
47-
t.Helper()
13+
enc := func(ctx *fasthttp.RequestCtx, v any) error {
14+
b := v.([]byte)
15+
if len(b) == 0 {
16+
return io.EOF
17+
}
18+
ctx.Response.SetBodyRaw(b)
19+
return nil
20+
}
4821

49-
encoding.RegisterEncoder(dec, contentType, alias)
22+
encoding.RegisterEncoder(enc, "response-encoder", "response-encoder-alias")
5023

51-
for _, v := range []string{contentType, alias} {
52-
encode := encoding.GetEncoder(v)
24+
for _, v := range []string{"response-encoder", "response-encoder-alias"} {
25+
encode := encoding.GetEncoder([]byte(v))
5326
if encode == nil {
5427
t.Error("encoder not found")
5528
continue
@@ -76,12 +49,12 @@ func TestGetEncoderMultipleContentTypes(t *testing.T) {
7649

7750
encoding.RegisterEncoder(encFn, "application/xml")
7851

79-
enc := encoding.GetEncoder("text/html,application/xhtml+xml,application/xml")
52+
enc := encoding.GetEncoder([]byte("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"))
8053
if enc == nil {
8154
t.Fatal("encoder not found")
8255
}
8356

84-
enc = encoding.GetEncoder("application/xhtml+xml")
57+
enc = encoding.GetEncoder([]byte("application/xhtml+xml"))
8558
if enc != nil {
8659
t.Fatal("encoder should not be found")
8760
}

encoding/json/json.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@ package json
44
import (
55
"github.com/abemedia/go-don/encoding"
66
"github.com/goccy/go-json"
7+
"github.com/valyala/fasthttp"
78
)
89

10+
func decode(ctx *fasthttp.RequestCtx, v any) error {
11+
return json.Unmarshal(ctx.Request.Body(), v)
12+
}
13+
14+
func encode(ctx *fasthttp.RequestCtx, v any) error {
15+
ctx.SetContentType("application/json")
16+
return json.NewEncoder(ctx).Encode(v)
17+
}
18+
919
func init() {
1020
mediaType := "application/json"
1121

12-
encoding.RegisterDecoder(json.Unmarshal, mediaType)
13-
encoding.RegisterEncoder(json.Marshal, mediaType)
22+
encoding.RegisterDecoder(decode, mediaType)
23+
encoding.RegisterEncoder(encode, mediaType)
1424
}

encoding/json/json_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ type item struct {
1212

1313
var opt = test.EncodingOptions[item]{
1414
Mime: "application/json",
15-
Raw: `{"foo":"bar"}`,
15+
Raw: `{"foo":"bar"}` + "\n",
1616
Parsed: item{Foo: "bar"},
1717
}
1818

encoding/msgpack/msgpack.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,32 @@ package msgpack
33

44
import (
55
"github.com/abemedia/go-don/encoding"
6+
"github.com/valyala/fasthttp"
67
"github.com/vmihailenco/msgpack/v5"
78
)
89

10+
func decode(ctx *fasthttp.RequestCtx, v any) error {
11+
dec := msgpack.GetDecoder()
12+
dec.UsePreallocateValues(true)
13+
dec.Reset(ctx.RequestBodyStream())
14+
err := dec.Decode(v)
15+
msgpack.PutDecoder(dec)
16+
return err
17+
}
18+
19+
func encode(ctx *fasthttp.RequestCtx, v any) error {
20+
ctx.SetContentType("application/msgpack")
21+
b, err := msgpack.Marshal(v)
22+
if err == nil {
23+
ctx.Response.SetBodyRaw(b)
24+
}
25+
return err
26+
}
27+
928
func init() {
1029
mediaType := "application/msgpack"
1130
aliases := []string{"application/x-msgpack", "application/vnd.msgpack"}
1231

13-
encoding.RegisterDecoder(msgpack.Unmarshal, mediaType, aliases...)
14-
encoding.RegisterEncoder(msgpack.Marshal, mediaType, aliases...)
32+
encoding.RegisterDecoder(decode, mediaType, aliases...)
33+
encoding.RegisterEncoder(encode, mediaType, aliases...)
1534
}

0 commit comments

Comments
 (0)