mirror of
https://github.com/gofiber/fiber.git
synced 2025-02-06 11:02:01 +00:00
🐛 bug: fix EnableSplittingOnParsers is not functional (#3231)
* 🐛 bug: fix EnableSplittingOnParsers is not functional
* remove wrong testcase
* add support for external xml decoders
* improve test coverage
* fix linter
* update
* add reset methods
* improve test coverage
* merge Form and MultipartForm methods
* fix linter
* split reset and putting steps
* fix linter
This commit is contained in:
parent
58677d5c86
commit
57744ebbe8
10
app.go
10
app.go
@ -341,6 +341,13 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
|
||||
// Default: xml.Marshal
|
||||
XMLEncoder utils.XMLMarshal `json:"-"`
|
||||
|
||||
// XMLDecoder set by an external client of Fiber it will use the provided implementation of a
|
||||
// XMLUnmarshal
|
||||
//
|
||||
// Allowing for flexibility in using another XML library for decoding
|
||||
// Default: xml.Unmarshal
|
||||
XMLDecoder utils.XMLUnmarshal `json:"-"`
|
||||
|
||||
// If you find yourself behind some sort of proxy, like a load balancer,
|
||||
// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.
|
||||
// For example, the Host HTTP header is usually used to return the requested host.
|
||||
@ -560,6 +567,9 @@ func New(config ...Config) *App {
|
||||
if app.config.XMLEncoder == nil {
|
||||
app.config.XMLEncoder = xml.Marshal
|
||||
}
|
||||
if app.config.XMLDecoder == nil {
|
||||
app.config.XMLDecoder = xml.Unmarshal
|
||||
}
|
||||
if len(app.config.RequestMethods) == 0 {
|
||||
app.config.RequestMethods = DefaultMethods
|
||||
}
|
||||
|
109
bind.go
109
bind.go
@ -77,7 +77,16 @@ func (b *Bind) Custom(name string, dest any) error {
|
||||
|
||||
// Header binds the request header strings into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) Header(out any) error {
|
||||
if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.HeaderBinding](&binder.HeaderBinderPool)
|
||||
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.HeaderBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Request(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -86,7 +95,16 @@ func (b *Bind) Header(out any) error {
|
||||
|
||||
// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) RespHeader(out any) error {
|
||||
if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.RespHeaderBinding](&binder.RespHeaderBinderPool)
|
||||
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.RespHeaderBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Response(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -96,7 +114,16 @@ func (b *Bind) RespHeader(out any) error {
|
||||
// Cookie binds the request cookie strings into the struct, map[string]string and map[string][]string.
|
||||
// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie.
|
||||
func (b *Bind) Cookie(out any) error {
|
||||
if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.CookieBinding](&binder.CookieBinderPool)
|
||||
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.CookieBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -105,7 +132,16 @@ func (b *Bind) Cookie(out any) error {
|
||||
|
||||
// Query binds the query string into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) Query(out any) error {
|
||||
if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.QueryBinding](&binder.QueryBinderPool)
|
||||
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.QueryBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -114,7 +150,16 @@ func (b *Bind) Query(out any) error {
|
||||
|
||||
// JSON binds the body string into the struct.
|
||||
func (b *Bind) JSON(out any) error {
|
||||
if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.JSONBinding](&binder.JSONBinderPool)
|
||||
bind.JSONDecoder = b.ctx.App().Config().JSONDecoder
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.JSONBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -123,7 +168,16 @@ func (b *Bind) JSON(out any) error {
|
||||
|
||||
// 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 {
|
||||
bind := binder.GetFromThePool[*binder.CBORBinding](&binder.CBORBinderPool)
|
||||
bind.CBORDecoder = b.ctx.App().Config().CBORDecoder
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.CBORBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.validateStruct(out)
|
||||
@ -131,7 +185,16 @@ func (b *Bind) CBOR(out any) error {
|
||||
|
||||
// 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 {
|
||||
bind := binder.GetFromThePool[*binder.XMLBinding](&binder.XMLBinderPool)
|
||||
bind.XMLDecoder = b.ctx.App().config.XMLDecoder
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.XMLBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -139,8 +202,20 @@ func (b *Bind) XML(out any) error {
|
||||
}
|
||||
|
||||
// Form binds the form into the struct, map[string]string and map[string][]string.
|
||||
// If Content-Type is "application/x-www-form-urlencoded" or "multipart/form-data", it will bind the form values.
|
||||
//
|
||||
// Binding multipart files is not supported yet.
|
||||
func (b *Bind) Form(out any) error {
|
||||
if err := b.returnErr(binder.FormBinder.Bind(b.ctx.RequestCtx(), out)); err != nil {
|
||||
bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool)
|
||||
bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers
|
||||
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
bind.Reset()
|
||||
binder.PutToThePool(&binder.FormBinderPool, bind)
|
||||
}()
|
||||
|
||||
if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -149,16 +224,14 @@ func (b *Bind) Form(out any) error {
|
||||
|
||||
// URI binds the route parameters into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) URI(out any) error {
|
||||
if err := b.returnErr(binder.URIBinder.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
bind := binder.GetFromThePool[*binder.URIBinding](&binder.URIBinderPool)
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
// Reset & put binder
|
||||
defer func() {
|
||||
binder.PutToThePool(&binder.URIBinderPool, bind)
|
||||
}()
|
||||
|
||||
// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) MultipartForm(out any) error {
|
||||
if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.RequestCtx(), out)); err != nil {
|
||||
if err := b.returnErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -193,10 +266,8 @@ func (b *Bind) Body(out any) error {
|
||||
return b.XML(out)
|
||||
case MIMEApplicationCBOR:
|
||||
return b.CBOR(out)
|
||||
case MIMEApplicationForm:
|
||||
case MIMEApplicationForm, MIMEMultipartForm:
|
||||
return b.Form(out)
|
||||
case MIMEMultipartForm:
|
||||
return b.MultipartForm(out)
|
||||
}
|
||||
|
||||
// No suitable content type found
|
||||
|
38
bind_test.go
38
bind_test.go
@ -32,7 +32,9 @@ func Test_returnErr(t *testing.T) {
|
||||
// go test -run Test_Bind_Query -v
|
||||
func Test_Bind_Query(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query struct {
|
||||
@ -111,7 +113,9 @@ func Test_Bind_Query(t *testing.T) {
|
||||
func Test_Bind_Query_Map(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Request().SetBody([]byte(``))
|
||||
@ -318,13 +322,13 @@ func Test_Bind_Header(t *testing.T) {
|
||||
c.Request().Header.Add("Hobby", "golang,fiber")
|
||||
q := new(Header)
|
||||
require.NoError(t, c.Bind().Header(q))
|
||||
require.Len(t, q.Hobby, 2)
|
||||
require.Len(t, q.Hobby, 1)
|
||||
|
||||
c.Request().Header.Del("hobby")
|
||||
c.Request().Header.Add("Hobby", "golang,fiber,go")
|
||||
q = new(Header)
|
||||
require.NoError(t, c.Bind().Header(q))
|
||||
require.Len(t, q.Hobby, 3)
|
||||
require.Len(t, q.Hobby, 1)
|
||||
|
||||
empty := new(Header)
|
||||
c.Request().Header.Del("hobby")
|
||||
@ -357,7 +361,7 @@ func Test_Bind_Header(t *testing.T) {
|
||||
require.Equal(t, "go,fiber", h2.Hobby)
|
||||
require.True(t, h2.Bool)
|
||||
require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten
|
||||
require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks)
|
||||
require.Equal(t, []string{"milo,coke,pepsi"}, h2.FavouriteDrinks)
|
||||
var nilSlice []string
|
||||
require.Equal(t, nilSlice, h2.Empty)
|
||||
require.Equal(t, []string{""}, h2.Alloc)
|
||||
@ -386,13 +390,13 @@ func Test_Bind_Header_Map(t *testing.T) {
|
||||
c.Request().Header.Add("Hobby", "golang,fiber")
|
||||
q := make(map[string][]string, 0)
|
||||
require.NoError(t, c.Bind().Header(&q))
|
||||
require.Len(t, q["Hobby"], 2)
|
||||
require.Len(t, q["Hobby"], 1)
|
||||
|
||||
c.Request().Header.Del("hobby")
|
||||
c.Request().Header.Add("Hobby", "golang,fiber,go")
|
||||
q = make(map[string][]string, 0)
|
||||
require.NoError(t, c.Bind().Header(&q))
|
||||
require.Len(t, q["Hobby"], 3)
|
||||
require.Len(t, q["Hobby"], 1)
|
||||
|
||||
empty := make(map[string][]string, 0)
|
||||
c.Request().Header.Del("hobby")
|
||||
@ -543,7 +547,9 @@ func Test_Bind_Header_Schema(t *testing.T) {
|
||||
// go test -run Test_Bind_Resp_Header -v
|
||||
func Test_Bind_RespHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Header struct {
|
||||
@ -627,13 +633,13 @@ func Test_Bind_RespHeader_Map(t *testing.T) {
|
||||
c.Response().Header.Add("Hobby", "golang,fiber")
|
||||
q := make(map[string][]string, 0)
|
||||
require.NoError(t, c.Bind().RespHeader(&q))
|
||||
require.Len(t, q["Hobby"], 2)
|
||||
require.Len(t, q["Hobby"], 1)
|
||||
|
||||
c.Response().Header.Del("hobby")
|
||||
c.Response().Header.Add("Hobby", "golang,fiber,go")
|
||||
q = make(map[string][]string, 0)
|
||||
require.NoError(t, c.Bind().RespHeader(&q))
|
||||
require.Len(t, q["Hobby"], 3)
|
||||
require.Len(t, q["Hobby"], 1)
|
||||
|
||||
empty := make(map[string][]string, 0)
|
||||
c.Response().Header.Del("hobby")
|
||||
@ -751,7 +757,9 @@ func Benchmark_Bind_Query_WithParseParam(b *testing.B) {
|
||||
func Benchmark_Bind_Query_Comma(b *testing.B) {
|
||||
var err error
|
||||
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query struct {
|
||||
@ -1341,7 +1349,9 @@ func Benchmark_Bind_URI_Map(b *testing.B) {
|
||||
func Test_Bind_Cookie(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Cookie struct {
|
||||
@ -1414,7 +1424,9 @@ func Test_Bind_Cookie(t *testing.T) {
|
||||
func Test_Bind_Cookie_Map(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
c := app.AcquireCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Request().SetBody([]byte(``))
|
||||
|
@ -2,6 +2,7 @@ package binder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Binder errors
|
||||
@ -10,15 +11,69 @@ var (
|
||||
ErrMapNotConvertable = errors.New("binder: map is not convertable to map[string]string or map[string][]string")
|
||||
)
|
||||
|
||||
// Init default binders for Fiber
|
||||
var (
|
||||
HeaderBinder = &headerBinding{}
|
||||
RespHeaderBinder = &respHeaderBinding{}
|
||||
CookieBinder = &cookieBinding{}
|
||||
QueryBinder = &queryBinding{}
|
||||
FormBinder = &formBinding{}
|
||||
URIBinder = &uriBinding{}
|
||||
XMLBinder = &xmlBinding{}
|
||||
JSONBinder = &jsonBinding{}
|
||||
CBORBinder = &cborBinding{}
|
||||
)
|
||||
var HeaderBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &HeaderBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var RespHeaderBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &RespHeaderBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var CookieBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &CookieBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var QueryBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &QueryBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var FormBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &FormBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var URIBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &URIBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var XMLBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &XMLBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var JSONBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &JSONBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
var CBORBinderPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &CBORBinding{}
|
||||
},
|
||||
}
|
||||
|
||||
func GetFromThePool[T any](pool *sync.Pool) T {
|
||||
binder, ok := pool.Get().(T)
|
||||
if !ok {
|
||||
panic(errors.New("failed to type-assert to T"))
|
||||
}
|
||||
|
||||
return binder
|
||||
}
|
||||
|
||||
func PutToThePool[T any](pool *sync.Pool, binder T) {
|
||||
pool.Put(binder)
|
||||
}
|
||||
|
28
binder/binder_test.go
Normal file
28
binder/binder_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetAndPutToThePool(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Panics in case we get from another pool
|
||||
require.Panics(t, func() {
|
||||
_ = GetFromThePool[*HeaderBinding](&CookieBinderPool)
|
||||
})
|
||||
|
||||
// We get from the pool
|
||||
binder := GetFromThePool[*HeaderBinding](&HeaderBinderPool)
|
||||
PutToThePool(&HeaderBinderPool, binder)
|
||||
|
||||
_ = GetFromThePool[*RespHeaderBinding](&RespHeaderBinderPool)
|
||||
_ = GetFromThePool[*QueryBinding](&QueryBinderPool)
|
||||
_ = GetFromThePool[*FormBinding](&FormBinderPool)
|
||||
_ = GetFromThePool[*URIBinding](&URIBinderPool)
|
||||
_ = GetFromThePool[*XMLBinding](&XMLBinderPool)
|
||||
_ = GetFromThePool[*JSONBinding](&JSONBinderPool)
|
||||
_ = GetFromThePool[*CBORBinding](&CBORBinderPool)
|
||||
}
|
@ -4,15 +4,22 @@ import (
|
||||
"github.com/gofiber/utils/v2"
|
||||
)
|
||||
|
||||
// cborBinding is the CBOR binder for CBOR request body.
|
||||
type cborBinding struct{}
|
||||
// CBORBinding is the CBOR binder for CBOR request body.
|
||||
type CBORBinding struct {
|
||||
CBORDecoder utils.CBORUnmarshal
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*cborBinding) Name() string {
|
||||
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)
|
||||
func (b *CBORBinding) Bind(body []byte, out any) error {
|
||||
return b.CBORDecoder(body, out)
|
||||
}
|
||||
|
||||
// Reset resets the CBORBinding binder.
|
||||
func (b *CBORBinding) Reset() {
|
||||
b.CBORDecoder = nil
|
||||
}
|
||||
|
92
binder/cbor_test.go
Normal file
92
binder/cbor_test.go
Normal file
@ -0,0 +1,92 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_CBORBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &CBORBinding{
|
||||
CBORDecoder: cbor.Unmarshal,
|
||||
}
|
||||
require.Equal(t, "cbor", b.Name())
|
||||
|
||||
type Post struct {
|
||||
Title string `cbor:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `cbor:"name"`
|
||||
Posts []Post `cbor:"posts"`
|
||||
Names []string `cbor:"names"`
|
||||
Age int `cbor:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
wantedUser := User{
|
||||
Name: "john",
|
||||
Names: []string{
|
||||
"john",
|
||||
"doe",
|
||||
},
|
||||
Age: 42,
|
||||
Posts: []Post{
|
||||
{Title: "post1"},
|
||||
{Title: "post2"},
|
||||
{Title: "post3"},
|
||||
},
|
||||
}
|
||||
|
||||
body, err := cbor.Marshal(wantedUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.Bind(body, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Len(t, user.Posts, 3)
|
||||
require.Equal(t, "post1", user.Posts[0].Title)
|
||||
require.Equal(t, "post2", user.Posts[1].Title)
|
||||
require.Equal(t, "post3", user.Posts[2].Title)
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
|
||||
b.Reset()
|
||||
require.Nil(t, b.CBORDecoder)
|
||||
}
|
||||
|
||||
func Benchmark_CBORBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &CBORBinding{
|
||||
CBORDecoder: cbor.Unmarshal,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `cbor:"name"`
|
||||
Age int `cbor:"age"`
|
||||
}
|
||||
|
||||
var user User
|
||||
wantedUser := User{
|
||||
Name: "john",
|
||||
Age: 42,
|
||||
}
|
||||
|
||||
body, err := cbor.Marshal(wantedUser)
|
||||
require.NoError(b, err)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(body, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
}
|
@ -8,20 +8,22 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// cookieBinding is the cookie binder for cookie request body.
|
||||
type cookieBinding struct{}
|
||||
// CookieBinding is the cookie binder for cookie request body.
|
||||
type CookieBinding struct {
|
||||
EnableSplitting bool
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*cookieBinding) Name() string {
|
||||
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 {
|
||||
func (b *CookieBinding) Bind(req *fasthttp.Request, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.Request.Header.VisitAllCookie(func(key, val []byte) {
|
||||
req.Header.VisitAllCookie(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -29,7 +31,7 @@ func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
@ -45,3 +47,8 @@ func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
// Reset resets the CookieBinding binder.
|
||||
func (b *CookieBinding) Reset() {
|
||||
b.EnableSplitting = false
|
||||
}
|
||||
|
90
binder/cookie_test.go
Normal file
90
binder/cookie_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_CookieBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &CookieBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "cookie", b.Name())
|
||||
|
||||
type Post struct {
|
||||
Title string `form:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `form:"name"`
|
||||
Names []string `form:"names"`
|
||||
Posts []Post `form:"posts"`
|
||||
Age int `form:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
|
||||
req.Header.SetCookie("name", "john")
|
||||
req.Header.SetCookie("names", "john,doe")
|
||||
req.Header.SetCookie("age", "42")
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
err := b.Bind(req, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
|
||||
b.Reset()
|
||||
require.False(t, b.EnableSplitting)
|
||||
}
|
||||
|
||||
func Benchmark_CookieBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &CookieBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `query:"name"`
|
||||
Posts []string `query:"posts"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
b.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
req.Header.SetCookie("name", "john")
|
||||
req.Header.SetCookie("age", "42")
|
||||
req.Header.SetCookie("posts", "post1,post2,post3")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(req, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
require.Contains(b, user.Posts, "post1")
|
||||
require.Contains(b, user.Posts, "post2")
|
||||
require.Contains(b, user.Posts, "post3")
|
||||
}
|
@ -8,20 +8,29 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// formBinding is the form binder for form request body.
|
||||
type formBinding struct{}
|
||||
const MIMEMultipartForm string = "multipart/form-data"
|
||||
|
||||
// FormBinding is the form binder for form request body.
|
||||
type FormBinding struct {
|
||||
EnableSplitting bool
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*formBinding) Name() string {
|
||||
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 {
|
||||
func (b *FormBinding) Bind(req *fasthttp.Request, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.PostArgs().VisitAll(func(key, val []byte) {
|
||||
// Handle multipart form
|
||||
if FilterFlags(utils.UnsafeString(req.Header.ContentType())) == MIMEMultipartForm {
|
||||
return b.bindMultipart(req, out)
|
||||
}
|
||||
|
||||
req.PostArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -33,7 +42,7 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
@ -50,12 +59,17 @@ 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()
|
||||
// bindMultipart parses the request body and returns the result.
|
||||
func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error {
|
||||
data, err := req.MultipartForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parse(b.Name(), out, data.Value)
|
||||
}
|
||||
|
||||
// Reset resets the FormBinding binder.
|
||||
func (b *FormBinding) Reset() {
|
||||
b.EnableSplitting = false
|
||||
}
|
||||
|
174
binder/form_test.go
Normal file
174
binder/form_test.go
Normal file
@ -0,0 +1,174 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_FormBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &FormBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "form", b.Name())
|
||||
|
||||
type Post struct {
|
||||
Title string `form:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `form:"name"`
|
||||
Names []string `form:"names"`
|
||||
Posts []Post `form:"posts"`
|
||||
Age int `form:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
req.SetBodyString("name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3")
|
||||
req.Header.SetContentType("application/x-www-form-urlencoded")
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
err := b.Bind(req, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Len(t, user.Posts, 3)
|
||||
require.Equal(t, "post1", user.Posts[0].Title)
|
||||
require.Equal(t, "post2", user.Posts[1].Title)
|
||||
require.Equal(t, "post3", user.Posts[2].Title)
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
|
||||
b.Reset()
|
||||
require.False(t, b.EnableSplitting)
|
||||
}
|
||||
|
||||
func Benchmark_FormBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &QueryBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `query:"name"`
|
||||
Posts []string `query:"posts"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3")
|
||||
req.Header.SetContentType("application/x-www-form-urlencoded")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(req, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
}
|
||||
|
||||
func Test_FormBinder_BindMultipart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &FormBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "form", b.Name())
|
||||
|
||||
type User struct {
|
||||
Name string `form:"name"`
|
||||
Names []string `form:"names"`
|
||||
Age int `form:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
mw := multipart.NewWriter(buf)
|
||||
|
||||
require.NoError(t, mw.WriteField("name", "john"))
|
||||
require.NoError(t, mw.WriteField("names", "john"))
|
||||
require.NoError(t, mw.WriteField("names", "doe"))
|
||||
require.NoError(t, mw.WriteField("age", "42"))
|
||||
require.NoError(t, mw.Close())
|
||||
|
||||
req.Header.SetContentType(mw.FormDataContentType())
|
||||
req.SetBody(buf.Bytes())
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
err := b.Bind(req, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
}
|
||||
|
||||
func Benchmark_FormBinder_BindMultipart(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &FormBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `form:"name"`
|
||||
Posts []string `form:"posts"`
|
||||
Age int `form:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
b.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
mw := multipart.NewWriter(buf)
|
||||
|
||||
require.NoError(b, mw.WriteField("name", "john"))
|
||||
require.NoError(b, mw.WriteField("age", "42"))
|
||||
require.NoError(b, mw.WriteField("posts", "post1"))
|
||||
require.NoError(b, mw.WriteField("posts", "post2"))
|
||||
require.NoError(b, mw.WriteField("posts", "post3"))
|
||||
require.NoError(b, mw.Close())
|
||||
|
||||
req.Header.SetContentType(mw.FormDataContentType())
|
||||
req.SetBody(buf.Bytes())
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(req, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
}
|
@ -8,22 +8,24 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// headerBinding is the header binder for header request body.
|
||||
type headerBinding struct{}
|
||||
// v is the header binder for header request body.
|
||||
type HeaderBinding struct {
|
||||
EnableSplitting bool
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*headerBinding) Name() string {
|
||||
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 {
|
||||
func (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error {
|
||||
data := make(map[string][]string)
|
||||
req.Header.VisitAll(func(key, val []byte) {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
@ -35,3 +37,8 @@ func (b *headerBinding) Bind(req *fasthttp.Request, out any) error {
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
// Reset resets the HeaderBinding binder.
|
||||
func (b *HeaderBinding) Reset() {
|
||||
b.EnableSplitting = false
|
||||
}
|
||||
|
88
binder/header_test.go
Normal file
88
binder/header_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_HeaderBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &HeaderBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "header", b.Name())
|
||||
|
||||
type User struct {
|
||||
Name string `header:"Name"`
|
||||
Names []string `header:"Names"`
|
||||
Posts []string `header:"Posts"`
|
||||
Age int `header:"Age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
req.Header.Set("name", "john")
|
||||
req.Header.Set("names", "john,doe")
|
||||
req.Header.Set("age", "42")
|
||||
req.Header.Set("posts", "post1,post2,post3")
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
err := b.Bind(req, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Len(t, user.Posts, 3)
|
||||
require.Equal(t, "post1", user.Posts[0])
|
||||
require.Equal(t, "post2", user.Posts[1])
|
||||
require.Equal(t, "post3", user.Posts[2])
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
|
||||
b.Reset()
|
||||
require.False(t, b.EnableSplitting)
|
||||
}
|
||||
|
||||
func Benchmark_HeaderBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &HeaderBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `header:"Name"`
|
||||
Posts []string `header:"Posts"`
|
||||
Age int `header:"Age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
b.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
req.Header.Set("name", "john")
|
||||
req.Header.Set("age", "42")
|
||||
req.Header.Set("posts", "post1,post2,post3")
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(req, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
require.Contains(b, user.Posts, "post1")
|
||||
require.Contains(b, user.Posts, "post2")
|
||||
require.Contains(b, user.Posts, "post3")
|
||||
}
|
@ -4,15 +4,22 @@ import (
|
||||
"github.com/gofiber/utils/v2"
|
||||
)
|
||||
|
||||
// jsonBinding is the JSON binder for JSON request body.
|
||||
type jsonBinding struct{}
|
||||
// JSONBinding is the JSON binder for JSON request body.
|
||||
type JSONBinding struct {
|
||||
JSONDecoder utils.JSONUnmarshal
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*jsonBinding) Name() string {
|
||||
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)
|
||||
func (b *JSONBinding) Bind(body []byte, out any) error {
|
||||
return b.JSONDecoder(body, out)
|
||||
}
|
||||
|
||||
// Reset resets the JSONBinding binder.
|
||||
func (b *JSONBinding) Reset() {
|
||||
b.JSONDecoder = nil
|
||||
}
|
||||
|
69
binder/json_test.go
Normal file
69
binder/json_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_JSON_Binding_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &JSONBinding{
|
||||
JSONDecoder: json.Unmarshal,
|
||||
}
|
||||
require.Equal(t, "json", b.Name())
|
||||
|
||||
type Post struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
Posts []Post `json:"posts"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
err := b.Bind([]byte(`{"name":"john","age":42,"posts":[{"title":"post1"},{"title":"post2"},{"title":"post3"}]}`), &user)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Len(t, user.Posts, 3)
|
||||
require.Equal(t, "post1", user.Posts[0].Title)
|
||||
require.Equal(t, "post2", user.Posts[1].Title)
|
||||
require.Equal(t, "post3", user.Posts[2].Title)
|
||||
|
||||
b.Reset()
|
||||
require.Nil(t, b.JSONDecoder)
|
||||
}
|
||||
|
||||
func Benchmark_JSON_Binding_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &JSONBinding{
|
||||
JSONDecoder: json.Unmarshal,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
Posts []string `json:"posts"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
var user User
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind([]byte(`{"name":"john","age":42,"posts":["post1","post2","post3"]}`), &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
require.Equal(b, "post1", user.Posts[0])
|
||||
require.Equal(b, "post2", user.Posts[1])
|
||||
require.Equal(b, "post3", user.Posts[2])
|
||||
}
|
@ -32,7 +32,7 @@ var (
|
||||
// decoderPoolMap helps to improve binders
|
||||
decoderPoolMap = map[string]*sync.Pool{}
|
||||
// tags is used to classify parser's pool
|
||||
tags = []string{HeaderBinder.Name(), RespHeaderBinder.Name(), CookieBinder.Name(), QueryBinder.Name(), FormBinder.Name(), URIBinder.Name()}
|
||||
tags = []string{"header", "respHeader", "cookie", "query", "form", "uri"}
|
||||
)
|
||||
|
||||
// SetParserDecoder allow globally change the option of form decoder, update decoderPool
|
||||
@ -107,8 +107,9 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
||||
func parseToMap(ptr any, data map[string][]string) error {
|
||||
elem := reflect.TypeOf(ptr).Elem()
|
||||
|
||||
// map[string][]string
|
||||
if elem.Kind() == reflect.Slice {
|
||||
//nolint:exhaustive // it's not necessary to check all types
|
||||
switch elem.Kind() {
|
||||
case reflect.Slice:
|
||||
newMap, ok := ptr.(map[string][]string)
|
||||
if !ok {
|
||||
return ErrMapNotConvertable
|
||||
@ -117,18 +118,20 @@ func parseToMap(ptr any, data map[string][]string) error {
|
||||
for k, v := range data {
|
||||
newMap[k] = v
|
||||
}
|
||||
case reflect.String, reflect.Interface:
|
||||
newMap, ok := ptr.(map[string]string)
|
||||
if !ok {
|
||||
return ErrMapNotConvertable
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
for k, v := range data {
|
||||
if len(v) == 0 {
|
||||
newMap[k] = ""
|
||||
continue
|
||||
}
|
||||
|
||||
// map[string]string
|
||||
newMap, ok := ptr.(map[string]string)
|
||||
if !ok {
|
||||
return ErrMapNotConvertable
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
newMap[k] = v[len(v)-1]
|
||||
newMap[k] = v[len(v)-1]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -223,7 +226,7 @@ func equalFieldType(out any, kind reflect.Kind, key string) bool {
|
||||
continue
|
||||
}
|
||||
// Get tag from field if exist
|
||||
inputFieldName := typeField.Tag.Get(QueryBinder.Name())
|
||||
inputFieldName := typeField.Tag.Get("query") // Name of query binder
|
||||
if inputFieldName == "" {
|
||||
inputFieldName = typeField.Name
|
||||
} else {
|
||||
|
@ -29,6 +29,21 @@ func Test_EqualFieldType(t *testing.T) {
|
||||
require.True(t, equalFieldType(&user, reflect.String, "Address"))
|
||||
require.True(t, equalFieldType(&user, reflect.Int, "AGE"))
|
||||
require.True(t, equalFieldType(&user, reflect.Int, "age"))
|
||||
|
||||
var user2 struct {
|
||||
User struct {
|
||||
Name string
|
||||
Address string `query:"address"`
|
||||
Age int `query:"AGE"`
|
||||
} `query:"user"`
|
||||
}
|
||||
|
||||
require.True(t, equalFieldType(&user2, reflect.String, "user.name"))
|
||||
require.True(t, equalFieldType(&user2, reflect.String, "user.Name"))
|
||||
require.True(t, equalFieldType(&user2, reflect.String, "user.address"))
|
||||
require.True(t, equalFieldType(&user2, reflect.String, "user.Address"))
|
||||
require.True(t, equalFieldType(&user2, reflect.Int, "user.AGE"))
|
||||
require.True(t, equalFieldType(&user2, reflect.Int, "user.age"))
|
||||
}
|
||||
|
||||
func Test_ParseParamSquareBrackets(t *testing.T) {
|
||||
@ -97,3 +112,68 @@ func Test_ParseParamSquareBrackets(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseToMap(t *testing.T) {
|
||||
inputMap := map[string][]string{
|
||||
"key1": {"value1", "value2"},
|
||||
"key2": {"value3"},
|
||||
"key3": {"value4"},
|
||||
}
|
||||
|
||||
// Test map[string]string
|
||||
m := make(map[string]string)
|
||||
err := parseToMap(m, inputMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "value2", m["key1"])
|
||||
require.Equal(t, "value3", m["key2"])
|
||||
require.Equal(t, "value4", m["key3"])
|
||||
|
||||
// Test map[string][]string
|
||||
m2 := make(map[string][]string)
|
||||
err = parseToMap(m2, inputMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, m2["key1"], 2)
|
||||
require.Contains(t, m2["key1"], "value1")
|
||||
require.Contains(t, m2["key1"], "value2")
|
||||
require.Len(t, m2["key2"], 1)
|
||||
require.Len(t, m2["key3"], 1)
|
||||
|
||||
// Test map[string]any
|
||||
m3 := make(map[string]any)
|
||||
err = parseToMap(m3, inputMap)
|
||||
require.ErrorIs(t, err, ErrMapNotConvertable)
|
||||
}
|
||||
|
||||
func Test_FilterFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "text/javascript; charset=utf-8",
|
||||
expected: "text/javascript",
|
||||
},
|
||||
{
|
||||
input: "text/javascript",
|
||||
expected: "text/javascript",
|
||||
},
|
||||
|
||||
{
|
||||
input: "text/javascript; charset=utf-8; foo=bar",
|
||||
expected: "text/javascript",
|
||||
},
|
||||
{
|
||||
input: "text/javascript charset=utf-8",
|
||||
expected: "text/javascript",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := FilterFlags(tt.input)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -8,20 +8,22 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// queryBinding is the query binder for query request body.
|
||||
type queryBinding struct{}
|
||||
// QueryBinding is the query binder for query request body.
|
||||
type QueryBinding struct {
|
||||
EnableSplitting bool
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*queryBinding) Name() string {
|
||||
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 {
|
||||
func (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.QueryArgs().VisitAll(func(key, val []byte) {
|
||||
reqCtx.URI().QueryArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -33,7 +35,7 @@ func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
@ -49,3 +51,8 @@ func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
// Reset resets the QueryBinding binder.
|
||||
func (b *QueryBinding) Reset() {
|
||||
b.EnableSplitting = false
|
||||
}
|
||||
|
87
binder/query_test.go
Normal file
87
binder/query_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_QueryBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &QueryBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "query", b.Name())
|
||||
|
||||
type Post struct {
|
||||
Title string `query:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `query:"name"`
|
||||
Names []string `query:"names"`
|
||||
Posts []Post `query:"posts"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
req.URI().SetQueryString("name=john&names=john,doe&age=42&posts[0][title]=post1&posts[1][title]=post2&posts[2][title]=post3")
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
err := b.Bind(req, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Len(t, user.Posts, 3)
|
||||
require.Equal(t, "post1", user.Posts[0].Title)
|
||||
require.Equal(t, "post2", user.Posts[1].Title)
|
||||
require.Equal(t, "post3", user.Posts[2].Title)
|
||||
require.Contains(t, user.Names, "john")
|
||||
require.Contains(t, user.Names, "doe")
|
||||
|
||||
b.Reset()
|
||||
require.False(t, b.EnableSplitting)
|
||||
}
|
||||
|
||||
func Benchmark_QueryBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &QueryBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `query:"name"`
|
||||
Posts []string `query:"posts"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
req := fasthttp.AcquireRequest()
|
||||
b.Cleanup(func() {
|
||||
fasthttp.ReleaseRequest(req)
|
||||
})
|
||||
|
||||
req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3")
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(req, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Len(b, user.Posts, 3)
|
||||
require.Contains(b, user.Posts, "post1")
|
||||
require.Contains(b, user.Posts, "post2")
|
||||
require.Contains(b, user.Posts, "post3")
|
||||
}
|
@ -8,22 +8,24 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// respHeaderBinding is the respHeader binder for response header.
|
||||
type respHeaderBinding struct{}
|
||||
// RespHeaderBinding is the respHeader binder for response header.
|
||||
type RespHeaderBinding struct {
|
||||
EnableSplitting bool
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*respHeaderBinding) Name() string {
|
||||
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 {
|
||||
func (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
|
||||
data := make(map[string][]string)
|
||||
resp.Header.VisitAll(func(key, val []byte) {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
if b.EnableSplitting && strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
@ -35,3 +37,8 @@ func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
// Reset resets the RespHeaderBinding binder.
|
||||
func (b *RespHeaderBinding) Reset() {
|
||||
b.EnableSplitting = false
|
||||
}
|
||||
|
79
binder/resp_header_test.go
Normal file
79
binder/resp_header_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_RespHeaderBinder_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &RespHeaderBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
require.Equal(t, "respHeader", b.Name())
|
||||
|
||||
type User struct {
|
||||
Name string `respHeader:"name"`
|
||||
Posts []string `respHeader:"posts"`
|
||||
Age int `respHeader:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
resp.Header.Set("name", "john")
|
||||
resp.Header.Set("age", "42")
|
||||
resp.Header.Set("posts", "post1,post2,post3")
|
||||
|
||||
t.Cleanup(func() {
|
||||
fasthttp.ReleaseResponse(resp)
|
||||
})
|
||||
|
||||
err := b.Bind(resp, &user)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Equal(t, []string{"post1", "post2", "post3"}, user.Posts)
|
||||
|
||||
b.Reset()
|
||||
require.False(t, b.EnableSplitting)
|
||||
}
|
||||
|
||||
func Benchmark_RespHeaderBinder_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
binder := &RespHeaderBinding{
|
||||
EnableSplitting: true,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `respHeader:"name"`
|
||||
Posts []string `respHeader:"posts"`
|
||||
Age int `respHeader:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
resp := fasthttp.AcquireResponse()
|
||||
resp.Header.Set("name", "john")
|
||||
resp.Header.Set("age", "42")
|
||||
resp.Header.Set("posts", "post1,post2,post3")
|
||||
|
||||
b.Cleanup(func() {
|
||||
fasthttp.ReleaseResponse(resp)
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(resp, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Equal(b, []string{"post1", "post2", "post3"}, user.Posts)
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package binder
|
||||
|
||||
// uriBinding is the URI binder for URI parameters.
|
||||
type uriBinding struct{}
|
||||
type URIBinding struct{}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*uriBinding) Name() string {
|
||||
func (*URIBinding) Name() string {
|
||||
return "uri"
|
||||
}
|
||||
|
||||
// Bind parses the URI parameters and returns the result.
|
||||
func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error {
|
||||
func (b *URIBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error {
|
||||
data := make(map[string][]string, len(params))
|
||||
for _, param := range params {
|
||||
data[param] = append(data[param], paramsFunc(param))
|
||||
@ -17,3 +17,8 @@ func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultVa
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
// Reset resets URIBinding binder.
|
||||
func (*URIBinding) Reset() {
|
||||
// Nothing to reset
|
||||
}
|
||||
|
77
binder/uri_test.go
Normal file
77
binder/uri_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_URIBinding_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &URIBinding{}
|
||||
require.Equal(t, "uri", b.Name())
|
||||
|
||||
type User struct {
|
||||
Name string `uri:"name"`
|
||||
Posts []string `uri:"posts"`
|
||||
Age int `uri:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
paramsKey := []string{"name", "age", "posts"}
|
||||
paramsVals := []string{"john", "42", "post1,post2,post3"}
|
||||
paramsFunc := func(key string, _ ...string) string {
|
||||
for i, k := range paramsKey {
|
||||
if k == key {
|
||||
return paramsVals[i]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
err := b.Bind(paramsKey, paramsFunc, &user)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Equal(t, []string{"post1,post2,post3"}, user.Posts)
|
||||
|
||||
b.Reset()
|
||||
}
|
||||
|
||||
func Benchmark_URIBinding_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
binder := &URIBinding{}
|
||||
|
||||
type User struct {
|
||||
Name string `uri:"name"`
|
||||
Posts []string `uri:"posts"`
|
||||
Age int `uri:"age"`
|
||||
}
|
||||
var user User
|
||||
|
||||
paramsKey := []string{"name", "age", "posts"}
|
||||
paramsVals := []string{"john", "42", "post1,post2,post3"}
|
||||
paramsFunc := func(key string, _ ...string) string {
|
||||
for i, k := range paramsKey {
|
||||
if k == key {
|
||||
return paramsVals[i]
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(paramsKey, paramsFunc, &user)
|
||||
}
|
||||
|
||||
require.NoError(b, err)
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
require.Equal(b, []string{"post1,post2,post3"}, user.Posts)
|
||||
}
|
@ -1,23 +1,31 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/utils/v2"
|
||||
)
|
||||
|
||||
// xmlBinding is the XML binder for XML request body.
|
||||
type xmlBinding struct{}
|
||||
// XMLBinding is the XML binder for XML request body.
|
||||
type XMLBinding struct {
|
||||
XMLDecoder utils.XMLUnmarshal
|
||||
}
|
||||
|
||||
// Name returns the binding name.
|
||||
func (*xmlBinding) Name() string {
|
||||
func (*XMLBinding) Name() string {
|
||||
return "xml"
|
||||
}
|
||||
|
||||
// Bind parses the request body as XML and returns the result.
|
||||
func (*xmlBinding) Bind(body []byte, out any) error {
|
||||
if err := xml.Unmarshal(body, out); err != nil {
|
||||
func (b *XMLBinding) Bind(body []byte, out any) error {
|
||||
if err := b.XMLDecoder(body, out); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal xml: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset resets the XMLBinding binder.
|
||||
func (b *XMLBinding) Reset() {
|
||||
b.XMLDecoder = nil
|
||||
}
|
||||
|
135
binder/xml_test.go
Normal file
135
binder/xml_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
package binder
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_XMLBinding_Bind(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &XMLBinding{
|
||||
XMLDecoder: xml.Unmarshal,
|
||||
}
|
||||
require.Equal(t, "xml", b.Name())
|
||||
|
||||
type Posts struct {
|
||||
XMLName xml.Name `xml:"post"`
|
||||
Title string `xml:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `xml:"name"`
|
||||
Ignore string `xml:"-"`
|
||||
Posts []Posts `xml:"posts>post"`
|
||||
Age int `xml:"age"`
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
err := b.Bind([]byte(`
|
||||
<user>
|
||||
<name>john</name>
|
||||
<age>42</age>
|
||||
<ignore>ignore</ignore>
|
||||
<posts>
|
||||
<post>
|
||||
<title>post1</title>
|
||||
</post>
|
||||
<post>
|
||||
<title>post2</title>
|
||||
</post>
|
||||
</posts>
|
||||
</user>
|
||||
`), user)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "john", user.Name)
|
||||
require.Equal(t, 42, user.Age)
|
||||
require.Empty(t, user.Ignore)
|
||||
|
||||
require.Len(t, user.Posts, 2)
|
||||
require.Equal(t, "post1", user.Posts[0].Title)
|
||||
require.Equal(t, "post2", user.Posts[1].Title)
|
||||
|
||||
b.Reset()
|
||||
require.Nil(t, b.XMLDecoder)
|
||||
}
|
||||
|
||||
func Test_XMLBinding_Bind_error(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := &XMLBinding{
|
||||
XMLDecoder: xml.Unmarshal,
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `xml:"name"`
|
||||
Age int `xml:"age"`
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
err := b.Bind([]byte(`
|
||||
<user>
|
||||
<name>john</name>
|
||||
<age>42</age>
|
||||
<unknown>unknown</unknown>
|
||||
</user
|
||||
`), user)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "failed to unmarshal xml")
|
||||
}
|
||||
|
||||
func Benchmark_XMLBinding_Bind(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
binder := &XMLBinding{
|
||||
XMLDecoder: xml.Unmarshal,
|
||||
}
|
||||
|
||||
type Posts struct {
|
||||
XMLName xml.Name `xml:"post"`
|
||||
Title string `xml:"title"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Name string `xml:"name"`
|
||||
Posts []Posts `xml:"posts>post"`
|
||||
Age int `xml:"age"`
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
data := []byte(`
|
||||
<user>
|
||||
<name>john</name>
|
||||
<age>42</age>
|
||||
<ignore>ignore</ignore>
|
||||
<posts>
|
||||
<post>
|
||||
<title>post1</title>
|
||||
</post>
|
||||
<post>
|
||||
<title>post2</title>
|
||||
</post>
|
||||
</posts>
|
||||
</user>
|
||||
`)
|
||||
|
||||
b.StartTimer()
|
||||
|
||||
var err error
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = binder.Bind(data, user)
|
||||
}
|
||||
require.NoError(b, err)
|
||||
|
||||
user = new(User)
|
||||
err = binder.Bind(data, user)
|
||||
require.NoError(b, err)
|
||||
|
||||
require.Equal(b, "john", user.Name)
|
||||
require.Equal(b, 42, user.Age)
|
||||
|
||||
require.Len(b, user.Posts, 2)
|
||||
require.Equal(b, "post1", user.Posts[0].Title)
|
||||
require.Equal(b, "post2", user.Posts[1].Title)
|
||||
}
|
@ -1433,7 +1433,9 @@ func Benchmark_Ctx_Fresh_LastModified(b *testing.B) {
|
||||
func Test_Ctx_Binders(t *testing.T) {
|
||||
t.Parallel()
|
||||
// setup
|
||||
app := New()
|
||||
app := New(Config{
|
||||
EnableSplittingOnParsers: true,
|
||||
})
|
||||
|
||||
type TestEmbeddedStruct struct {
|
||||
Names []string `query:"names"`
|
||||
|
@ -18,7 +18,6 @@ Make copies or use the [**`Immutable`**](./ctx.md) setting instead. [Read more..
|
||||
- [Body](#body)
|
||||
- [Form](#form)
|
||||
- [JSON](#json)
|
||||
- [MultipartForm](#multipartform)
|
||||
- [XML](#xml)
|
||||
- [CBOR](#cbor)
|
||||
- [Cookie](#cookie)
|
||||
@ -83,7 +82,7 @@ curl -X POST -F name=john -F pass=doe http://localhost:3000
|
||||
|
||||
### Form
|
||||
|
||||
Binds the request form body to a struct.
|
||||
Binds the request or multipart form body data to a struct.
|
||||
|
||||
It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a form body with a field called `Pass`, you would use a struct field with `form:"pass"`.
|
||||
|
||||
@ -111,12 +110,16 @@ app.Post("/", func(c fiber.Ctx) error {
|
||||
})
|
||||
```
|
||||
|
||||
Run tests with the following `curl` command:
|
||||
Run tests with the following `curl` commands for both `application/x-www-form-urlencoded` and `multipart/form-data`:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000
|
||||
```
|
||||
|
||||
### JSON
|
||||
|
||||
Binds the request JSON body to a struct.
|
||||
@ -153,43 +156,6 @@ Run tests with the following `curl` command:
|
||||
curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000
|
||||
```
|
||||
|
||||
### MultipartForm
|
||||
|
||||
Binds the request multipart form body to a struct.
|
||||
|
||||
It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a multipart form body with a field called `Pass`, you would use a struct field with `form:"pass"`.
|
||||
|
||||
```go title="Signature"
|
||||
func (b *Bind) MultipartForm(out any) error
|
||||
```
|
||||
|
||||
```go title="Example"
|
||||
// Field names should start with an uppercase letter
|
||||
type Person struct {
|
||||
Name string `form:"name"`
|
||||
Pass string `form:"pass"`
|
||||
}
|
||||
|
||||
app.Post("/", func(c fiber.Ctx) error {
|
||||
p := new(Person)
|
||||
|
||||
if err := c.Bind().MultipartForm(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println(p.Name) // john
|
||||
log.Println(p.Pass) // doe
|
||||
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
Run tests with the following `curl` command:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000
|
||||
```
|
||||
|
||||
### XML
|
||||
|
||||
Binds the request XML body to a struct.
|
||||
|
@ -83,6 +83,7 @@ app := fiber.New(fiber.Config{
|
||||
| <Reference id="writebuffersize">WriteBufferSize</Reference> | `int` | Per-connection buffer size for responses' writing. | `4096` |
|
||||
| <Reference id="writetimeout">WriteTimeout</Reference> | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` |
|
||||
| <Reference id="xmlencoder">XMLEncoder</Reference> | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` |
|
||||
| <Reference id="xmldecoder">XMLDecoder</Reference> | `utils.XMLUnmarshal` | Allowing for flexibility in using another XML library for decoding. | `xml.Unmarshal` |
|
||||
|
||||
## Server listening
|
||||
|
||||
|
@ -49,6 +49,7 @@ We have made several changes to the Fiber app, including:
|
||||
- `EnablePrintRoutes`
|
||||
- `ListenerNetwork` (previously `Network`)
|
||||
- **Trusted Proxy Configuration**: The `EnabledTrustedProxyCheck` has been moved to `app.Config.TrustProxy`, and `TrustedProxies` has been moved to `TrustProxyConfig.Proxies`.
|
||||
- **XMLDecoder Config Property**: The `XMLDecoder` property has been added to allow usage of 3rd-party XML libraries in XML binder.
|
||||
|
||||
### New Methods
|
||||
|
||||
|
@ -146,10 +146,8 @@ func (r *Redirect) WithInput() *Redirect {
|
||||
|
||||
oldInput := make(map[string]string)
|
||||
switch ctype {
|
||||
case MIMEApplicationForm:
|
||||
case MIMEApplicationForm, MIMEMultipartForm:
|
||||
_ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed
|
||||
case MIMEMultipartForm:
|
||||
_ = r.c.Bind().MultipartForm(oldInput) //nolint:errcheck // not needed
|
||||
default:
|
||||
_ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user