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 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
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
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)
efectn marked this conversation as resolved.
Show resolved Hide resolved

// 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Documentation needs CBOR usage examples.

To maintain consistency with other binder documentation, please add CBOR usage examples similar to the existing JSON and XML examples. This should include:

  1. Example struct with CBOR tags
  2. Example handler using CBOR binding
  3. Curl command demonstrating CBOR request

Here's a suggested addition after the existing examples:

 // XML
 curl -X POST -H "Content-Type: application/xml" --data "<login><name>john</name><pass>doe</pass></login>" localhost:3000
+
+// CBOR
+curl -X POST -H "Content-Type: application/cbor" --data-binary @person.cbor localhost:3000

Also consider adding a complete CBOR example:

### CBOR Binding Example

```go
type Person struct {
    Name string `cbor:"name" json:"name"`
    Pass string `cbor:"pass" json:"pass"`
}

app.Post("/", func(c fiber.Ctx) error {
    p := new(Person)

    if err := c.Bind().Body(p); err != nil {
        return err
    }

    // Additional logic...
    return c.CBOR(p)
})

You can test this endpoint using a tool that supports CBOR, or using Python with the cbor2 library:

import cbor2
import requests

data = {'name': 'john', 'pass': 'doe'}
cbor_data = cbor2.dumps(data)

response = requests.post(
    'http://localhost:3000',
    headers={'Content-Type': 'application/cbor'},
    data=cbor_data
)

---

_:hammer_and_wrench: Refactor suggestion_

**Document new methods mentioned in the AI summary.**

The AI summary mentions several new methods that should be documented:
- `CBOR()` method in the `Bind` struct
- Updated `Body()` method with CBOR support


Consider adding these to the documentation:

```markdown
### CBOR Binding

The `CBOR()` method allows direct binding of CBOR-encoded request bodies:

```go
if err := c.Bind().CBOR(&data); err != nil {
    return err
}

The Body() method automatically handles CBOR content when the Content-Type header is set to application/cbor.


<!-- This is an auto-generated comment by CodeRabbit -->


## 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"
)
gaby marked this conversation as resolved.
Show resolved Hide resolved

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

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

Check warning on line 12 in binder/cbor.go

View check run for this annotation

Codecov / codecov/patch

binder/cbor.go#L11-L12

Added lines #L11 - L12 were not covered by tests
}

// 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)

Check warning on line 17 in binder/cbor.go

View check run for this annotation

Codecov / codecov/patch

binder/cbor.go#L16-L17

Added lines #L16 - L17 were not covered by tests
}
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) {
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
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 @@
// 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 @@
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)

Check warning on line 54 in binder/mapping.go

View check run for this annotation

Codecov / codecov/patch

binder/mapping.go#L54

Added line #L54 was not covered by tests
}
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)
ReneWerner87 marked this conversation as resolved.
Show resolved Hide resolved
resp.Header.VisitAll(func(key, val []byte) {
Expand Down
Loading
Loading