Skip to content

Commit 26cc477

Browse files
imsk17gabyefectnReneWerner87
authored
🔥 feat: Add support for CBOR encoding (#3173)
* feat(cbor): allow encoding response bodies in cbor * fix(tests::cbor): encode struct instead of a randomly ordered hashmap * docs(whats_new): add cbor in context section * feat(binder): introduce CBOR * feat(client): allow cbor in fiber client * chore(tests): add more test * chore(packages): go mod tidy * fix(binder): update CBOR name and test * improve test coverage * improve test coverage * update1 * add docs * doc fixes * update * Fix markdown lint * Add missing entry from binder README * add/refresh documentation --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: M. Efe Çetin <efectn@protonmail.com> Co-authored-by: RW <rene@gofiber.io>
1 parent 89452fe commit 26cc477

37 files changed

+760
-198
lines changed

app.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import (
2525
"sync"
2626
"time"
2727

28+
"github.com/fxamacker/cbor/v2"
2829
"github.com/gofiber/fiber/v3/log"
2930
"github.com/gofiber/utils/v2"
30-
3131
"github.com/valyala/fasthttp"
3232
)
3333

@@ -320,6 +320,20 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
320320
// Default: json.Unmarshal
321321
JSONDecoder utils.JSONUnmarshal `json:"-"`
322322

323+
// When set by an external client of Fiber it will use the provided implementation of a
324+
// CBORMarshal
325+
//
326+
// Allowing for flexibility in using another cbor library for encoding
327+
// Default: cbor.Marshal
328+
CBOREncoder utils.CBORMarshal `json:"-"`
329+
330+
// When set by an external client of Fiber it will use the provided implementation of a
331+
// CBORUnmarshal
332+
//
333+
// Allowing for flexibility in using another cbor library for decoding
334+
// Default: cbor.Unmarshal
335+
CBORDecoder utils.CBORUnmarshal `json:"-"`
336+
323337
// XMLEncoder set by an external client of Fiber it will use the provided implementation of a
324338
// XMLMarshal
325339
//
@@ -537,6 +551,12 @@ func New(config ...Config) *App {
537551
if app.config.JSONDecoder == nil {
538552
app.config.JSONDecoder = json.Unmarshal
539553
}
554+
if app.config.CBOREncoder == nil {
555+
app.config.CBOREncoder = cbor.Marshal
556+
}
557+
if app.config.CBORDecoder == nil {
558+
app.config.CBORDecoder = cbor.Unmarshal
559+
}
540560
if app.config.XMLEncoder == nil {
541561
app.config.XMLEncoder = xml.Marshal
542562
}

bind.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ type Bind struct {
2323
dontHandleErrs bool
2424
}
2525

26-
// If you want to handle binder errors manually, you can use `WithoutAutoHandling`.
26+
// WithoutAutoHandling If you want to handle binder errors manually, you can use `WithoutAutoHandling`.
2727
// It's default behavior of binder.
2828
func (b *Bind) WithoutAutoHandling() *Bind {
2929
b.dontHandleErrs = true
3030

3131
return b
3232
}
3333

34-
// If you want to handle binder errors automatically, you can use `WithAutoHandling`.
34+
// WithAutoHandling If you want to handle binder errors automatically, you can use `WithAutoHandling`.
3535
// If there's an error, it will return the error and set HTTP status to `400 Bad Request`.
3636
// You must still return on error explicitly
3737
func (b *Bind) WithAutoHandling() *Bind {
@@ -121,6 +121,14 @@ func (b *Bind) JSON(out any) error {
121121
return b.validateStruct(out)
122122
}
123123

124+
// CBOR binds the body string into the struct.
125+
func (b *Bind) CBOR(out any) error {
126+
if err := b.returnErr(binder.CBORBinder.Bind(b.ctx.Body(), b.ctx.App().Config().CBORDecoder, out)); err != nil {
127+
return err
128+
}
129+
return b.validateStruct(out)
130+
}
131+
124132
// XML binds the body string into the struct.
125133
func (b *Bind) XML(out any) error {
126134
if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil {
@@ -183,6 +191,8 @@ func (b *Bind) Body(out any) error {
183191
return b.JSON(out)
184192
case MIMETextXML, MIMEApplicationXML:
185193
return b.XML(out)
194+
case MIMEApplicationCBOR:
195+
return b.CBOR(out)
186196
case MIMEApplicationForm:
187197
return b.Form(out)
188198
case MIMEMultipartForm:

bind_test.go

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"testing"
1313
"time"
1414

15+
"github.com/fxamacker/cbor/v2"
1516
"github.com/gofiber/fiber/v3/binder"
1617
"github.com/stretchr/testify/require"
1718
"github.com/valyala/fasthttp"
@@ -157,7 +158,7 @@ func Test_Bind_Query_WithSetParserDecoder(t *testing.T) {
157158
}
158159

159160
nonRFCTime := binder.ParserType{
160-
Customtype: NonRFCTime{},
161+
CustomType: NonRFCTime{},
161162
Converter: nonRFCConverter,
162163
}
163164

@@ -411,7 +412,7 @@ func Test_Bind_Header_WithSetParserDecoder(t *testing.T) {
411412
}
412413

413414
nonRFCTime := binder.ParserType{
414-
Customtype: NonRFCTime{},
415+
CustomType: NonRFCTime{},
415416
Converter: nonRFCConverter,
416417
}
417418

@@ -922,31 +923,48 @@ func Test_Bind_Body(t *testing.T) {
922923
testCompressedBody(t, compressedBody, "zstd")
923924
})
924925

925-
testDecodeParser := func(t *testing.T, contentType, body string) {
926+
testDecodeParser := func(t *testing.T, contentType string, body []byte) {
926927
t.Helper()
927928
c := app.AcquireCtx(&fasthttp.RequestCtx{})
928929
c.Request().Header.SetContentType(contentType)
929-
c.Request().SetBody([]byte(body))
930+
c.Request().SetBody(body)
930931
c.Request().Header.SetContentLength(len(body))
931932
d := new(Demo)
932933
require.NoError(t, c.Bind().Body(d))
933934
require.Equal(t, "john", d.Name)
934935
}
935936

936937
t.Run("JSON", func(t *testing.T) {
937-
testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`)
938+
testDecodeParser(t, MIMEApplicationJSON, []byte(`{"name":"john"}`))
939+
})
940+
t.Run("CBOR", func(t *testing.T) {
941+
enc, err := cbor.Marshal(&Demo{Name: "john"})
942+
if err != nil {
943+
t.Error(err)
944+
}
945+
testDecodeParser(t, MIMEApplicationCBOR, enc)
946+
947+
// Test invalid CBOR data
948+
t.Run("Invalid", func(t *testing.T) {
949+
invalidData := []byte{0xFF, 0xFF} // Invalid CBOR data
950+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
951+
c.Request().Header.SetContentType(MIMEApplicationCBOR)
952+
c.Request().SetBody(invalidData)
953+
d := new(Demo)
954+
require.Error(t, c.Bind().Body(d))
955+
})
938956
})
939957

940958
t.Run("XML", func(t *testing.T) {
941-
testDecodeParser(t, MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
959+
testDecodeParser(t, MIMEApplicationXML, []byte(`<Demo><name>john</name></Demo>`))
942960
})
943961

944962
t.Run("Form", func(t *testing.T) {
945-
testDecodeParser(t, MIMEApplicationForm, "name=john")
963+
testDecodeParser(t, MIMEApplicationForm, []byte("name=john"))
946964
})
947965

948966
t.Run("MultipartForm", func(t *testing.T) {
949-
testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
967+
testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--"))
950968
})
951969

952970
testDecodeParserError := func(t *testing.T, contentType, body string) {
@@ -1009,7 +1027,7 @@ func Test_Bind_Body_WithSetParserDecoder(t *testing.T) {
10091027
}
10101028

10111029
customTime := binder.ParserType{
1012-
Customtype: CustomTime{},
1030+
CustomType: CustomTime{},
10131031
Converter: timeConverter,
10141032
}
10151033

@@ -1100,6 +1118,35 @@ func Benchmark_Bind_Body_XML(b *testing.B) {
11001118
require.Equal(b, "john", d.Name)
11011119
}
11021120

1121+
// go test -v -run=^$ -bench=Benchmark_Bind_Body_CBOR -benchmem -count=4
1122+
func Benchmark_Bind_Body_CBOR(b *testing.B) {
1123+
var err error
1124+
1125+
app := New()
1126+
c := app.AcquireCtx(&fasthttp.RequestCtx{})
1127+
1128+
type Demo struct {
1129+
Name string `json:"name"`
1130+
}
1131+
body, err := cbor.Marshal(&Demo{Name: "john"})
1132+
if err != nil {
1133+
b.Error(err)
1134+
}
1135+
c.Request().SetBody(body)
1136+
c.Request().Header.SetContentType(MIMEApplicationCBOR)
1137+
c.Request().Header.SetContentLength(len(body))
1138+
d := new(Demo)
1139+
1140+
b.ReportAllocs()
1141+
b.ResetTimer()
1142+
1143+
for n := 0; n < b.N; n++ {
1144+
err = c.Bind().Body(d)
1145+
}
1146+
require.NoError(b, err)
1147+
require.Equal(b, "john", d.Name)
1148+
}
1149+
11031150
// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4
11041151
func Benchmark_Bind_Body_Form(b *testing.B) {
11051152
var err error
@@ -1404,7 +1451,7 @@ func Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) {
14041451
}
14051452

14061453
nonRFCTime := binder.ParserType{
1407-
Customtype: NonRFCTime{},
1454+
CustomType: NonRFCTime{},
14081455
Converter: nonRFCConverter,
14091456
}
14101457

@@ -1720,8 +1767,12 @@ func Test_Bind_RepeatParserWithSameStruct(t *testing.T) {
17201767
require.Equal(t, "body_param", r.BodyParam)
17211768
}
17221769

1770+
cb, err := cbor.Marshal(&Request{BodyParam: "body_param"})
1771+
require.NoError(t, err, "Failed to marshal CBOR data")
1772+
17231773
testDecodeParser(MIMEApplicationJSON, `{"body_param":"body_param"}`)
17241774
testDecodeParser(MIMEApplicationXML, `<Demo><body_param>body_param</body_param></Demo>`)
1775+
testDecodeParser(MIMEApplicationCBOR, string(cb))
17251776
testDecodeParser(MIMEApplicationForm, "body_param=body_param")
17261777
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--")
17271778
}

binder/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Fiber provides several default binders out of the box:
2222
- [Cookie](cookie.go)
2323
- [JSON](json.go)
2424
- [XML](xml.go)
25+
- [CBOR](cbor.go)
2526

2627
## Guides
2728

binder/binder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ var (
2020
URIBinder = &uriBinding{}
2121
XMLBinder = &xmlBinding{}
2222
JSONBinder = &jsonBinding{}
23+
CBORBinder = &cborBinding{}
2324
)

binder/cbor.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package binder
2+
3+
import (
4+
"github.com/gofiber/utils/v2"
5+
)
6+
7+
// cborBinding is the CBOR binder for CBOR request body.
8+
type cborBinding struct{}
9+
10+
// Name returns the binding name.
11+
func (*cborBinding) Name() string {
12+
return "cbor"
13+
}
14+
15+
// Bind parses the request body as CBOR and returns the result.
16+
func (*cborBinding) Bind(body []byte, cborDecoder utils.CBORUnmarshal, out any) error {
17+
return cborDecoder(body, out)
18+
}

binder/cookie.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"github.com/valyala/fasthttp"
99
)
1010

11+
// cookieBinding is the cookie binder for cookie request body.
1112
type cookieBinding struct{}
1213

14+
// Name returns the binding name.
1315
func (*cookieBinding) Name() string {
1416
return "cookie"
1517
}
1618

19+
// Bind parses the request cookie and returns the result.
1720
func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
1821
data := make(map[string][]string)
1922
var err error

binder/form.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"github.com/valyala/fasthttp"
99
)
1010

11+
// formBinding is the form binder for form request body.
1112
type formBinding struct{}
1213

14+
// Name returns the binding name.
1315
func (*formBinding) Name() string {
1416
return "form"
1517
}
1618

19+
// Bind parses the request body and returns the result.
1720
func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
1821
data := make(map[string][]string)
1922
var err error
@@ -47,6 +50,7 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
4750
return parse(b.Name(), out, data)
4851
}
4952

53+
// BindMultipart parses the request body and returns the result.
5054
func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error {
5155
data, err := reqCtx.MultipartForm()
5256
if err != nil {

binder/header.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ import (
88
"github.com/valyala/fasthttp"
99
)
1010

11+
// headerBinding is the header binder for header request body.
1112
type headerBinding struct{}
1213

14+
// Name returns the binding name.
1315
func (*headerBinding) Name() string {
1416
return "header"
1517
}
1618

19+
// Bind parses the request header and returns the result.
1720
func (b *headerBinding) Bind(req *fasthttp.Request, out any) error {
1821
data := make(map[string][]string)
1922
req.Header.VisitAll(func(key, val []byte) {

binder/json.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import (
44
"github.com/gofiber/utils/v2"
55
)
66

7+
// jsonBinding is the JSON binder for JSON request body.
78
type jsonBinding struct{}
89

10+
// Name returns the binding name.
911
func (*jsonBinding) Name() string {
1012
return "json"
1113
}
1214

15+
// Bind parses the request body as JSON and returns the result.
1316
func (*jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error {
1417
return jsonDecoder(body, out)
1518
}

0 commit comments

Comments
 (0)