From 725855d025a294ad48c3d5f83cc8471cd1faa978 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 5 Dec 2024 10:57:15 +0300 Subject: [PATCH 01/12] :bug: bug: fix EnableSplittingOnParsers is not functional --- bind.go | 110 ++++++++++++++++++++++++++++++++++++++---- bind_test.go | 38 ++++++++++----- binder/binder.go | 79 +++++++++++++++++++++++++----- binder/cbor.go | 12 +++-- binder/cookie.go | 12 +++-- binder/form.go | 14 +++--- binder/header.go | 12 +++-- binder/json.go | 12 +++-- binder/mapping.go | 4 +- binder/query.go | 12 +++-- binder/resp_header.go | 12 +++-- binder/uri.go | 6 +-- binder/xml.go | 15 +++--- ctx_test.go | 106 ++++++++++++++++++++++++++++++++++++++++ 14 files changed, 362 insertions(+), 82 deletions(-) diff --git a/bind.go b/bind.go index 5af83743a0..47ef687cef 100644 --- a/bind.go +++ b/bind.go @@ -1,6 +1,8 @@ package fiber import ( + "encoding/xml" + "github.com/gofiber/fiber/v3/binder" "github.com/gofiber/utils/v2" ) @@ -77,7 +79,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.EnableSplitting = false + binder.PutToThePool(&binder.HeaderBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.Request(), out)); err != nil { return err } @@ -86,7 +97,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.EnableSplitting = false + binder.PutToThePool(&binder.RespHeaderBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.Response(), out)); err != nil { return err } @@ -96,7 +116,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.EnableSplitting = false + binder.PutToThePool(&binder.CookieBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -105,7 +134,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.EnableSplitting = false + binder.PutToThePool(&binder.QueryBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -114,7 +152,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.JSONDecoder = nil + binder.PutToThePool(&binder.JSONBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { return err } @@ -123,7 +170,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.CBORDecoder = nil + 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 +187,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 = xml.Unmarshal + + // Reset & put binder + defer func() { + bind.XMLDecoder = nil + binder.PutToThePool(&binder.XMLBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { return err } @@ -140,7 +205,16 @@ func (b *Bind) XML(out any) error { // Form binds the form into the struct, map[string]string and map[string][]string. 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.EnableSplitting = false + binder.PutToThePool(&binder.FormBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { return err } @@ -149,7 +223,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 { + bind := binder.GetFromThePool[*binder.URIBinding](&binder.URIBinderPool) + + // Reset & put binder + defer func() { + binder.PutToThePool(&binder.URIBinderPool, bind) + }() + + if err := b.returnErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil { return err } @@ -158,7 +239,16 @@ func (b *Bind) URI(out any) error { // 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 { + bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool) + bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers + + // Reset & put binder + defer func() { + bind.EnableSplitting = false + binder.PutToThePool(&binder.FormBinderPool, bind) + }() + + if err := b.returnErr(bind.BindMultipart(b.ctx.RequestCtx(), out)); err != nil { return err } diff --git a/bind_test.go b/bind_test.go index 55d2dd75e9..52c9004c61 100644 --- a/bind_test.go +++ b/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(``)) diff --git a/binder/binder.go b/binder/binder.go index bb3fc2b394..06c7c926a5 100644 --- a/binder/binder.go +++ b/binder/binder.go @@ -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) +} diff --git a/binder/cbor.go b/binder/cbor.go index 6f47893531..6207b9ec44 100644 --- a/binder/cbor.go +++ b/binder/cbor.go @@ -4,15 +4,17 @@ 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) } diff --git a/binder/cookie.go b/binder/cookie.go index 62271c8e38..1a9715cf93 100644 --- a/binder/cookie.go +++ b/binder/cookie.go @@ -8,16 +8,18 @@ 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(reqCtx *fasthttp.RequestCtx, out any) error { data := make(map[string][]string) var err error @@ -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]) diff --git a/binder/form.go b/binder/form.go index e0f1acd302..37c9a4e5b6 100644 --- a/binder/form.go +++ b/binder/form.go @@ -8,16 +8,18 @@ import ( "github.com/valyala/fasthttp" ) -// formBinding is the form binder for form request body. -type formBinding struct{} +// 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(reqCtx *fasthttp.RequestCtx, out any) error { data := make(map[string][]string) var err error @@ -33,7 +35,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]) @@ -51,7 +53,7 @@ func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { } // BindMultipart parses the request body and returns the result. -func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { +func (b *FormBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { data, err := reqCtx.MultipartForm() if err != nil { return err diff --git a/binder/header.go b/binder/header.go index 258a0b2229..42658fbab0 100644 --- a/binder/header.go +++ b/binder/header.go @@ -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]) diff --git a/binder/json.go b/binder/json.go index 7889aee8a2..5145422e4b 100644 --- a/binder/json.go +++ b/binder/json.go @@ -4,15 +4,17 @@ 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) } diff --git a/binder/mapping.go b/binder/mapping.go index 055345fe26..015c7fd6d2 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -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 @@ -223,7 +223,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 { diff --git a/binder/query.go b/binder/query.go index 8f029d30c4..f693076df5 100644 --- a/binder/query.go +++ b/binder/query.go @@ -8,16 +8,18 @@ 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.RequestCtx, out any) error { data := make(map[string][]string) var err error @@ -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]) diff --git a/binder/resp_header.go b/binder/resp_header.go index ef14255315..5dbc5ebdc4 100644 --- a/binder/resp_header.go +++ b/binder/resp_header.go @@ -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]) diff --git a/binder/uri.go b/binder/uri.go index b58d9d49c4..ae7b63fa90 100644 --- a/binder/uri.go +++ b/binder/uri.go @@ -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)) diff --git a/binder/xml.go b/binder/xml.go index 58da2b9b07..b024a19902 100644 --- a/binder/xml.go +++ b/binder/xml.go @@ -1,21 +1,24 @@ 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) } diff --git a/ctx_test.go b/ctx_test.go index 72c415a9ca..fb42865cfd 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1447,6 +1447,112 @@ func Test_Ctx_Binders(t *testing.T) { ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` } + withValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) { + t.Helper() + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + testStruct := new(TestStruct) + + require.NoError(t, actionFn(c, testStruct)) + require.Equal(t, "foo", testStruct.Name) + require.Equal(t, 111, testStruct.Class) + require.Equal(t, "bar", testStruct.NameWithDefault) + require.Equal(t, 222, testStruct.ClassWithDefault) + require.Equal(t, []string{"foo,bar,test"}, testStruct.TestEmbeddedStruct.Names) + } + + t.Run("Body:xml", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().SetBody([]byte(`foo111bar222foobartest`)) + return c.Bind().Body(testStruct) + }) + }) + t.Run("Body:form", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222&names=foo,bar,test`)) + return c.Bind().Body(testStruct) + }) + }) + t.Run("BodyParser:json", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222,"names":["foo","bar","test"]}`)) + return c.Bind().Body(testStruct) + }) + }) + t.Run("Body:multiform", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\ntest\r\n--b--") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) + return c.Bind().Body(testStruct) + }) + }) + t.Run("Cookie", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222;names=foo,bar,test") + return c.Bind().Cookie(testStruct) + }) + }) + t.Run("Query", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222&names=foo,bar,test") + return c.Bind().Query(testStruct) + }) + }) + t.Run("URI", func(t *testing.T) { + t.Skip("URI is not ready for v3") + //nolint:gocritic // TODO: uncomment + //t.Parallel() + //withValues(t, func(c Ctx, testStruct *TestStruct) error { + // c.Route().Params = []string{"name", "name2", "class", "class2"} + // c.Params().value = [30]string{"foo", "bar", "111", "222"} + // return c.Bind().URI(testStruct) + //}) + }) + t.Run("ReqHeader", func(t *testing.T) { + t.Parallel() + withValues(t, func(c Ctx, testStruct *TestStruct) error { + c.Request().Header.Add("name", "foo") + c.Request().Header.Add("name2", "bar") + c.Request().Header.Add("class", "111") + c.Request().Header.Add("class2", "222") + c.Request().Header.Add("names", "foo,bar,test") + return c.Bind().Header(testStruct) + }) + }) +} + +// go test -run Test_Ctx_Binders_Comma -v +func Test_Ctx_Binders_Comma(t *testing.T) { + t.Parallel() + // setup + app := New(Config{ + EnableSplittingOnParsers: true, + }) + + type TestEmbeddedStruct struct { + Names []string `query:"names"` + } + + type TestStruct struct { + Name string + NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" header:"Name2"` + TestEmbeddedStruct + Class int + ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` + } + withValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) { t.Helper() From 2260663df47a7265ffede2441936e1540d293d08 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 5 Dec 2024 11:15:56 +0300 Subject: [PATCH 02/12] remove wrong testcase --- ctx_test.go | 104 ---------------------------------------------------- 1 file changed, 104 deletions(-) diff --git a/ctx_test.go b/ctx_test.go index fb42865cfd..fe9a4a412d 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1431,110 +1431,6 @@ func Benchmark_Ctx_Fresh_LastModified(b *testing.B) { // go test -run Test_Ctx_Binders -v func Test_Ctx_Binders(t *testing.T) { - t.Parallel() - // setup - app := New() - - type TestEmbeddedStruct struct { - Names []string `query:"names"` - } - - type TestStruct struct { - Name string - NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" header:"Name2"` - TestEmbeddedStruct - Class int - ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` - } - - withValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) { - t.Helper() - - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - testStruct := new(TestStruct) - - require.NoError(t, actionFn(c, testStruct)) - require.Equal(t, "foo", testStruct.Name) - require.Equal(t, 111, testStruct.Class) - require.Equal(t, "bar", testStruct.NameWithDefault) - require.Equal(t, 222, testStruct.ClassWithDefault) - require.Equal(t, []string{"foo,bar,test"}, testStruct.TestEmbeddedStruct.Names) - } - - t.Run("Body:xml", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationXML) - c.Request().SetBody([]byte(`foo111bar222foobartest`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Body:form", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222&names=foo,bar,test`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("BodyParser:json", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222,"names":["foo","bar","test"]}`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Body:multiform", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\ntest\r\n--b--") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) - c.Request().Header.SetContentLength(len(body)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Cookie", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222;names=foo,bar,test") - return c.Bind().Cookie(testStruct) - }) - }) - t.Run("Query", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222&names=foo,bar,test") - return c.Bind().Query(testStruct) - }) - }) - t.Run("URI", func(t *testing.T) { - t.Skip("URI is not ready for v3") - //nolint:gocritic // TODO: uncomment - //t.Parallel() - //withValues(t, func(c Ctx, testStruct *TestStruct) error { - // c.Route().Params = []string{"name", "name2", "class", "class2"} - // c.Params().value = [30]string{"foo", "bar", "111", "222"} - // return c.Bind().URI(testStruct) - //}) - }) - t.Run("ReqHeader", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.Add("name", "foo") - c.Request().Header.Add("name2", "bar") - c.Request().Header.Add("class", "111") - c.Request().Header.Add("class2", "222") - c.Request().Header.Add("names", "foo,bar,test") - return c.Bind().Header(testStruct) - }) - }) -} - -// go test -run Test_Ctx_Binders_Comma -v -func Test_Ctx_Binders_Comma(t *testing.T) { t.Parallel() // setup app := New(Config{ From a56943d0c5402340b8b96aac14f5fa78b27a789a Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 5 Dec 2024 11:22:09 +0300 Subject: [PATCH 03/12] add support for external xml decoders --- app.go | 10 ++++++++++ bind.go | 4 +--- docs/api/fiber.md | 1 + docs/whats_new.md | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 7f9193a1a1..5e5475b5f1 100644 --- a/app.go +++ b/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 } diff --git a/bind.go b/bind.go index 47ef687cef..51bbebc9ea 100644 --- a/bind.go +++ b/bind.go @@ -1,8 +1,6 @@ package fiber import ( - "encoding/xml" - "github.com/gofiber/fiber/v3/binder" "github.com/gofiber/utils/v2" ) @@ -188,7 +186,7 @@ func (b *Bind) CBOR(out any) error { // XML binds the body string into the struct. func (b *Bind) XML(out any) error { bind := binder.GetFromThePool[*binder.XMLBinding](&binder.XMLBinderPool) - bind.XMLDecoder = xml.Unmarshal + bind.XMLDecoder = b.ctx.App().config.XMLDecoder // Reset & put binder defer func() { diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 2c50339d50..0f2a7aa840 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -83,6 +83,7 @@ app := fiber.New(fiber.Config{ | WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | | WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | | XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | +| XMLDecoder | `utils.XMLUnmarshal` | Allowing for flexibility in using another XML library for decoding. | `xml.Unmarshal` | ## Server listening diff --git a/docs/whats_new.md b/docs/whats_new.md index 3221f5dd5e..5e8966e468 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -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 From 8ce1fb4078c07e0c6addaaa4110b99a8cf37ace8 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 20 Dec 2024 22:19:07 +0300 Subject: [PATCH 04/12] improve test coverage --- bind.go | 8 +- binder/binder_test.go | 28 ++++++ binder/cbor_test.go | 90 ++++++++++++++++++ binder/cookie.go | 4 +- binder/cookie_test.go | 84 +++++++++++++++++ binder/form.go | 8 +- binder/form_test.go | 182 +++++++++++++++++++++++++++++++++++++ binder/header_test.go | 74 +++++++++++++++ binder/json_test.go | 68 ++++++++++++++ binder/mapping.go | 29 +++--- binder/mapping_test.go | 82 +++++++++++++++++ binder/query.go | 4 +- binder/query_test.go | 73 +++++++++++++++ binder/resp_header_test.go | 76 ++++++++++++++++ binder/uri_test.go | 75 +++++++++++++++ binder/xml_test.go | 134 +++++++++++++++++++++++++++ 16 files changed, 995 insertions(+), 24 deletions(-) create mode 100644 binder/binder_test.go create mode 100644 binder/cbor_test.go create mode 100644 binder/cookie_test.go create mode 100644 binder/form_test.go create mode 100644 binder/header_test.go create mode 100644 binder/json_test.go create mode 100644 binder/query_test.go create mode 100644 binder/resp_header_test.go create mode 100644 binder/uri_test.go create mode 100644 binder/xml_test.go diff --git a/bind.go b/bind.go index 51bbebc9ea..7a6454ee31 100644 --- a/bind.go +++ b/bind.go @@ -123,7 +123,7 @@ func (b *Bind) Cookie(out any) error { binder.PutToThePool(&binder.CookieBinderPool, bind) }() - if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { + if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { return err } @@ -141,7 +141,7 @@ func (b *Bind) Query(out any) error { binder.PutToThePool(&binder.QueryBinderPool, bind) }() - if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { + if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { return err } @@ -212,7 +212,7 @@ func (b *Bind) Form(out any) error { binder.PutToThePool(&binder.FormBinderPool, bind) }() - if err := b.returnErr(bind.Bind(b.ctx.RequestCtx(), out)); err != nil { + if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { return err } @@ -246,7 +246,7 @@ func (b *Bind) MultipartForm(out any) error { binder.PutToThePool(&binder.FormBinderPool, bind) }() - if err := b.returnErr(bind.BindMultipart(b.ctx.RequestCtx(), out)); err != nil { + if err := b.returnErr(bind.BindMultipart(&b.ctx.RequestCtx().Request, out)); err != nil { return err } diff --git a/binder/binder_test.go b/binder/binder_test.go new file mode 100644 index 0000000000..d078ed02c6 --- /dev/null +++ b/binder/binder_test.go @@ -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) +} diff --git a/binder/cbor_test.go b/binder/cbor_test.go new file mode 100644 index 0000000000..74a94500f7 --- /dev/null +++ b/binder/cbor_test.go @@ -0,0 +1,90 @@ +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"` + Names []string `cbor:"names"` + Age int `cbor:"age"` + + Posts []Post `cbor:"posts"` + } + 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") +} + +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) +} diff --git a/binder/cookie.go b/binder/cookie.go index 1a9715cf93..00e228ac0d 100644 --- a/binder/cookie.go +++ b/binder/cookie.go @@ -19,11 +19,11 @@ func (*CookieBinding) Name() string { } // 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 } diff --git a/binder/cookie_test.go b/binder/cookie_test.go new file mode 100644 index 0000000000..657df24498 --- /dev/null +++ b/binder/cookie_test.go @@ -0,0 +1,84 @@ +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"` + Age int `form:"age"` + + Posts []Post `form:"posts"` + } + 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") +} + +func Benchmark_CookieBinder_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &CookieBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + + Posts []string `query:"posts"` + } + var user User + + req := fasthttp.AcquireRequest() + + req.Header.SetCookie("name", "john") + req.Header.SetCookie("age", "42") + req.Header.SetCookie("posts", "post1,post2,post3") + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _ = binder.Bind(req, &user) + } + + 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") +} diff --git a/binder/form.go b/binder/form.go index 37c9a4e5b6..ea7ec0a40e 100644 --- a/binder/form.go +++ b/binder/form.go @@ -19,11 +19,11 @@ func (*FormBinding) Name() string { } // 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) { + req.PostArgs().VisitAll(func(key, val []byte) { if err != nil { return } @@ -53,8 +53,8 @@ func (b *FormBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { } // BindMultipart parses the request body and returns the result. -func (b *FormBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { - data, err := reqCtx.MultipartForm() +func (b *FormBinding) BindMultipart(req *fasthttp.Request, out any) error { + data, err := req.MultipartForm() if err != nil { return err } diff --git a/binder/form_test.go b/binder/form_test.go new file mode 100644 index 0000000000..b6715a435c --- /dev/null +++ b/binder/form_test.go @@ -0,0 +1,182 @@ +package binder + +import ( + "bytes" + "fmt" + "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"` + Age int `form:"age"` + + Posts []Post `form:"posts"` + } + 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") +} + +func Benchmark_FormBinder_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &QueryBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + + Posts []string `query:"posts"` + } + 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() + + for i := 0; i < b.N; i++ { + _ = binder.Bind(req, &user) + } + + 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 Post struct { + Title string `form:"title"` + } + + type User struct { + Name string `form:"name"` + Names []string `form:"names"` + Age int `form:"age"` + + Posts []Post `form:"posts"` + } + 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.WriteField("posts[0][title]", "post1")) + // require.NoError(t, mw.WriteField("posts[1][title]", "post2")) + // require.NoError(t, mw.WriteField("posts[2][title]", "post3")) + require.NoError(t, mw.Close()) + + req.Header.SetContentType(mw.FormDataContentType()) + req.SetBody(buf.Bytes()) + + t.Cleanup(func() { + fasthttp.ReleaseRequest(req) + }) + + err := b.BindMultipart(req, &user) + fmt.Print(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") +} + +func Benchmark_FormBinder_BindMultipart(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &FormBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + + Posts []string `query:"posts"` + } + var user User + + req := fasthttp.AcquireRequest() + + 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() + + for i := 0; i < b.N; i++ { + _ = binder.BindMultipart(req, &user) + } + + require.Equal(b, "john", user.Name) + require.Equal(b, 42, user.Age) + require.Len(b, user.Posts, 3) +} diff --git a/binder/header_test.go b/binder/header_test.go new file mode 100644 index 0000000000..195fe6bbf9 --- /dev/null +++ b/binder/header_test.go @@ -0,0 +1,74 @@ +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"` + Age int `header:"age"` + + Posts []string `header:"posts"` + } + 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") +} + +func Benchmark_HeaderBinder_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &HeaderBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + + Posts []string `query:"posts"` + } + var user User + + req := fasthttp.AcquireRequest() + req.Header.Set("name", "john") + req.Header.Set("age", "42") + req.Header.Set("posts", "post1,post2,post3") + + for i := 0; i < b.N; i++ { + _ = binder.Bind(req, &user) + } +} diff --git a/binder/json_test.go b/binder/json_test.go new file mode 100644 index 0000000000..8c3618ef89 --- /dev/null +++ b/binder/json_test.go @@ -0,0 +1,68 @@ +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"` + Age int `json:"age"` + + Posts []Post `json:"posts"` + } + 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) +} + +func Benchmark_JSON_Binding_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &JSONBinding{ + JSONDecoder: json.Unmarshal, + } + + type User struct { + Name string `json:"name"` + Age int `json:"age"` + + Posts []string `json:"posts"` + } + + 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]) +} diff --git a/binder/mapping.go b/binder/mapping.go index 015c7fd6d2..d9623f8953 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -105,10 +105,11 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error { // Parse data into the map // thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go func parseToMap(ptr any, data map[string][]string) error { + fmt.Print(ptr) elem := reflect.TypeOf(ptr).Elem() - - // map[string][]string - if elem.Kind() == reflect.Slice { + fmt.Print(elem.Kind()) + switch elem.Kind() { + case reflect.Slice: newMap, ok := ptr.(map[string][]string) if !ok { return ErrMapNotConvertable @@ -117,18 +118,22 @@ 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 - } + fmt.Print(newMap) - // map[string]string - newMap, ok := ptr.(map[string]string) - if !ok { - return ErrMapNotConvertable - } + for k, v := range data { + if len(v) == 0 { + newMap[k] = "" + continue + } - for k, v := range data { - newMap[k] = v[len(v)-1] + newMap[k] = v[len(v)-1] + } } return nil diff --git a/binder/mapping_test.go b/binder/mapping_test.go index e6fc8146f7..4abddc8932 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -29,6 +29,22 @@ 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 +113,69 @@ 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]interface{} + m3 := make(map[string]interface{}) + 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) + }) + } +} diff --git a/binder/query.go b/binder/query.go index f693076df5..0811b3d8d2 100644 --- a/binder/query.go +++ b/binder/query.go @@ -19,11 +19,11 @@ func (*QueryBinding) Name() string { } // 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 } diff --git a/binder/query_test.go b/binder/query_test.go new file mode 100644 index 0000000000..e9183a40c0 --- /dev/null +++ b/binder/query_test.go @@ -0,0 +1,73 @@ +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"` + Age int `query:"age"` + + Posts []Post `query:"posts"` + } + 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") +} + +func Benchmark_QueryBinder_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &QueryBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + + Posts []string `query:"posts"` + } + var user User + + req := fasthttp.AcquireRequest() + req.URI().SetQueryString("name=john&age=42&posts=post1,post2,post3") + + for i := 0; i < b.N; i++ { + _ = binder.Bind(req, &user) + } +} diff --git a/binder/resp_header_test.go b/binder/resp_header_test.go new file mode 100644 index 0000000000..2805a83caf --- /dev/null +++ b/binder/resp_header_test.go @@ -0,0 +1,76 @@ +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"` + Age int `respHeader:"age"` + + Posts []string `respHeader:"posts"` + } + 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) +} + +func Benchmark_RespHeaderBinder_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &RespHeaderBinding{ + EnableSplitting: true, + } + + type User struct { + Name string `respHeader:"name"` + Age int `respHeader:"age"` + + Posts []string `respHeader:"posts"` + } + 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.StartTimer() + for i := 0; i < b.N; i++ { + _ = binder.Bind(resp, &user) + } + + require.Equal(b, "john", user.Name) + require.Equal(b, 42, user.Age) + require.Equal(b, []string{"post1", "post2", "post3"}, user.Posts) +} diff --git a/binder/uri_test.go b/binder/uri_test.go new file mode 100644 index 0000000000..e5308d5ce7 --- /dev/null +++ b/binder/uri_test.go @@ -0,0 +1,75 @@ +package binder + +import ( + "fmt" + "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"` + Age int `uri:"age"` + + Posts []string `uri:"posts"` + } + var user User + + paramsKey := []string{"name", "age", "posts"} + paramsVals := []string{"john", "42", "post1,post2,post3"} + paramsFunc := func(key string, defaultValue ...string) string { + for i, k := range paramsKey { + if k == key { + return paramsVals[i] + } + } + + return "" + } + + err := b.Bind(paramsKey, paramsFunc, &user) + + fmt.Println(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) +} + +func Benchmark_URIBinding_Bind(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + binder := &URIBinding{} + + type User struct { + Name string `uri:"name"` + Age int `uri:"age"` + + Posts []string `uri:"posts"` + } + var user User + + paramsKey := []string{"name", "age", "posts"} + paramsVals := []string{"john", "42", "post1,post2,post3"} + paramsFunc := func(key string, defaultValue ...string) string { + for i, k := range paramsKey { + if k == key { + return paramsVals[i] + } + } + + return "" + } + + for i := 0; i < b.N; i++ { + _ = binder.Bind(paramsKey, paramsFunc, &user) + } +} diff --git a/binder/xml_test.go b/binder/xml_test.go new file mode 100644 index 0000000000..33ba65a67e --- /dev/null +++ b/binder/xml_test.go @@ -0,0 +1,134 @@ +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"` + Age int `xml:"age"` + + Posts []Posts `xml:"posts>post"` + + // This field should be ignored + Ignore string `xml:"-"` + } + + user := new(User) + err := b.Bind([]byte(` + + john + 42 + ignore + + + post1 + + + post2 + + + + `), 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) +} + +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(` + + john + 42 + unknown + post"` + } + + user := new(User) + data := []byte(` + + john + 42 + ignore + + + post1 + + + post2 + + + + `) + + b.StartTimer() + + for i := 0; i < b.N; i++ { + _ = binder.Bind(data, user) + } + + 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) +} From 043dd20b61e6d62fdb5e8580d5e4803f42f998f1 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 20 Dec 2024 22:55:10 +0300 Subject: [PATCH 05/12] fix linter --- binder/cbor_test.go | 3 +-- binder/cookie_test.go | 12 ++++++------ binder/form_test.go | 36 +++++++++++------------------------- binder/header_test.go | 27 +++++++++++++++++---------- binder/json_test.go | 10 ++++------ binder/mapping.go | 6 ++---- binder/mapping_test.go | 6 ++---- binder/query_test.go | 19 +++++++++++++------ binder/resp_header_test.go | 18 +++++++++--------- binder/uri_test.go | 26 +++++++++++++------------- binder/xml_test.go | 22 ++++++++++------------ 11 files changed, 88 insertions(+), 97 deletions(-) diff --git a/binder/cbor_test.go b/binder/cbor_test.go index 74a94500f7..bf84699870 100644 --- a/binder/cbor_test.go +++ b/binder/cbor_test.go @@ -21,10 +21,9 @@ func Test_CBORBinder_Bind(t *testing.T) { type User struct { Name string `cbor:"name"` + Posts []Post `cbor:"posts"` Names []string `cbor:"names"` Age int `cbor:"age"` - - Posts []Post `cbor:"posts"` } var user User diff --git a/binder/cookie_test.go b/binder/cookie_test.go index 657df24498..c26395b40a 100644 --- a/binder/cookie_test.go +++ b/binder/cookie_test.go @@ -22,9 +22,8 @@ func Test_CookieBinder_Bind(t *testing.T) { type User struct { Name string `form:"name"` Names []string `form:"names"` + Posts []Post `form:"posts"` Age int `form:"age"` - - Posts []Post `form:"posts"` } var user User @@ -56,10 +55,9 @@ func Benchmark_CookieBinder_Bind(b *testing.B) { } type User struct { - Name string `query:"name"` - Age int `query:"age"` - + Name string `query:"name"` Posts []string `query:"posts"` + Age int `query:"age"` } var user User @@ -71,10 +69,12 @@ func Benchmark_CookieBinder_Bind(b *testing.B) { b.ResetTimer() + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(req, &user) + 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) diff --git a/binder/form_test.go b/binder/form_test.go index b6715a435c..c8b16ef44e 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -2,7 +2,6 @@ package binder import ( "bytes" - "fmt" "mime/multipart" "testing" @@ -25,9 +24,8 @@ func Test_FormBinder_Bind(t *testing.T) { type User struct { Name string `form:"name"` Names []string `form:"names"` + Posts []Post `form:"posts"` Age int `form:"age"` - - Posts []Post `form:"posts"` } var user User @@ -61,10 +59,9 @@ func Benchmark_FormBinder_Bind(b *testing.B) { } type User struct { - Name string `query:"name"` - Age int `query:"age"` - + Name string `query:"name"` Posts []string `query:"posts"` + Age int `query:"age"` } var user User @@ -74,10 +71,12 @@ func Benchmark_FormBinder_Bind(b *testing.B) { b.ResetTimer() + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(req, &user) + 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) @@ -91,16 +90,10 @@ func Test_FormBinder_BindMultipart(t *testing.T) { } require.Equal(t, "form", b.Name()) - type Post struct { - Title string `form:"title"` - } - type User struct { Name string `form:"name"` Names []string `form:"names"` Age int `form:"age"` - - Posts []Post `form:"posts"` } var user User @@ -113,9 +106,6 @@ func Test_FormBinder_BindMultipart(t *testing.T) { 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.WriteField("posts[0][title]", "post1")) - // require.NoError(t, mw.WriteField("posts[1][title]", "post2")) - // require.NoError(t, mw.WriteField("posts[2][title]", "post3")) require.NoError(t, mw.Close()) req.Header.SetContentType(mw.FormDataContentType()) @@ -126,15 +116,10 @@ func Test_FormBinder_BindMultipart(t *testing.T) { }) err := b.BindMultipart(req, &user) - fmt.Print(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") } @@ -148,10 +133,9 @@ func Benchmark_FormBinder_BindMultipart(b *testing.B) { } type User struct { - Name string `query:"name"` - Age int `query:"age"` - + Name string `query:"name"` Posts []string `query:"posts"` + Age int `query:"age"` } var user User @@ -172,10 +156,12 @@ func Benchmark_FormBinder_BindMultipart(b *testing.B) { b.ResetTimer() + var err error for i := 0; i < b.N; i++ { - _ = binder.BindMultipart(req, &user) + err = binder.BindMultipart(req, &user) } + require.NoError(b, err) require.Equal(b, "john", user.Name) require.Equal(b, 42, user.Age) require.Len(b, user.Posts, 3) diff --git a/binder/header_test.go b/binder/header_test.go index 195fe6bbf9..3bf2632f43 100644 --- a/binder/header_test.go +++ b/binder/header_test.go @@ -16,11 +16,10 @@ func Test_HeaderBinder_Bind(t *testing.T) { require.Equal(t, "header", b.Name()) type User struct { - Name string `header:"name"` - Names []string `header:"names"` - Age int `header:"age"` - - Posts []string `header:"posts"` + Name string `header:"Name"` + Names []string `header:"Names"` + Posts []string `header:"Posts"` + Age int `header:"Age"` } var user User @@ -56,10 +55,9 @@ func Benchmark_HeaderBinder_Bind(b *testing.B) { } type User struct { - Name string `query:"name"` - Age int `query:"age"` - - Posts []string `query:"posts"` + Name string `header:"Name"` + Posts []string `header:"Posts"` + Age int `header:"Age"` } var user User @@ -68,7 +66,16 @@ func Benchmark_HeaderBinder_Bind(b *testing.B) { req.Header.Set("age", "42") req.Header.Set("posts", "post1,post2,post3") + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(req, &user) + 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") } diff --git a/binder/json_test.go b/binder/json_test.go index 8c3618ef89..2764c8f5fb 100644 --- a/binder/json_test.go +++ b/binder/json_test.go @@ -20,10 +20,9 @@ func Test_JSON_Binding_Bind(t *testing.T) { } type User struct { - Name string `json:"name"` - Age int `json:"age"` - + Name string `json:"name"` Posts []Post `json:"posts"` + Age int `json:"age"` } var user User @@ -46,10 +45,9 @@ func Benchmark_JSON_Binding_Bind(b *testing.B) { } type User struct { - Name string `json:"name"` - Age int `json:"age"` - + Name string `json:"name"` Posts []string `json:"posts"` + Age int `json:"age"` } var user User diff --git a/binder/mapping.go b/binder/mapping.go index d9623f8953..d8b692f7e4 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -105,9 +105,9 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error { // Parse data into the map // thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go func parseToMap(ptr any, data map[string][]string) error { - fmt.Print(ptr) elem := reflect.TypeOf(ptr).Elem() - fmt.Print(elem.Kind()) + + //nolint:exhaustive // it's not necessary to check all types switch elem.Kind() { case reflect.Slice: newMap, ok := ptr.(map[string][]string) @@ -124,8 +124,6 @@ func parseToMap(ptr any, data map[string][]string) error { return ErrMapNotConvertable } - fmt.Print(newMap) - for k, v := range data { if len(v) == 0 { newMap[k] = "" diff --git a/binder/mapping_test.go b/binder/mapping_test.go index 4abddc8932..75cdc78305 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -44,7 +44,6 @@ func Test_EqualFieldType(t *testing.T) { 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) { @@ -141,11 +140,10 @@ func Test_parseToMap(t *testing.T) { require.Len(t, m2["key2"], 1) require.Len(t, m2["key3"], 1) - // Test map[string]interface{} - m3 := make(map[string]interface{}) + // Test map[string]any + m3 := make(map[string]any) err = parseToMap(m3, inputMap) require.ErrorIs(t, err, ErrMapNotConvertable) - } func Test_FilterFlags(t *testing.T) { diff --git a/binder/query_test.go b/binder/query_test.go index e9183a40c0..60d821f240 100644 --- a/binder/query_test.go +++ b/binder/query_test.go @@ -22,9 +22,8 @@ func Test_QueryBinder_Bind(t *testing.T) { type User struct { Name string `query:"name"` Names []string `query:"names"` + Posts []Post `query:"posts"` Age int `query:"age"` - - Posts []Post `query:"posts"` } var user User @@ -57,17 +56,25 @@ func Benchmark_QueryBinder_Bind(b *testing.B) { } type User struct { - Name string `query:"name"` - Age int `query:"age"` - + 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") + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(req, &user) + 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") } diff --git a/binder/resp_header_test.go b/binder/resp_header_test.go index 2805a83caf..771b2c0d04 100644 --- a/binder/resp_header_test.go +++ b/binder/resp_header_test.go @@ -16,10 +16,9 @@ func Test_RespHeaderBinder_Bind(t *testing.T) { require.Equal(t, "respHeader", b.Name()) type User struct { - Name string `respHeader:"name"` - Age int `respHeader:"age"` - + Name string `respHeader:"name"` Posts []string `respHeader:"posts"` + Age int `respHeader:"age"` } var user User @@ -42,17 +41,15 @@ func Test_RespHeaderBinder_Bind(t *testing.T) { func Benchmark_RespHeaderBinder_Bind(b *testing.B) { b.ReportAllocs() - b.ResetTimer() binder := &RespHeaderBinding{ EnableSplitting: true, } type User struct { - Name string `respHeader:"name"` - Age int `respHeader:"age"` - + Name string `respHeader:"name"` Posts []string `respHeader:"posts"` + Age int `respHeader:"age"` } var user User @@ -65,11 +62,14 @@ func Benchmark_RespHeaderBinder_Bind(b *testing.B) { fasthttp.ReleaseResponse(resp) }) - b.StartTimer() + b.ResetTimer() + + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(resp, &user) + 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) diff --git a/binder/uri_test.go b/binder/uri_test.go index e5308d5ce7..64b986aa3e 100644 --- a/binder/uri_test.go +++ b/binder/uri_test.go @@ -1,7 +1,6 @@ package binder import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -14,16 +13,15 @@ func Test_URIBinding_Bind(t *testing.T) { require.Equal(t, "uri", b.Name()) type User struct { - Name string `uri:"name"` - Age int `uri:"age"` - + 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, defaultValue ...string) string { + paramsFunc := func(key string, _ ...string) string { for i, k := range paramsKey { if k == key { return paramsVals[i] @@ -34,9 +32,6 @@ func Test_URIBinding_Bind(t *testing.T) { } err := b.Bind(paramsKey, paramsFunc, &user) - - fmt.Println(user) - require.NoError(t, err) require.Equal(t, "john", user.Name) require.Equal(t, 42, user.Age) @@ -50,16 +45,15 @@ func Benchmark_URIBinding_Bind(b *testing.B) { binder := &URIBinding{} type User struct { - Name string `uri:"name"` - Age int `uri:"age"` - + 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, defaultValue ...string) string { + paramsFunc := func(key string, _ ...string) string { for i, k := range paramsKey { if k == key { return paramsVals[i] @@ -69,7 +63,13 @@ func Benchmark_URIBinding_Bind(b *testing.B) { return "" } + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(paramsKey, paramsFunc, &user) + 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) } diff --git a/binder/xml_test.go b/binder/xml_test.go index 33ba65a67e..df274ee5e7 100644 --- a/binder/xml_test.go +++ b/binder/xml_test.go @@ -21,13 +21,10 @@ func Test_XMLBinding_Bind(t *testing.T) { } type User struct { - Name string `xml:"name"` - Age int `xml:"age"` - - Posts []Posts `xml:"posts>post"` - - // This field should be ignored - Ignore string `xml:"-"` + Name string `xml:"name"` + Ignore string `xml:"-"` + Posts []Posts `xml:"posts>post"` + Age int `xml:"age"` } user := new(User) @@ -92,10 +89,9 @@ func Benchmark_XMLBinding_Bind(b *testing.B) { } type User struct { - Name string `xml:"name"` - Age int `xml:"age"` - + Name string `xml:"name"` Posts []Posts `xml:"posts>post"` + Age int `xml:"age"` } user := new(User) @@ -117,12 +113,14 @@ func Benchmark_XMLBinding_Bind(b *testing.B) { b.StartTimer() + var err error for i := 0; i < b.N; i++ { - _ = binder.Bind(data, user) + err = binder.Bind(data, user) } + require.NoError(b, err) user = new(User) - err := binder.Bind(data, user) + err = binder.Bind(data, user) require.NoError(b, err) require.Equal(b, "john", user.Name) From d2d2ab777eef2671de09db667dba76b34f284343 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 20 Dec 2024 23:11:59 +0300 Subject: [PATCH 06/12] update --- binder/cookie_test.go | 3 +++ binder/form_test.go | 9 ++++++--- binder/header_test.go | 4 ++++ binder/query_test.go | 4 ++++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/binder/cookie_test.go b/binder/cookie_test.go index c26395b40a..5f6d84e724 100644 --- a/binder/cookie_test.go +++ b/binder/cookie_test.go @@ -62,6 +62,9 @@ func Benchmark_CookieBinder_Bind(b *testing.B) { var user User req := fasthttp.AcquireRequest() + b.Cleanup(func() { + fasthttp.ReleaseRequest(req) + }) req.Header.SetCookie("name", "john") req.Header.SetCookie("age", "42") diff --git a/binder/form_test.go b/binder/form_test.go index c8b16ef44e..a146b74dcf 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -133,13 +133,16 @@ func Benchmark_FormBinder_BindMultipart(b *testing.B) { } type User struct { - Name string `query:"name"` - Posts []string `query:"posts"` - Age int `query:"age"` + 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) diff --git a/binder/header_test.go b/binder/header_test.go index 3bf2632f43..d1b96a098a 100644 --- a/binder/header_test.go +++ b/binder/header_test.go @@ -62,6 +62,10 @@ func Benchmark_HeaderBinder_Bind(b *testing.B) { 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") diff --git a/binder/query_test.go b/binder/query_test.go index 60d821f240..aa4c687a6a 100644 --- a/binder/query_test.go +++ b/binder/query_test.go @@ -63,6 +63,10 @@ func Benchmark_QueryBinder_Bind(b *testing.B) { 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 From e2e93a41005b40e845db0f4b1d350a1f93c1b101 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Mon, 23 Dec 2024 10:33:44 +0300 Subject: [PATCH 07/12] add reset methods --- bind.go | 29 ++++++++++------------------- binder/cbor.go | 6 ++++++ binder/cookie.go | 6 ++++++ binder/form.go | 6 ++++++ binder/header.go | 6 ++++++ binder/json.go | 6 ++++++ binder/query.go | 6 ++++++ binder/resp_header.go | 6 ++++++ binder/uri.go | 5 +++++ binder/xml.go | 6 ++++++ 10 files changed, 63 insertions(+), 19 deletions(-) diff --git a/bind.go b/bind.go index 7a6454ee31..d3083cde02 100644 --- a/bind.go +++ b/bind.go @@ -82,8 +82,7 @@ func (b *Bind) Header(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.HeaderBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Request(), out)); err != nil { @@ -100,8 +99,7 @@ func (b *Bind) RespHeader(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.RespHeaderBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Response(), out)); err != nil { @@ -119,8 +117,7 @@ func (b *Bind) Cookie(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.CookieBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { @@ -137,8 +134,7 @@ func (b *Bind) Query(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.QueryBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { @@ -155,8 +151,7 @@ func (b *Bind) JSON(out any) error { // Reset & put binder defer func() { - bind.JSONDecoder = nil - binder.PutToThePool(&binder.JSONBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -173,8 +168,7 @@ func (b *Bind) CBOR(out any) error { // Reset & put binder defer func() { - bind.CBORDecoder = nil - binder.PutToThePool(&binder.CBORBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -190,8 +184,7 @@ func (b *Bind) XML(out any) error { // Reset & put binder defer func() { - bind.XMLDecoder = nil - binder.PutToThePool(&binder.XMLBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -208,8 +201,7 @@ func (b *Bind) Form(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.FormBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(&b.ctx.RequestCtx().Request, out)); err != nil { @@ -225,7 +217,7 @@ func (b *Bind) URI(out any) error { // Reset & put binder defer func() { - binder.PutToThePool(&binder.URIBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil { @@ -242,8 +234,7 @@ func (b *Bind) MultipartForm(out any) error { // Reset & put binder defer func() { - bind.EnableSplitting = false - binder.PutToThePool(&binder.FormBinderPool, bind) + bind.Reset() }() if err := b.returnErr(bind.BindMultipart(&b.ctx.RequestCtx().Request, out)); err != nil { diff --git a/binder/cbor.go b/binder/cbor.go index 6207b9ec44..44c9a08e37 100644 --- a/binder/cbor.go +++ b/binder/cbor.go @@ -18,3 +18,9 @@ func (*CBORBinding) Name() string { func (b *CBORBinding) Bind(body []byte, out any) error { return b.CBORDecoder(body, out) } + +// Reset resets the CBORBinding binder and puts it back to the binder pool. +func (b *CBORBinding) Reset() { + b.CBORDecoder = nil + PutToThePool(&CBORBinderPool, b) +} diff --git a/binder/cookie.go b/binder/cookie.go index 00e228ac0d..9e5b2a53f8 100644 --- a/binder/cookie.go +++ b/binder/cookie.go @@ -47,3 +47,9 @@ func (b *CookieBinding) Bind(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } + +// Reset resets the CookieBinding binder and puts it back to the binder pool. +func (b *CookieBinding) Reset() { + b.EnableSplitting = false + PutToThePool(&CookieBinderPool, b) +} diff --git a/binder/form.go b/binder/form.go index ea7ec0a40e..8e4aba12e4 100644 --- a/binder/form.go +++ b/binder/form.go @@ -61,3 +61,9 @@ func (b *FormBinding) BindMultipart(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data.Value) } + +// Reset resets the FormBinding binder and puts it back to the binder pool. +func (b *FormBinding) Reset() { + b.EnableSplitting = false + PutToThePool(&FormBinderPool, b) +} diff --git a/binder/header.go b/binder/header.go index 42658fbab0..a334eb19b1 100644 --- a/binder/header.go +++ b/binder/header.go @@ -37,3 +37,9 @@ func (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } + +// Reset resets the HeaderBinding binder and puts it back to the binder pool. +func (b *HeaderBinding) Reset() { + b.EnableSplitting = false + PutToThePool(&HeaderBinderPool, b) +} diff --git a/binder/json.go b/binder/json.go index 5145422e4b..d53e9739b5 100644 --- a/binder/json.go +++ b/binder/json.go @@ -18,3 +18,9 @@ func (*JSONBinding) Name() string { func (b *JSONBinding) Bind(body []byte, out any) error { return b.JSONDecoder(body, out) } + +// Reset resets the JSONBinding binder and puts it back to the binder pool. +func (b *JSONBinding) Reset() { + b.JSONDecoder = nil + PutToThePool(&JSONBinderPool, b) +} diff --git a/binder/query.go b/binder/query.go index 0811b3d8d2..7bad1c30af 100644 --- a/binder/query.go +++ b/binder/query.go @@ -51,3 +51,9 @@ func (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } + +// Reset resets the QueryBinding binder and puts it back to the binder pool. +func (b *QueryBinding) Reset() { + b.EnableSplitting = false + PutToThePool(&QueryBinderPool, b) +} diff --git a/binder/resp_header.go b/binder/resp_header.go index 5dbc5ebdc4..29c54c1031 100644 --- a/binder/resp_header.go +++ b/binder/resp_header.go @@ -37,3 +37,9 @@ func (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error { return parse(b.Name(), out, data) } + +// Reset resets the RespHeaderBinding binder and puts it back to the binder pool. +func (b *RespHeaderBinding) Reset() { + b.EnableSplitting = false + PutToThePool(&RespHeaderBinderPool, b) +} diff --git a/binder/uri.go b/binder/uri.go index ae7b63fa90..1b87a0d4e8 100644 --- a/binder/uri.go +++ b/binder/uri.go @@ -17,3 +17,8 @@ func (b *URIBinding) Bind(params []string, paramsFunc func(key string, defaultVa return parse(b.Name(), out, data) } + +// Reset puts URI binding back to the pool. +func (b *URIBinding) Reset() { + PutToThePool(&URIBinderPool, b) +} diff --git a/binder/xml.go b/binder/xml.go index b024a19902..b049876e40 100644 --- a/binder/xml.go +++ b/binder/xml.go @@ -24,3 +24,9 @@ func (b *XMLBinding) Bind(body []byte, out any) error { return nil } + +// Reset puts XML binding back to the pool. +func (b *XMLBinding) Reset() { + b.XMLDecoder = nil + PutToThePool(&XMLBinderPool, b) +} From 47a8f324a69e9488452576c6725e53bda009611b Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Mon, 23 Dec 2024 11:19:49 +0300 Subject: [PATCH 08/12] improve test coverage --- binder/cbor_test.go | 3 +++ binder/cookie_test.go | 3 +++ binder/form_test.go | 3 +++ binder/header_test.go | 3 +++ binder/json_test.go | 3 +++ binder/query_test.go | 3 +++ binder/resp_header_test.go | 3 +++ binder/uri_test.go | 2 ++ binder/xml_test.go | 3 +++ 9 files changed, 26 insertions(+) diff --git a/binder/cbor_test.go b/binder/cbor_test.go index bf84699870..16c24cbbca 100644 --- a/binder/cbor_test.go +++ b/binder/cbor_test.go @@ -55,6 +55,9 @@ func Test_CBORBinder_Bind(t *testing.T) { 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) { diff --git a/binder/cookie_test.go b/binder/cookie_test.go index 5f6d84e724..bca316c9fe 100644 --- a/binder/cookie_test.go +++ b/binder/cookie_test.go @@ -44,6 +44,9 @@ func Test_CookieBinder_Bind(t *testing.T) { 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) { diff --git a/binder/form_test.go b/binder/form_test.go index a146b74dcf..1c8a96f59c 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -48,6 +48,9 @@ func Test_FormBinder_Bind(t *testing.T) { 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) { diff --git a/binder/header_test.go b/binder/header_test.go index d1b96a098a..bdef8680ac 100644 --- a/binder/header_test.go +++ b/binder/header_test.go @@ -44,6 +44,9 @@ func Test_HeaderBinder_Bind(t *testing.T) { 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) { diff --git a/binder/json_test.go b/binder/json_test.go index 2764c8f5fb..00718fdf26 100644 --- a/binder/json_test.go +++ b/binder/json_test.go @@ -34,6 +34,9 @@ func Test_JSON_Binding_Bind(t *testing.T) { 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) { diff --git a/binder/query_test.go b/binder/query_test.go index aa4c687a6a..0d457e5795 100644 --- a/binder/query_test.go +++ b/binder/query_test.go @@ -45,6 +45,9 @@ func Test_QueryBinder_Bind(t *testing.T) { 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) { diff --git a/binder/resp_header_test.go b/binder/resp_header_test.go index 771b2c0d04..ff3b51f604 100644 --- a/binder/resp_header_test.go +++ b/binder/resp_header_test.go @@ -37,6 +37,9 @@ func Test_RespHeaderBinder_Bind(t *testing.T) { 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) { diff --git a/binder/uri_test.go b/binder/uri_test.go index 64b986aa3e..8babdef962 100644 --- a/binder/uri_test.go +++ b/binder/uri_test.go @@ -36,6 +36,8 @@ func Test_URIBinding_Bind(t *testing.T) { 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) { diff --git a/binder/xml_test.go b/binder/xml_test.go index df274ee5e7..879ccf0b78 100644 --- a/binder/xml_test.go +++ b/binder/xml_test.go @@ -51,6 +51,9 @@ func Test_XMLBinding_Bind(t *testing.T) { 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) { From e89943b2afcead207ef1224562cf38e3ae7645e5 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Mon, 23 Dec 2024 14:47:55 +0300 Subject: [PATCH 09/12] merge Form and MultipartForm methods --- bind.go | 24 ++++-------------------- binder/form.go | 11 +++++++++-- binder/form_test.go | 4 ++-- docs/api/bind.md | 45 ++++++--------------------------------------- redirect.go | 4 +--- 5 files changed, 22 insertions(+), 66 deletions(-) diff --git a/bind.go b/bind.go index d3083cde02..bd31523ecd 100644 --- a/bind.go +++ b/bind.go @@ -195,6 +195,9 @@ 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 { bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool) bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers @@ -227,23 +230,6 @@ func (b *Bind) URI(out any) error { return b.validateStruct(out) } -// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string. -func (b *Bind) MultipartForm(out any) error { - bind := binder.GetFromThePool[*binder.FormBinding](&binder.FormBinderPool) - bind.EnableSplitting = b.ctx.App().config.EnableSplittingOnParsers - - // Reset & put binder - defer func() { - bind.Reset() - }() - - if err := b.returnErr(bind.BindMultipart(&b.ctx.RequestCtx().Request, out)); err != nil { - return err - } - - return b.validateStruct(out) -} - // Body binds the request body into the struct, map[string]string and map[string][]string. // It supports decoding the following content types based on the Content-Type header: // application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data @@ -272,10 +258,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 diff --git a/binder/form.go b/binder/form.go index 8e4aba12e4..4bd9c24e07 100644 --- a/binder/form.go +++ b/binder/form.go @@ -8,6 +8,8 @@ import ( "github.com/valyala/fasthttp" ) +const MIMEMultipartForm string = "multipart/form-data" + // FormBinding is the form binder for form request body. type FormBinding struct { EnableSplitting bool @@ -23,6 +25,11 @@ func (b *FormBinding) Bind(req *fasthttp.Request, out any) error { data := make(map[string][]string) var err error + // 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 @@ -52,8 +59,8 @@ func (b *FormBinding) Bind(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } -// BindMultipart parses the request body and returns the result. -func (b *FormBinding) BindMultipart(req *fasthttp.Request, out any) error { +// 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 diff --git a/binder/form_test.go b/binder/form_test.go index 1c8a96f59c..c3c52c73fd 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -118,7 +118,7 @@ func Test_FormBinder_BindMultipart(t *testing.T) { fasthttp.ReleaseRequest(req) }) - err := b.BindMultipart(req, &user) + err := b.Bind(req, &user) require.NoError(t, err) require.Equal(t, "john", user.Name) @@ -164,7 +164,7 @@ func Benchmark_FormBinder_BindMultipart(b *testing.B) { var err error for i := 0; i < b.N; i++ { - err = binder.BindMultipart(req, &user) + err = binder.Bind(req, &user) } require.NoError(b, err) diff --git a/docs/api/bind.md b/docs/api/bind.md index e91fed4273..ff231af54a 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -83,7 +83,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 +111,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 +157,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. diff --git a/redirect.go b/redirect.go index bc79314922..483272c7b5 100644 --- a/redirect.go +++ b/redirect.go @@ -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 } From 182d53aabce6b302d475d6e08432c71e3705816d Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Mon, 23 Dec 2024 14:50:38 +0300 Subject: [PATCH 10/12] fix linter --- docs/api/bind.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api/bind.md b/docs/api/bind.md index ff231af54a..d2b336310d 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -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) From 7d50265487c48c4704742a237c48ce3d12e4f978 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 24 Dec 2024 23:42:38 +0300 Subject: [PATCH 11/12] split reset and putting steps --- bind.go | 10 +++++++++- binder/cbor.go | 3 +-- binder/cookie.go | 3 +-- binder/form.go | 3 +-- binder/header.go | 3 +-- binder/json.go | 3 +-- binder/query.go | 3 +-- binder/resp_header.go | 3 +-- binder/uri.go | 4 ++-- binder/xml.go | 3 +-- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bind.go b/bind.go index bd31523ecd..13d9d3675e 100644 --- a/bind.go +++ b/bind.go @@ -83,6 +83,7 @@ func (b *Bind) Header(out any) error { // Reset & put binder defer func() { bind.Reset() + binder.PutToThePool(&binder.HeaderBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Request(), out)); err != nil { @@ -100,6 +101,7 @@ func (b *Bind) RespHeader(out any) error { // Reset & put binder defer func() { bind.Reset() + binder.PutToThePool(&binder.RespHeaderBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Response(), out)); err != nil { @@ -118,6 +120,7 @@ func (b *Bind) Cookie(out any) error { // 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 { @@ -135,6 +138,7 @@ func (b *Bind) Query(out any) error { // 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 { @@ -152,6 +156,7 @@ func (b *Bind) JSON(out any) error { // Reset & put binder defer func() { bind.Reset() + binder.PutToThePool(&binder.JSONBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -169,6 +174,7 @@ func (b *Bind) CBOR(out any) error { // Reset & put binder defer func() { bind.Reset() + binder.PutToThePool(&binder.CBORBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -185,6 +191,7 @@ func (b *Bind) XML(out any) error { // Reset & put binder defer func() { bind.Reset() + binder.PutToThePool(&binder.XMLBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Body(), out)); err != nil { @@ -205,6 +212,7 @@ func (b *Bind) Form(out any) error { // 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 { @@ -220,7 +228,7 @@ func (b *Bind) URI(out any) error { // Reset & put binder defer func() { - bind.Reset() + binder.PutToThePool(&binder.URIBinderPool, bind) }() if err := b.returnErr(bind.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil { diff --git a/binder/cbor.go b/binder/cbor.go index 44c9a08e37..8b1d0d4291 100644 --- a/binder/cbor.go +++ b/binder/cbor.go @@ -19,8 +19,7 @@ func (b *CBORBinding) Bind(body []byte, out any) error { return b.CBORDecoder(body, out) } -// Reset resets the CBORBinding binder and puts it back to the binder pool. +// Reset resets the CBORBinding binder. func (b *CBORBinding) Reset() { b.CBORDecoder = nil - PutToThePool(&CBORBinderPool, b) } diff --git a/binder/cookie.go b/binder/cookie.go index 9e5b2a53f8..230794f45a 100644 --- a/binder/cookie.go +++ b/binder/cookie.go @@ -48,8 +48,7 @@ func (b *CookieBinding) Bind(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } -// Reset resets the CookieBinding binder and puts it back to the binder pool. +// Reset resets the CookieBinding binder. func (b *CookieBinding) Reset() { b.EnableSplitting = false - PutToThePool(&CookieBinderPool, b) } diff --git a/binder/form.go b/binder/form.go index 4bd9c24e07..7ab0b1b258 100644 --- a/binder/form.go +++ b/binder/form.go @@ -69,8 +69,7 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data.Value) } -// Reset resets the FormBinding binder and puts it back to the binder pool. +// Reset resets the FormBinding binder. func (b *FormBinding) Reset() { b.EnableSplitting = false - PutToThePool(&FormBinderPool, b) } diff --git a/binder/header.go b/binder/header.go index a334eb19b1..b04ce9add3 100644 --- a/binder/header.go +++ b/binder/header.go @@ -38,8 +38,7 @@ func (b *HeaderBinding) Bind(req *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } -// Reset resets the HeaderBinding binder and puts it back to the binder pool. +// Reset resets the HeaderBinding binder. func (b *HeaderBinding) Reset() { b.EnableSplitting = false - PutToThePool(&HeaderBinderPool, b) } diff --git a/binder/json.go b/binder/json.go index d53e9739b5..a6a904b550 100644 --- a/binder/json.go +++ b/binder/json.go @@ -19,8 +19,7 @@ func (b *JSONBinding) Bind(body []byte, out any) error { return b.JSONDecoder(body, out) } -// Reset resets the JSONBinding binder and puts it back to the binder pool. +// Reset resets the JSONBinding binder. func (b *JSONBinding) Reset() { b.JSONDecoder = nil - PutToThePool(&JSONBinderPool, b) } diff --git a/binder/query.go b/binder/query.go index 7bad1c30af..9ee500ba63 100644 --- a/binder/query.go +++ b/binder/query.go @@ -52,8 +52,7 @@ func (b *QueryBinding) Bind(reqCtx *fasthttp.Request, out any) error { return parse(b.Name(), out, data) } -// Reset resets the QueryBinding binder and puts it back to the binder pool. +// Reset resets the QueryBinding binder. func (b *QueryBinding) Reset() { b.EnableSplitting = false - PutToThePool(&QueryBinderPool, b) } diff --git a/binder/resp_header.go b/binder/resp_header.go index 29c54c1031..fc84d01402 100644 --- a/binder/resp_header.go +++ b/binder/resp_header.go @@ -38,8 +38,7 @@ func (b *RespHeaderBinding) Bind(resp *fasthttp.Response, out any) error { return parse(b.Name(), out, data) } -// Reset resets the RespHeaderBinding binder and puts it back to the binder pool. +// Reset resets the RespHeaderBinding binder. func (b *RespHeaderBinding) Reset() { b.EnableSplitting = false - PutToThePool(&RespHeaderBinderPool, b) } diff --git a/binder/uri.go b/binder/uri.go index 1b87a0d4e8..042b5e65f6 100644 --- a/binder/uri.go +++ b/binder/uri.go @@ -18,7 +18,7 @@ func (b *URIBinding) Bind(params []string, paramsFunc func(key string, defaultVa return parse(b.Name(), out, data) } -// Reset puts URI binding back to the pool. +// Reset resets URIBinding binder. func (b *URIBinding) Reset() { - PutToThePool(&URIBinderPool, b) + // Nothing to reset } diff --git a/binder/xml.go b/binder/xml.go index b049876e40..0c345a4236 100644 --- a/binder/xml.go +++ b/binder/xml.go @@ -25,8 +25,7 @@ func (b *XMLBinding) Bind(body []byte, out any) error { return nil } -// Reset puts XML binding back to the pool. +// Reset resets the XMLBinding binder. func (b *XMLBinding) Reset() { b.XMLDecoder = nil - PutToThePool(&XMLBinderPool, b) } From 2ac8bbf813637aa9364cbfba6613840de07c1b5c Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 24 Dec 2024 23:54:58 +0300 Subject: [PATCH 12/12] fix linter --- binder/uri.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/uri.go b/binder/uri.go index 042b5e65f6..9b358c64b8 100644 --- a/binder/uri.go +++ b/binder/uri.go @@ -19,6 +19,6 @@ func (b *URIBinding) Bind(params []string, paramsFunc func(key string, defaultVa } // Reset resets URIBinding binder. -func (b *URIBinding) Reset() { +func (*URIBinding) Reset() { // Nothing to reset }