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: Add support for CBOR encoding #3173

Merged
merged 24 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
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
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
// 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
9 changes: 9 additions & 0 deletions bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@
return b.validateStruct(out)
}

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
}

Check warning on line 127 in bind.go

View check run for this annotation

Codecov / codecov/patch

bind.go#L126-L127

Added lines #L126 - L127 were not covered by tests
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 +190,8 @@
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
53 changes: 47 additions & 6 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 @@ -922,31 +923,38 @@ 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)
efectn marked this conversation as resolved.
Show resolved Hide resolved
})

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 @@ -1100,6 +1108,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 @@ -1720,8 +1757,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/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{}
)
15 changes: 15 additions & 0 deletions binder/cbor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package binder

import (
"github.com/gofiber/utils/v2"
)
gaby marked this conversation as resolved.
Show resolved Hide resolved

type cborBinding struct{}

func (*cborBinding) Name() string {
efectn marked this conversation as resolved.
Show resolved Hide resolved
return "cbor"

Check warning on line 10 in binder/cbor.go

View check run for this annotation

Codecov / codecov/patch

binder/cbor.go#L9-L10

Added lines #L9 - L10 were not covered by tests
}

func (*cborBinding) Bind(body []byte, cborDecoder utils.CBORUnmarshal, out any) error {
return cborDecoder(body, out)

Check warning on line 14 in binder/cbor.go

View check run for this annotation

Codecov / codecov/patch

binder/cbor.go#L13-L14

Added lines #L13 - L14 were not covered by tests
}
27 changes: 27 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"sync"
"time"

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

"github.com/gofiber/utils/v2"
Expand Down Expand Up @@ -44,6 +45,8 @@
jsonUnmarshal utils.JSONUnmarshal
xmlMarshal utils.XMLMarshal
xmlUnmarshal utils.XMLUnmarshal
cborMarshal utils.CBORMarshal
cborUnmarshal utils.CBORUnmarshal
gaby marked this conversation as resolved.
Show resolved Hide resolved

cookieJar *CookieJar

Expand Down Expand Up @@ -150,6 +153,28 @@
return c
}

// CBORMarshal returns CBOR marshal function in Core.
func (c *Client) CBORMarshal() utils.CBORMarshal {
return c.cborMarshal
}

// SetCBORMarshal sets CBOR encoder.
func (c *Client) SetCBORMarshal(f utils.CBORMarshal) *Client {
c.cborMarshal = f
return c
}

// CBORUnmarshal returns CBOR unmarshal function in Core.
func (c *Client) CBORUnmarshal() utils.CBORUnmarshal {
return c.cborUnmarshal

Check warning on line 169 in client/client.go

View check run for this annotation

Codecov / codecov/patch

client/client.go#L168-L169

Added lines #L168 - L169 were not covered by tests
}

// SetCBORUnmarshal sets CBOR decoder.
func (c *Client) SetCBORUnmarshal(f utils.CBORUnmarshal) *Client {
c.cborUnmarshal = f
return c

Check warning on line 175 in client/client.go

View check run for this annotation

Codecov / codecov/patch

client/client.go#L173-L175

Added lines #L173 - L175 were not covered by tests
}

// TLSConfig returns tlsConfig in client.
// If client don't have tlsConfig, this function will init it.
func (c *Client) TLSConfig() *tls.Config {
Expand Down Expand Up @@ -706,6 +731,8 @@
jsonMarshal: json.Marshal,
jsonUnmarshal: json.Unmarshal,
xmlMarshal: xml.Marshal,
cborMarshal: cbor.Marshal,
cborUnmarshal: cbor.Unmarshal,
xmlUnmarshal: xml.Unmarshal,
logger: log.DefaultLogger(),
}
Expand Down
32 changes: 31 additions & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"context"
"crypto/tls"
"encoding/hex"
"errors"
"io"
"net"
Expand Down Expand Up @@ -225,6 +226,33 @@
require.Equal(t, errors.New("empty xml"), err)
})

efectn marked this conversation as resolved.
Show resolved Hide resolved
t.Run("set cbor marshal", func(t *testing.T) {
t.Parallel()
bs, err := hex.DecodeString("f6")
if err != nil {
t.Error(err)
}
client := New().
SetCBORMarshal(func(_ any) ([]byte, error) {
return bs, nil
})
val, err := client.CBORMarshal()(nil)

require.NoError(t, err)
require.Equal(t, bs, val)
})

t.Run("set cbor marshal error", func(t *testing.T) {
t.Parallel()
client := New().SetCBORMarshal(func(_ any) ([]byte, error) {
return nil, errors.New("invalid struct")
})

val, err := client.CBORMarshal()(nil)
require.Nil(t, val)
require.Equal(t, errors.New("invalid struct"), err)
})
gaby marked this conversation as resolved.
Show resolved Hide resolved

t.Run("set xml unmarshal", func(t *testing.T) {
t.Parallel()
client := New().
Expand Down Expand Up @@ -1443,10 +1471,12 @@

t.Run("set ctx", func(t *testing.T) {
t.Parallel()
key := struct{}{}

type ctxKey struct{}
var key ctxKey = struct{}{}

ctx := context.Background()
ctx = context.WithValue(ctx, key, "v1") //nolint: staticcheck // not needed for tests

Check failure on line 1479 in client/client_test.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint: staticcheck // not needed for tests` is unused for linter "staticcheck" (nolintlint)

req := AcquireRequest()

Expand Down
9 changes: 9 additions & 0 deletions client/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
headerAccept = "Accept"

applicationJSON = "application/json"
applicationCBOR = "application/cbor"
applicationXML = "application/xml"
applicationForm = "application/x-www-form-urlencoded"
multipartFormData = "multipart/form-data"
Expand Down Expand Up @@ -129,6 +130,8 @@
req.RawRequest.Header.Set(headerAccept, applicationJSON)
case xmlBody:
req.RawRequest.Header.SetContentType(applicationXML)
case cborBody:
req.RawRequest.Header.SetContentType(applicationCBOR)
case formBody:
req.RawRequest.Header.SetContentType(applicationForm)
case filesBody:
Expand Down Expand Up @@ -189,6 +192,12 @@
return err
}
req.RawRequest.SetBody(body)
case cborBody:
efectn marked this conversation as resolved.
Show resolved Hide resolved
body, err := c.cborMarshal(req.body)
if err != nil {
return err
}

Check warning on line 199 in client/hooks.go

View check run for this annotation

Codecov / codecov/patch

client/hooks.go#L198-L199

Added lines #L198 - L199 were not covered by tests
req.RawRequest.SetBody(body)
case formBody:
req.RawRequest.SetBody(req.formData.QueryString())
case filesBody:
Expand Down
25 changes: 25 additions & 0 deletions client/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"testing"

"github.com/fxamacker/cbor/v2"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -456,6 +457,30 @@ func Test_Parser_Request_Body(t *testing.T) {
require.Equal(t, []byte("<body><name>foo</name></body>"), req.RawRequest.Body())
})

t.Run("CBOR body", func(t *testing.T) {
t.Parallel()
type cborData struct {
Name string `cbor:"name"`
Age int `cbor:"age"`
}

data := cborData{
Name: "foo",
Age: 12,
}

client := New()
req := AcquireRequest().
SetCBOR(data)

err := parserRequestBody(client, req)
require.NoError(t, err)

encoded, err := cbor.Marshal(data)
require.NoError(t, err)
require.Equal(t, encoded, req.RawRequest.Body())
})

t.Run("form data body", func(t *testing.T) {
t.Parallel()
client := New()
Expand Down
7 changes: 7 additions & 0 deletions client/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
formBody
filesBody
rawBody
cborBody
)

var ErrClientNil = errors.New("client can not be nil")
Expand Down Expand Up @@ -337,6 +338,12 @@ func (r *Request) SetXML(v any) *Request {
return r
}

func (r *Request) SetCBOR(v any) *Request {
efectn marked this conversation as resolved.
Show resolved Hide resolved
r.body = v
r.bodyType = cborBody
return r
}

// SetRawBody method sets body with raw data in request.
func (r *Request) SetRawBody(v []byte) *Request {
r.body = v
Expand Down
Loading
Loading