Skip to content

Commit

Permalink
πŸ”₯ feat: Add support for CBOR encoding (#3173)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
4 people authored Dec 1, 2024
1 parent 89452fe commit 26cc477
Show file tree
Hide file tree
Showing 37 changed files with 760 additions and 198 deletions.
22 changes: 21 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import (
"sync"
"time"

"github.com/fxamacker/cbor/v2"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/utils/v2"

"github.com/valyala/fasthttp"
)

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

// When set by an external client of Fiber it will use the provided implementation of a
// CBORMarshal
//
// Allowing for flexibility in using another cbor library for encoding
// Default: cbor.Marshal
CBOREncoder utils.CBORMarshal `json:"-"`

// When set by an external client of Fiber it will use the provided implementation of a
// CBORUnmarshal
//
// Allowing for flexibility in using another cbor library for decoding
// Default: cbor.Unmarshal
CBORDecoder utils.CBORUnmarshal `json:"-"`

// XMLEncoder set by an external client of Fiber it will use the provided implementation of a
// XMLMarshal
//
Expand Down Expand Up @@ -537,6 +551,12 @@ func New(config ...Config) *App {
if app.config.JSONDecoder == nil {
app.config.JSONDecoder = json.Unmarshal
}
if app.config.CBOREncoder == nil {
app.config.CBOREncoder = cbor.Marshal
}
if app.config.CBORDecoder == nil {
app.config.CBORDecoder = cbor.Unmarshal
}
if app.config.XMLEncoder == nil {
app.config.XMLEncoder = xml.Marshal
}
Expand Down
14 changes: 12 additions & 2 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ type Bind struct {
dontHandleErrs bool
}

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

return b
}

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

// CBOR binds the body string into the struct.
func (b *Bind) CBOR(out any) error {
if err := b.returnErr(binder.CBORBinder.Bind(b.ctx.Body(), b.ctx.App().Config().CBORDecoder, out)); err != nil {
return err
}
return b.validateStruct(out)
}

// XML binds the body string into the struct.
func (b *Bind) XML(out any) error {
if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil {
Expand Down Expand Up @@ -183,6 +191,8 @@ func (b *Bind) Body(out any) error {
return b.JSON(out)
case MIMETextXML, MIMEApplicationXML:
return b.XML(out)
case MIMEApplicationCBOR:
return b.CBOR(out)
case MIMEApplicationForm:
return b.Form(out)
case MIMEMultipartForm:
Expand Down
71 changes: 61 additions & 10 deletions bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"
"time"

"github.com/fxamacker/cbor/v2"
"github.com/gofiber/fiber/v3/binder"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
Expand Down Expand Up @@ -157,7 +158,7 @@ func Test_Bind_Query_WithSetParserDecoder(t *testing.T) {
}

nonRFCTime := binder.ParserType{
Customtype: NonRFCTime{},
CustomType: NonRFCTime{},
Converter: nonRFCConverter,
}

Expand Down Expand Up @@ -411,7 +412,7 @@ func Test_Bind_Header_WithSetParserDecoder(t *testing.T) {
}

nonRFCTime := binder.ParserType{
Customtype: NonRFCTime{},
CustomType: NonRFCTime{},
Converter: nonRFCConverter,
}

Expand Down Expand Up @@ -922,31 +923,48 @@ func Test_Bind_Body(t *testing.T) {
testCompressedBody(t, compressedBody, "zstd")
})

testDecodeParser := func(t *testing.T, contentType, body string) {
testDecodeParser := func(t *testing.T, contentType string, body []byte) {
t.Helper()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.SetContentType(contentType)
c.Request().SetBody([]byte(body))
c.Request().SetBody(body)
c.Request().Header.SetContentLength(len(body))
d := new(Demo)
require.NoError(t, c.Bind().Body(d))
require.Equal(t, "john", d.Name)
}

t.Run("JSON", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`)
testDecodeParser(t, MIMEApplicationJSON, []byte(`{"name":"john"}`))
})
t.Run("CBOR", func(t *testing.T) {
enc, err := cbor.Marshal(&Demo{Name: "john"})
if err != nil {
t.Error(err)
}
testDecodeParser(t, MIMEApplicationCBOR, enc)

// Test invalid CBOR data
t.Run("Invalid", func(t *testing.T) {
invalidData := []byte{0xFF, 0xFF} // Invalid CBOR data
c := app.AcquireCtx(&fasthttp.RequestCtx{})
c.Request().Header.SetContentType(MIMEApplicationCBOR)
c.Request().SetBody(invalidData)
d := new(Demo)
require.Error(t, c.Bind().Body(d))
})
})

t.Run("XML", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
testDecodeParser(t, MIMEApplicationXML, []byte(`<Demo><name>john</name></Demo>`))
})

t.Run("Form", func(t *testing.T) {
testDecodeParser(t, MIMEApplicationForm, "name=john")
testDecodeParser(t, MIMEApplicationForm, []byte("name=john"))
})

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

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

customTime := binder.ParserType{
Customtype: CustomTime{},
CustomType: CustomTime{},
Converter: timeConverter,
}

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

// go test -v -run=^$ -bench=Benchmark_Bind_Body_CBOR -benchmem -count=4
func Benchmark_Bind_Body_CBOR(b *testing.B) {
var err error

app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

type Demo struct {
Name string `json:"name"`
}
body, err := cbor.Marshal(&Demo{Name: "john"})
if err != nil {
b.Error(err)
}
c.Request().SetBody(body)
c.Request().Header.SetContentType(MIMEApplicationCBOR)
c.Request().Header.SetContentLength(len(body))
d := new(Demo)

b.ReportAllocs()
b.ResetTimer()

for n := 0; n < b.N; n++ {
err = c.Bind().Body(d)
}
require.NoError(b, err)
require.Equal(b, "john", d.Name)
}

// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4
func Benchmark_Bind_Body_Form(b *testing.B) {
var err error
Expand Down Expand Up @@ -1404,7 +1451,7 @@ func Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) {
}

nonRFCTime := binder.ParserType{
Customtype: NonRFCTime{},
CustomType: NonRFCTime{},
Converter: nonRFCConverter,
}

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

cb, err := cbor.Marshal(&Request{BodyParam: "body_param"})
require.NoError(t, err, "Failed to marshal CBOR data")

testDecodeParser(MIMEApplicationJSON, `{"body_param":"body_param"}`)
testDecodeParser(MIMEApplicationXML, `<Demo><body_param>body_param</body_param></Demo>`)
testDecodeParser(MIMEApplicationCBOR, string(cb))
testDecodeParser(MIMEApplicationForm, "body_param=body_param")
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--")
}
1 change: 1 addition & 0 deletions binder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Fiber provides several default binders out of the box:
- [Cookie](cookie.go)
- [JSON](json.go)
- [XML](xml.go)
- [CBOR](cbor.go)

## Guides

Expand Down
1 change: 1 addition & 0 deletions binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ var (
URIBinder = &uriBinding{}
XMLBinder = &xmlBinding{}
JSONBinder = &jsonBinding{}
CBORBinder = &cborBinding{}
)
18 changes: 18 additions & 0 deletions binder/cbor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package binder

import (
"github.com/gofiber/utils/v2"
)

// cborBinding is the CBOR binder for CBOR request body.
type cborBinding struct{}

// Name returns the binding name.
func (*cborBinding) Name() string {
return "cbor"
}

// Bind parses the request body as CBOR and returns the result.
func (*cborBinding) Bind(body []byte, cborDecoder utils.CBORUnmarshal, out any) error {
return cborDecoder(body, out)
}
3 changes: 3 additions & 0 deletions binder/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"github.com/valyala/fasthttp"
)

// cookieBinding is the cookie binder for cookie request body.
type cookieBinding struct{}

// Name returns the binding name.
func (*cookieBinding) Name() string {
return "cookie"
}

// Bind parses the request cookie and returns the result.
func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
data := make(map[string][]string)
var err error
Expand Down
4 changes: 4 additions & 0 deletions binder/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"github.com/valyala/fasthttp"
)

// formBinding is the form binder for form request body.
type formBinding struct{}

// Name returns the binding name.
func (*formBinding) Name() string {
return "form"
}

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

// BindMultipart parses the request body and returns the result.
func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error {
data, err := reqCtx.MultipartForm()
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions binder/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"github.com/valyala/fasthttp"
)

// headerBinding is the header binder for header request body.
type headerBinding struct{}

// Name returns the binding name.
func (*headerBinding) Name() string {
return "header"
}

// Bind parses the request header and returns the result.
func (b *headerBinding) Bind(req *fasthttp.Request, out any) error {
data := make(map[string][]string)
req.Header.VisitAll(func(key, val []byte) {
Expand Down
3 changes: 3 additions & 0 deletions binder/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"github.com/gofiber/utils/v2"
)

// jsonBinding is the JSON binder for JSON request body.
type jsonBinding struct{}

// Name returns the binding name.
func (*jsonBinding) Name() string {
return "json"
}

// Bind parses the request body as JSON and returns the result.
func (*jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error {
return jsonDecoder(body, out)
}
4 changes: 2 additions & 2 deletions binder/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type ParserConfig struct {
// ParserType require two element, type and converter for register.
// Use ParserType with BodyParser for parsing custom type in form data.
type ParserType struct {
Customtype any
CustomType any
Converter func(string) reflect.Value
}

Expand All @@ -51,7 +51,7 @@ func decoderBuilder(parserConfig ParserConfig) any {
decoder.SetAliasTag(parserConfig.SetAliasTag)
}
for _, v := range parserConfig.ParserType {
decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter)
decoder.RegisterConverter(reflect.ValueOf(v.CustomType).Interface(), v.Converter)
}
decoder.ZeroEmpty(parserConfig.ZeroEmpty)
return decoder
Expand Down
3 changes: 3 additions & 0 deletions binder/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"github.com/valyala/fasthttp"
)

// queryBinding is the query binder for query request body.
type queryBinding struct{}

// Name returns the binding name.
func (*queryBinding) Name() string {
return "query"
}

// Bind parses the request query and returns the result.
func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
data := make(map[string][]string)
var err error
Expand Down
3 changes: 3 additions & 0 deletions binder/resp_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"github.com/valyala/fasthttp"
)

// respHeaderBinding is the respHeader binder for response header.
type respHeaderBinding struct{}

// Name returns the binding name.
func (*respHeaderBinding) Name() string {
return "respHeader"
}

// Bind parses the response header and returns the result.
func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
data := make(map[string][]string)
resp.Header.VisitAll(func(key, val []byte) {
Expand Down
Loading

1 comment on commit 26cc477

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 26cc477 Previous: 8c84b0f Ratio
Benchmark_Ctx_Send 7.132 ns/op 0 B/op 0 allocs/op 4.335 ns/op 0 B/op 0 allocs/op 1.65
Benchmark_Ctx_Send - ns/op 7.132 ns/op 4.335 ns/op 1.65
Benchmark_Ctx_SendString_B 15.56 ns/op 0 B/op 0 allocs/op 7.764 ns/op 0 B/op 0 allocs/op 2.00
Benchmark_Ctx_SendString_B - ns/op 15.56 ns/op 7.764 ns/op 2.00
Benchmark_Utils_GetOffer/1_parameter 212.2 ns/op 0 B/op 0 allocs/op 131 ns/op 0 B/op 0 allocs/op 1.62
Benchmark_Utils_GetOffer/1_parameter - ns/op 212.2 ns/op 131 ns/op 1.62
Benchmark_Middleware_BasicAuth - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_Middleware_BasicAuth_Upper - B/op 80 B/op 48 B/op 1.67
Benchmark_Middleware_BasicAuth_Upper - allocs/op 5 allocs/op 3 allocs/op 1.67
Benchmark_CORS_NewHandler - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandler - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - B/op 16 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerSingleOrigin - allocs/op 1 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflight - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflight - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightSingleOrigin - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - B/op 104 B/op 0 B/op +∞
Benchmark_CORS_NewHandlerPreflightWildcard - allocs/op 5 allocs/op 0 allocs/op +∞
Benchmark_Middleware_CSRF_GenerateToken - B/op 519 B/op 327 B/op 1.59
Benchmark_Middleware_CSRF_GenerateToken - allocs/op 10 allocs/op 6 allocs/op 1.67

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.