From eacde70294be2ca277f6125e62cc1ea04907f2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Efe=20=C3=87etin?= Date: Mon, 8 Aug 2022 10:16:08 +0300 Subject: [PATCH] :sparkles: v3 (feature): initial support for binding (#1981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: v3 (feature): initial support for binding * ✨ v3 (feature): initial support for binding #1981 use pointer/references instead of copies * :sparkles: v3 (feature): initial support for binding embed bind into the ctx * :sparkles: v3 (feature): initial support for binding - add URI binder. * :sparkles: v3 (feature): initial support for binding - add response header binder. * :sparkles: v3 (feature): initial support for binding - add response header binder. * :sparkles: v3 (feature): initial support for binding - add cookie binder. * :sparkles: v3 (feature): initial support for binding - custom binder support for body binding. - test case for custom binder. * :sparkles: v3 (feature): initial support for binding - add map[string][]string & map[string]string support for binders. * :sparkles: v3 (feature): initial support for binding - fix Test_Bind_Header_Map * :sparkles: v3 (feature): initial support for binding - Functional Should/Must * :sparkles: v3 (feature): initial support for binding - custom struct validator support. * :sparkles: v3 (feature): initial support for binding - README for binding. - Docs for binding methods. * :sparkles: v3 (feature): initial support for binding - Bind() -> BindVars(), Binding() -> Bind() * :sparkles: v3 (feature): initial support for binding - fix doc problems * :sparkles: v3 (feature): initial support for binding - fix doc problems Co-authored-by: wernerr --- app.go | 27 +- bind.go | 194 +++++ bind_test.go | 1544 +++++++++++++++++++++++++++++++++++ binder/README.md | 194 +++++ binder/binder.go | 19 + binder/cookie.go | 45 + binder/form.go | 53 ++ binder/header.go | 34 + binder/json.go | 15 + binder/mapping.go | 201 +++++ binder/mapping_test.go | 31 + binder/query.go | 49 ++ binder/resp_header.go | 34 + binder/uri.go | 16 + binder/xml.go | 15 + ctx.go | 274 +------ ctx_interface.go | 34 +- ctx_test.go | 899 +------------------- error.go | 12 + middleware/logger/logger.go | 7 +- 20 files changed, 2513 insertions(+), 1184 deletions(-) create mode 100644 bind.go create mode 100644 bind_test.go create mode 100644 binder/README.md create mode 100644 binder/binder.go create mode 100644 binder/cookie.go create mode 100644 binder/form.go create mode 100644 binder/header.go create mode 100644 binder/json.go create mode 100644 binder/mapping.go create mode 100644 binder/mapping_test.go create mode 100644 binder/query.go create mode 100644 binder/resp_header.go create mode 100644 binder/uri.go create mode 100644 binder/xml.go diff --git a/app.go b/app.go index 3ccda524..405a4296 100644 --- a/app.go +++ b/app.go @@ -123,6 +123,8 @@ type App struct { latestGroup *Group // newCtxFunc newCtxFunc func(app *App) CustomCtx + // custom binders + customBinders []CustomBinder } // Config is a struct holding the server settings. @@ -366,6 +368,12 @@ type Config struct { // If set to true, will print all routes with their method, path and handler. // Default: false EnablePrintRoutes bool `json:"enable_print_routes"` + + // If you want to validate header/form/query... automatically when to bind, you can define struct validator. + // Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator. + // + // Default: nil + StructValidator StructValidator } // Static defines configuration options when defining static assets. @@ -453,12 +461,13 @@ func New(config ...Config) *App { stack: make([][]*Route, len(intMethod)), treeStack: make([]map[string][]*Route, len(intMethod)), // Create config - config: Config{}, - getBytes: utils.UnsafeBytes, - getString: utils.UnsafeString, - appList: make(map[string]*App), - latestRoute: &Route{}, - latestGroup: &Group{}, + config: Config{}, + getBytes: utils.UnsafeBytes, + getString: utils.UnsafeString, + appList: make(map[string]*App), + latestRoute: &Route{}, + latestGroup: &Group{}, + customBinders: []CustomBinder{}, } // Create Ctx pool @@ -546,6 +555,12 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) { app.newCtxFunc = function } +// You can register custom binders to use as Bind().Custom("name"). +// They should be compatible with CustomBinder interface. +func (app *App) RegisterCustomBinder(binder CustomBinder) { + app.customBinders = append(app.customBinders, binder) +} + // Mount attaches another app instance as a sub-router along a routing path. // It's very useful to split up a large API as many independent routers and // compose them as a single service using Mount. The fiber's error handler and diff --git a/bind.go b/bind.go new file mode 100644 index 00000000..985ee34c --- /dev/null +++ b/bind.go @@ -0,0 +1,194 @@ +package fiber + +import ( + "github.com/gofiber/fiber/v3/binder" + "github.com/gofiber/fiber/v3/utils" +) + +// An interface to register custom binders. +type CustomBinder interface { + Name() string + MIMETypes() []string + Parse(Ctx, any) error +} + +// An interface to register custom struct validator for binding. +type StructValidator interface { + Engine() any + ValidateStruct(any) error +} + +// Bind struct +type Bind struct { + ctx *DefaultCtx + should bool +} + +// To handle binder errors manually, you can prefer Should method. +// It's default behavior of binder. +func (b *Bind) Should() *Bind { + b.should = true + + return b +} + +// If you want to handle binder errors automatically, you can use Must. +// If there's an error it'll return error and 400 as HTTP status. +func (b *Bind) Must() *Bind { + b.should = false + + return b +} + +// Check Should/Must errors and return it by usage. +func (b *Bind) returnErr(err error) error { + if !b.should { + b.ctx.Status(StatusBadRequest) + return NewErrors(StatusBadRequest, "Bad request: "+err.Error()) + } + + return err +} + +// Struct validation. +func (b *Bind) validateStruct(out any) error { + validator := b.ctx.app.config.StructValidator + if validator != nil { + return validator.ValidateStruct(out) + } + + return nil +} + +// To use custom binders, you have to use this method. +// You can register them from RegisterCustomBinder method of Fiber instance. +// They're checked by name, if it's not found, it will return an error. +// NOTE: Should/Must is still valid for Custom binders. +func (b *Bind) Custom(name string, dest any) error { + binders := b.ctx.App().customBinders + for _, binder := range binders { + if binder.Name() == name { + return b.returnErr(binder.Parse(b.ctx, dest)) + } + } + + return ErrCustomBinderNotFound +} + +// 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 { + return err + } + + return b.validateStruct(out) +} + +// 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 { + return err + } + + return b.validateStruct(out) +} + +// Cookie binds the requesr 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.Context(), out)); err != nil { + return err + } + + return b.validateStruct(out) +} + +// QueryParser 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.Context(), out)); err != nil { + return err + } + + return b.validateStruct(out) +} + +// 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 { + return err + } + + return b.validateStruct(out) +} + +// XML binds the body string into the struct. +func (b *Bind) XML(out any) error { + if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil { + return err + } + + return b.validateStruct(out) +} + +// 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.Context(), out)); err != nil { + return err + } + + return b.validateStruct(out) +} + +// URI binds the route parameters into the struct, map[string]string and map[string][]string. +func (b *Bind) URI(out any) error { + if err := b.returnErr(binder.URIBinder.Bind(b.ctx.route.Params, b.ctx.Params, out)); err != nil { + return err + } + + 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 { + if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), 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 +// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder. +// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error. +func (b *Bind) Body(out any) error { + // Get content-type + ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType())) + ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) + + // Parse body accordingly + switch ctype { + case MIMEApplicationJSON: + return b.JSON(out) + case MIMETextXML, MIMEApplicationXML: + return b.XML(out) + case MIMEApplicationForm: + return b.Form(out) + case MIMEMultipartForm: + return b.MultipartForm(out) + } + + // Check custom binders + binders := b.ctx.App().customBinders + for _, binder := range binders { + for _, mime := range binder.MIMETypes() { + if mime == ctype { + return b.returnErr(binder.Parse(b.ctx, out)) + } + } + } + + // No suitable content type found + return ErrUnprocessableEntity +} diff --git a/bind_test.go b/bind_test.go new file mode 100644 index 00000000..6d64f8e0 --- /dev/null +++ b/bind_test.go @@ -0,0 +1,1544 @@ +package fiber + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/gofiber/fiber/v3/binder" + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +// go test -run Test_Bind_Query -v +func Test_Bind_Query(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := new(Query) + utils.AssertEqual(t, nil, c.Bind().Query(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.Bind().Query(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.Bind().Query(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Query) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.Bind().Query(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Query2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + q2 := new(Query2) + q2.Bool = true + q2.Name = "hello world" + utils.AssertEqual(t, nil, c.Bind().Query(q2)) + utils.AssertEqual(t, "basketball,football", q2.Hobby) + utils.AssertEqual(t, true, q2.Bool) + utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, q2.Empty) + utils.AssertEqual(t, []string{""}, q2.Alloc) + utils.AssertEqual(t, []int64{1}, q2.No) + + type RequiredQuery struct { + Name string `query:"name,required"` + } + rq := new(RequiredQuery) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, "name is empty", c.Bind().Query(rq).Error()) + + type ArrayQuery struct { + Data []string + } + aq := new(ArrayQuery) + c.Request().URI().SetQueryString("data[]=john&data[]=doe") + utils.AssertEqual(t, nil, c.Bind().Query(aq)) + utils.AssertEqual(t, 2, len(aq.Data)) +} + +// go test -run Test_Bind_Query_Map -v +func Test_Bind_Query_Map(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := make(map[string][]string) + utils.AssertEqual(t, nil, c.Bind().Query(&q)) + utils.AssertEqual(t, 2, len(q["hobby"])) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q = make(map[string][]string) + utils.AssertEqual(t, nil, c.Bind().Query(&q)) + utils.AssertEqual(t, 2, len(q["hobby"])) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = make(map[string][]string) + utils.AssertEqual(t, nil, c.Bind().Query(&q)) + utils.AssertEqual(t, 3, len(q["hobby"])) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer") + qq := make(map[string]string) + utils.AssertEqual(t, nil, c.Bind().Query(&qq)) + utils.AssertEqual(t, "1", qq["id"]) + + empty := make(map[string][]string) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.Bind().Query(&empty)) + utils.AssertEqual(t, 0, len(empty["hobby"])) + + em := make(map[string][]int) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, binder.ErrMapNotConvertable, c.Bind().Query(&em)) +} + +// go test -run Test_Bind_Query_WithSetParserDecoder -v +func Test_Bind_Query_WithSetParserDecoder(t *testing.T) { + type NonRFCTime time.Time + + NonRFCConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + nonRFCTime := binder.ParserType{ + Customtype: NonRFCTime{}, + Converter: NonRFCConverter, + } + + binder.SetParserDecoder(binder.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []binder.ParserType{nonRFCTime}, + ZeroEmpty: true, + SetAliasTag: "query", + }) + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type NonRFCTimeInput struct { + Date NonRFCTime `query:"date"` + Title string `query:"title"` + Body string `query:"body"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + q := new(NonRFCTimeInput) + + c.Request().URI().SetQueryString("date=2021-04-10&title=CustomDateTest&Body=October") + utils.AssertEqual(t, nil, c.Bind().Query(q)) + fmt.Println(q.Date, "q.Date") + utils.AssertEqual(t, "CustomDateTest", q.Title) + date := fmt.Sprintf("%v", q.Date) + utils.AssertEqual(t, "{0 63753609600 }", date) + utils.AssertEqual(t, "October", q.Body) + + c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October") + q = &NonRFCTimeInput{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.Bind().Query(q)) + utils.AssertEqual(t, "", q.Title) +} + +// go test -run Test_Bind_Query_Schema -v +func Test_Bind_Query_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Query1 struct { + Name string `query:"name,required"` + Nested struct { + Age int `query:"age"` + } `query:"nested,required"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("name=tom&nested.age=10") + q := new(Query1) + utils.AssertEqual(t, nil, c.Bind().Query(q)) + + c.Request().URI().SetQueryString("namex=tom&nested.age=10") + q = new(Query1) + utils.AssertEqual(t, "name is empty", c.Bind().Query(q).Error()) + + c.Request().URI().SetQueryString("name=tom&nested.agex=10") + q = new(Query1) + utils.AssertEqual(t, nil, c.Bind().Query(q)) + + c.Request().URI().SetQueryString("name=tom&test.age=10") + q = new(Query1) + utils.AssertEqual(t, "nested is empty", c.Bind().Query(q).Error()) + + type Query2 struct { + Name string `query:"name"` + Nested struct { + Age int `query:"age,required"` + } `query:"nested"` + } + c.Request().URI().SetQueryString("name=tom&nested.age=10") + q2 := new(Query2) + utils.AssertEqual(t, nil, c.Bind().Query(q2)) + + c.Request().URI().SetQueryString("nested.age=10") + q2 = new(Query2) + utils.AssertEqual(t, nil, c.Bind().Query(q2)) + + c.Request().URI().SetQueryString("nested.agex=10") + q2 = new(Query2) + utils.AssertEqual(t, "nested.age is empty", c.Bind().Query(q2).Error()) + + c.Request().URI().SetQueryString("nested.agex=10") + q2 = new(Query2) + utils.AssertEqual(t, "nested.age is empty", c.Bind().Query(q2).Error()) + + type Node struct { + Value int `query:"val,required"` + Next *Node `query:"next,required"` + } + c.Request().URI().SetQueryString("val=1&next.val=3") + n := new(Node) + utils.AssertEqual(t, nil, c.Bind().Query(n)) + utils.AssertEqual(t, 1, n.Value) + utils.AssertEqual(t, 3, n.Next.Value) + + c.Request().URI().SetQueryString("next.val=2") + n = new(Node) + utils.AssertEqual(t, "val is empty", c.Bind().Query(n).Error()) + + c.Request().URI().SetQueryString("val=3&next.value=2") + n = new(Node) + n.Next = new(Node) + utils.AssertEqual(t, nil, c.Bind().Query(n)) + utils.AssertEqual(t, 3, n.Value) + utils.AssertEqual(t, 0, n.Next.Value) + + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type CollectionQuery struct { + Data []Person `query:"data"` + } + + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12") + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.Bind().Query(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, 10, cq.Data[0].Age) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + utils.AssertEqual(t, 12, cq.Data[1].Age) + + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") + cq = new(CollectionQuery) + utils.AssertEqual(t, nil, c.Bind().Query(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, 10, cq.Data[0].Age) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + utils.AssertEqual(t, 12, cq.Data[1].Age) +} + +// go test -run Test_Bind_Header -v +func Test_Bind_Header(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Header struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + q := new(Header) + utils.AssertEqual(t, nil, c.Bind().Header(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.Bind().Header(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Header) + c.Request().Header.Del("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Header2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Request().Header.Add("id", "2") + c.Request().Header.Add("Name", "Jane Doe") + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "go,fiber") + c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi") + c.Request().Header.Add("alloc", "") + c.Request().Header.Add("no", "1") + + h2 := new(Header2) + h2.Bool = true + h2.Name = "hello world" + utils.AssertEqual(t, nil, c.Bind().Header(h2)) + utils.AssertEqual(t, "go,fiber", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, h2.Empty) + utils.AssertEqual(t, []string{""}, h2.Alloc) + utils.AssertEqual(t, []int64{1}, h2.No) + + type RequiredHeader struct { + Name string `header:"name,required"` + } + rh := new(RequiredHeader) + c.Request().Header.Del("name") + utils.AssertEqual(t, "name is empty", c.Bind().Header(rh).Error()) +} + +// go test -run Test_Bind_Header_Map -v +func Test_Bind_Header_Map(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + q := make(map[string][]string, 0) + utils.AssertEqual(t, nil, c.Bind().Header(&q)) + utils.AssertEqual(t, 2, len(q["Hobby"])) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = make(map[string][]string, 0) + utils.AssertEqual(t, nil, c.Bind().Header(&q)) + utils.AssertEqual(t, 3, len(q["Hobby"])) + + empty := make(map[string][]string, 0) + c.Request().Header.Del("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(&empty)) + utils.AssertEqual(t, 0, len(empty["Hobby"])) +} + +// go test -run Test_Bind_Header_WithSetParserDecoder -v +func Test_Bind_Header_WithSetParserDecoder(t *testing.T) { + type NonRFCTime time.Time + + NonRFCConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + nonRFCTime := binder.ParserType{ + Customtype: NonRFCTime{}, + Converter: NonRFCConverter, + } + + binder.SetParserDecoder(binder.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []binder.ParserType{nonRFCTime}, + ZeroEmpty: true, + SetAliasTag: "req", + }) + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type NonRFCTimeInput struct { + Date NonRFCTime `req:"date"` + Title string `req:"title"` + Body string `req:"body"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + r := new(NonRFCTimeInput) + + c.Request().Header.Add("Date", "2021-04-10") + c.Request().Header.Add("Title", "CustomDateTest") + c.Request().Header.Add("Body", "October") + + utils.AssertEqual(t, nil, c.Bind().Header(r)) + fmt.Println(r.Date, "q.Date") + utils.AssertEqual(t, "CustomDateTest", r.Title) + date := fmt.Sprintf("%v", r.Date) + utils.AssertEqual(t, "{0 63753609600 }", date) + utils.AssertEqual(t, "October", r.Body) + + c.Request().Header.Add("Title", "") + r = &NonRFCTimeInput{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.Bind().Header(r)) + utils.AssertEqual(t, "", r.Title) +} + +// go test -run Test_Bind_Header_Schema -v +func Test_Bind_Header_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Header1 struct { + Name string `header:"Name,required"` + Nested struct { + Age int `header:"Age"` + } `header:"Nested,required"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Add("Nested.Age", "10") + q := new(Header1) + utils.AssertEqual(t, nil, c.Bind().Header(q)) + + c.Request().Header.Del("Name") + q = new(Header1) + utils.AssertEqual(t, "Name is empty", c.Bind().Header(q).Error()) + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Del("Nested.Age") + c.Request().Header.Add("Nested.Agex", "10") + q = new(Header1) + utils.AssertEqual(t, nil, c.Bind().Header(q)) + + c.Request().Header.Del("Nested.Agex") + q = new(Header1) + utils.AssertEqual(t, "Nested is empty", c.Bind().Header(q).Error()) + + c.Request().Header.Del("Nested.Agex") + c.Request().Header.Del("Name") + + type Header2 struct { + Name string `header:"Name"` + Nested struct { + Age int `header:"age,required"` + } `header:"Nested"` + } + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Add("Nested.Age", "10") + + h2 := new(Header2) + utils.AssertEqual(t, nil, c.Bind().Header(h2)) + + c.Request().Header.Del("Name") + h2 = new(Header2) + utils.AssertEqual(t, nil, c.Bind().Header(h2)) + + c.Request().Header.Del("Name") + c.Request().Header.Del("Nested.Age") + c.Request().Header.Add("Nested.Agex", "10") + h2 = new(Header2) + utils.AssertEqual(t, "Nested.age is empty", c.Bind().Header(h2).Error()) + + type Node struct { + Value int `header:"Val,required"` + Next *Node `header:"Next,required"` + } + c.Request().Header.Add("Val", "1") + c.Request().Header.Add("Next.Val", "3") + n := new(Node) + utils.AssertEqual(t, nil, c.Bind().Header(n)) + utils.AssertEqual(t, 1, n.Value) + utils.AssertEqual(t, 3, n.Next.Value) + + c.Request().Header.Del("Val") + n = new(Node) + utils.AssertEqual(t, "Val is empty", c.Bind().Header(n).Error()) + + c.Request().Header.Add("Val", "3") + c.Request().Header.Del("Next.Val") + c.Request().Header.Add("Next.Value", "2") + n = new(Node) + n.Next = new(Node) + utils.AssertEqual(t, nil, c.Bind().Header(n)) + utils.AssertEqual(t, 3, n.Value) + utils.AssertEqual(t, 0, n.Next.Value) +} + +// go test -run Test_Bind_Resp_Header -v +func Test_Bind_RespHeader(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Header struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Response().Header.Add("id", "1") + c.Response().Header.Add("Name", "John Doe") + c.Response().Header.Add("Hobby", "golang,fiber") + q := new(Header) + utils.AssertEqual(t, nil, c.Bind().RespHeader(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Response().Header.Del("hobby") + c.Response().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.Bind().RespHeader(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Header) + c.Response().Header.Del("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Header2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Response().Header.Add("id", "2") + c.Response().Header.Add("Name", "Jane Doe") + c.Response().Header.Del("hobby") + c.Response().Header.Add("Hobby", "go,fiber") + c.Response().Header.Add("favouriteDrinks", "milo,coke,pepsi") + c.Response().Header.Add("alloc", "") + c.Response().Header.Add("no", "1") + + h2 := new(Header2) + h2.Bool = true + h2.Name = "hello world" + utils.AssertEqual(t, nil, c.Bind().RespHeader(h2)) + utils.AssertEqual(t, "go,fiber", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, h2.Empty) + utils.AssertEqual(t, []string{""}, h2.Alloc) + utils.AssertEqual(t, []int64{1}, h2.No) + + type RequiredHeader struct { + Name string `respHeader:"name,required"` + } + rh := new(RequiredHeader) + c.Response().Header.Del("name") + utils.AssertEqual(t, "name is empty", c.Bind().RespHeader(rh).Error()) +} + +// go test -run Test_Bind_RespHeader_Map -v +func Test_Bind_RespHeader_Map(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Response().Header.Add("id", "1") + c.Response().Header.Add("Name", "John Doe") + c.Response().Header.Add("Hobby", "golang,fiber") + q := make(map[string][]string, 0) + utils.AssertEqual(t, nil, c.Bind().RespHeader(&q)) + utils.AssertEqual(t, 2, len(q["Hobby"])) + + c.Response().Header.Del("hobby") + c.Response().Header.Add("Hobby", "golang,fiber,go") + q = make(map[string][]string, 0) + utils.AssertEqual(t, nil, c.Bind().RespHeader(&q)) + utils.AssertEqual(t, 3, len(q["Hobby"])) + + empty := make(map[string][]string, 0) + c.Response().Header.Del("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(&empty)) + utils.AssertEqual(t, 0, len(empty["Hobby"])) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Query -benchmem -count=4 +func Benchmark_Bind_Query(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Query(q) + } + utils.AssertEqual(b, nil, c.Bind().Query(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Query_Map -benchmem -count=4 +func Benchmark_Bind_Query_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := make(map[string][]string) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Query(&q) + } + utils.AssertEqual(b, nil, c.Bind().Query(&q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Query_WithParseParam -benchmem -count=4 +func Benchmark_Bind_Query_WithParseParam(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type CollectionQuery struct { + Data []Person `query:"data"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10") + cq := new(CollectionQuery) + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Query(cq) + } + + utils.AssertEqual(b, nil, c.Bind().Query(cq)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Query_Comma -benchmem -count=4 +func Benchmark_Bind_Query_Comma(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + // c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Query(q) + } + utils.AssertEqual(b, nil, c.Bind().Query(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Header -benchmem -count=4 +func Benchmark_Bind_Header(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type ReqHeader struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + + q := new(ReqHeader) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Header(q) + } + utils.AssertEqual(b, nil, c.Bind().Header(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Header_Map -benchmem -count=4 +func Benchmark_Bind_Header_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + + q := make(map[string][]string) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().Header(&q) + } + utils.AssertEqual(b, nil, c.Bind().Header(&q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader -benchmem -count=4 +func Benchmark_Bind_RespHeader(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type ReqHeader struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Response().Header.Add("id", "1") + c.Response().Header.Add("Name", "John Doe") + c.Response().Header.Add("Hobby", "golang,fiber") + + q := new(ReqHeader) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().RespHeader(q) + } + utils.AssertEqual(b, nil, c.Bind().RespHeader(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader_Map -benchmem -count=4 +func Benchmark_Bind_RespHeader_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Response().Header.Add("id", "1") + c.Response().Header.Add("Name", "John Doe") + c.Response().Header.Add("Hobby", "golang,fiber") + + q := make(map[string][]string) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Bind().RespHeader(&q) + } + utils.AssertEqual(b, nil, c.Bind().RespHeader(&q)) +} + +// go test -run Test_Bind_Body +func Test_Bind_Body(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `json:"name" xml:"name" form:"name" query:"name"` + } + + { + var gzipJSON bytes.Buffer + w := gzip.NewWriter(&gzipJSON) + _, _ = w.Write([]byte(`{"name":"john"}`)) + _ = w.Close() + + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.Set(HeaderContentEncoding, "gzip") + c.Request().SetBody(gzipJSON.Bytes()) + c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) + d := new(Demo) + utils.AssertEqual(t, nil, c.Bind().Body(d)) + utils.AssertEqual(t, "john", d.Name) + c.Request().Header.Del(HeaderContentEncoding) + } + + testDecodeParser := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + utils.AssertEqual(t, nil, c.Bind().Body(d)) + utils.AssertEqual(t, "john", d.Name) + } + + testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) + testDecodeParser(MIMEApplicationXML, `john`) + testDecodeParser(MIMEApplicationForm, "name=john") + testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + + testDecodeParserError := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + utils.AssertEqual(t, false, c.Bind().Body(nil) == nil) + } + + testDecodeParserError("invalid-content-type", "") + testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b") + + type CollectionQuery struct { + Data []Demo `query:"data"` + } + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe")) + c.Request().Header.SetContentLength(len(c.Body())) + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.Bind().Body(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe")) + c.Request().Header.SetContentLength(len(c.Body())) + cq = new(CollectionQuery) + utils.AssertEqual(t, nil, c.Bind().Body(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) +} + +// go test -run Test_Bind_Body_WithSetParserDecoder +func Test_Bind_Body_WithSetParserDecoder(t *testing.T) { + type CustomTime time.Time + + timeConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + customTime := binder.ParserType{ + Customtype: CustomTime{}, + Converter: timeConverter, + } + + binder.SetParserDecoder(binder.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []binder.ParserType{customTime}, + ZeroEmpty: true, + SetAliasTag: "form", + }) + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Date CustomTime `form:"date"` + Title string `form:"title"` + Body string `form:"body"` + } + + testDecodeParser := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + d := Demo{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.Bind().Body(&d)) + date := fmt.Sprintf("%v", d.Date) + utils.AssertEqual(t, "{0 63743587200 }", date) + utils.AssertEqual(t, "", d.Title) + utils.AssertEqual(t, "New Body", d.Body) + } + + testDecodeParser(MIMEApplicationForm, "date=2020-12-15&title=&body=New Body") + testDecodeParser(MIMEMultipartForm+`; boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"date\"\r\n\r\n2020-12-15\r\n--b\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n\r\n--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nNew Body\r\n--b--") +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Body_JSON -benchmem -count=4 +func Benchmark_Bind_Body_JSON(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `json:"name"` + } + body := []byte(`{"name":"john"}`) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.Bind().Body(d) + } + utils.AssertEqual(b, nil, c.Bind().Body(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Body_XML -benchmem -count=4 +func Benchmark_Bind_Body_XML(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `xml:"name"` + } + body := []byte("john") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.Bind().Body(d) + } + utils.AssertEqual(b, nil, c.Bind().Body(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4 +func Benchmark_Bind_Body_Form(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `form:"name"` + } + body := []byte("name=john") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.Bind().Body(d) + } + utils.AssertEqual(b, nil, c.Bind().Body(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm -benchmem -count=4 +func Benchmark_Bind_Body_MultipartForm(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Demo struct { + Name string `form:"name"` + } + + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.Bind().Body(d) + } + utils.AssertEqual(b, nil, c.Bind().Body(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4 +func Benchmark_Bind_Body_Form_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + body := []byte("name=john") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().Header.SetContentLength(len(body)) + d := make(map[string]string) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.Bind().Body(&d) + } + utils.AssertEqual(b, nil, c.Bind().Body(&d)) + utils.AssertEqual(b, "john", d["name"]) +} + +// go test -run Test_Bind_URI +func Test_Bind_URI(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/test1/userId/role/:roleId", func(c Ctx) error { + type Demo struct { + UserID uint `uri:"userId"` + RoleID uint `uri:"roleId"` + } + var ( + d = new(Demo) + ) + if err := c.Bind().URI(d); err != nil { + t.Fatal(err) + } + utils.AssertEqual(t, uint(111), d.UserID) + utils.AssertEqual(t, uint(222), d.RoleID) + return nil + }) + app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) + app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) +} + +// go test -run Test_Bind_URI_Map +func Test_Bind_URI_Map(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/test1/userId/role/:roleId", func(c Ctx) error { + d := make(map[string]string) + + if err := c.Bind().URI(&d); err != nil { + t.Fatal(err) + } + utils.AssertEqual(t, uint(111), d["userId"]) + utils.AssertEqual(t, uint(222), d["roleId"]) + return nil + }) + app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) + app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_URI -benchmem -count=4 +func Benchmark_Bind_URI(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + + c.route = &Route{ + Params: []string{ + "param1", "param2", "param3", "param4", + }, + } + c.values = [maxParams]string{ + "john", "doe", "is", "awesome", + } + + var res struct { + Param1 string `uri:"param1"` + Param2 string `uri:"param2"` + Param3 string `uri:"param3"` + Param4 string `uri:"param4"` + } + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c.Bind().URI(&res) + } + + utils.AssertEqual(b, "john", res.Param1) + utils.AssertEqual(b, "doe", res.Param2) + utils.AssertEqual(b, "is", res.Param3) + utils.AssertEqual(b, "awesome", res.Param4) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_URI_Map -benchmem -count=4 +func Benchmark_Bind_URI_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + + c.route = &Route{ + Params: []string{ + "param1", "param2", "param3", "param4", + }, + } + c.values = [maxParams]string{ + "john", "doe", "is", "awesome", + } + + res := make(map[string]string) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c.Bind().URI(&res) + } + + utils.AssertEqual(b, "john", res["param1"]) + utils.AssertEqual(b, "doe", res["param2"]) + utils.AssertEqual(b, "is", res["param3"]) + utils.AssertEqual(b, "awesome", res["param4"]) +} + +// go test -run Test_Bind_Cookie -v +func Test_Bind_Cookie(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Cookie struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.SetCookie("id", "1") + c.Request().Header.SetCookie("Name", "John Doe") + c.Request().Header.SetCookie("Hobby", "golang,fiber") + q := new(Cookie) + utils.AssertEqual(t, nil, c.Bind().Cookie(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().Header.DelCookie("hobby") + c.Request().Header.SetCookie("Hobby", "golang,fiber,go") + q = new(Cookie) + utils.AssertEqual(t, nil, c.Bind().Cookie(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Cookie) + c.Request().Header.DelCookie("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Cookie2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Request().Header.SetCookie("id", "2") + c.Request().Header.SetCookie("Name", "Jane Doe") + c.Request().Header.DelCookie("hobby") + c.Request().Header.SetCookie("Hobby", "go,fiber") + c.Request().Header.SetCookie("favouriteDrinks", "milo,coke,pepsi") + c.Request().Header.SetCookie("alloc", "") + c.Request().Header.SetCookie("no", "1") + + h2 := new(Cookie2) + h2.Bool = true + h2.Name = "hello world" + utils.AssertEqual(t, nil, c.Bind().Cookie(h2)) + utils.AssertEqual(t, "go,fiber", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, h2.Empty) + utils.AssertEqual(t, []string{""}, h2.Alloc) + utils.AssertEqual(t, []int64{1}, h2.No) + + type RequiredCookie struct { + Name string `cookie:"name,required"` + } + rh := new(RequiredCookie) + c.Request().Header.DelCookie("name") + utils.AssertEqual(t, "name is empty", c.Bind().Cookie(rh).Error()) +} + +// go test -run Test_Bind_Cookie_Map -v +func Test_Bind_Cookie_Map(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.SetCookie("id", "1") + c.Request().Header.SetCookie("Name", "John Doe") + c.Request().Header.SetCookie("Hobby", "golang,fiber") + q := make(map[string][]string) + utils.AssertEqual(t, nil, c.Bind().Cookie(&q)) + utils.AssertEqual(t, 2, len(q["Hobby"])) + + c.Request().Header.DelCookie("hobby") + c.Request().Header.SetCookie("Hobby", "golang,fiber,go") + q = make(map[string][]string) + utils.AssertEqual(t, nil, c.Bind().Cookie(&q)) + utils.AssertEqual(t, 3, len(q["Hobby"])) + + empty := make(map[string][]string) + c.Request().Header.DelCookie("hobby") + utils.AssertEqual(t, nil, c.Bind().Query(&empty)) + utils.AssertEqual(t, 0, len(empty["Hobby"])) +} + +// go test -run Test_Bind_Cookie_WithSetParserDecoder -v +func Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) { + type NonRFCTime time.Time + + NonRFCConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + nonRFCTime := binder.ParserType{ + Customtype: NonRFCTime{}, + Converter: NonRFCConverter, + } + + binder.SetParserDecoder(binder.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []binder.ParserType{nonRFCTime}, + ZeroEmpty: true, + SetAliasTag: "cerez", + }) + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type NonRFCTimeInput struct { + Date NonRFCTime `cerez:"date"` + Title string `cerez:"title"` + Body string `cerez:"body"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + r := new(NonRFCTimeInput) + + c.Request().Header.SetCookie("Date", "2021-04-10") + c.Request().Header.SetCookie("Title", "CustomDateTest") + c.Request().Header.SetCookie("Body", "October") + + utils.AssertEqual(t, nil, c.Bind().Cookie(r)) + fmt.Println(r.Date, "q.Date") + utils.AssertEqual(t, "CustomDateTest", r.Title) + date := fmt.Sprintf("%v", r.Date) + utils.AssertEqual(t, "{0 63753609600 }", date) + utils.AssertEqual(t, "October", r.Body) + + c.Request().Header.SetCookie("Title", "") + r = &NonRFCTimeInput{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.Bind().Cookie(r)) + utils.AssertEqual(t, "", r.Title) +} + +// go test -run Test_Bind_Cookie_Schema -v +func Test_Bind_Cookie_Schema(t *testing.T) { + t.Parallel() + + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Cookie1 struct { + Name string `cookie:"Name,required"` + Nested struct { + Age int `cookie:"Age"` + } `cookie:"Nested,required"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.SetCookie("Name", "tom") + c.Request().Header.SetCookie("Nested.Age", "10") + q := new(Cookie1) + utils.AssertEqual(t, nil, c.Bind().Cookie(q)) + + c.Request().Header.DelCookie("Name") + q = new(Cookie1) + utils.AssertEqual(t, "Name is empty", c.Bind().Cookie(q).Error()) + + c.Request().Header.SetCookie("Name", "tom") + c.Request().Header.DelCookie("Nested.Age") + c.Request().Header.SetCookie("Nested.Agex", "10") + q = new(Cookie1) + utils.AssertEqual(t, nil, c.Bind().Cookie(q)) + + c.Request().Header.DelCookie("Nested.Agex") + q = new(Cookie1) + utils.AssertEqual(t, "Nested is empty", c.Bind().Cookie(q).Error()) + + c.Request().Header.DelCookie("Nested.Agex") + c.Request().Header.DelCookie("Name") + + type Cookie2 struct { + Name string `cookie:"Name"` + Nested struct { + Age int `cookie:"Age,required"` + } `cookie:"Nested"` + } + + c.Request().Header.SetCookie("Name", "tom") + c.Request().Header.SetCookie("Nested.Age", "10") + + h2 := new(Cookie2) + utils.AssertEqual(t, nil, c.Bind().Cookie(h2)) + + c.Request().Header.DelCookie("Name") + h2 = new(Cookie2) + utils.AssertEqual(t, nil, c.Bind().Cookie(h2)) + + c.Request().Header.DelCookie("Name") + c.Request().Header.DelCookie("Nested.Age") + c.Request().Header.SetCookie("Nested.Agex", "10") + h2 = new(Cookie2) + utils.AssertEqual(t, "Nested.Age is empty", c.Bind().Cookie(h2).Error()) + + type Node struct { + Value int `cookie:"Val,required"` + Next *Node `cookie:"Next,required"` + } + c.Request().Header.SetCookie("Val", "1") + c.Request().Header.SetCookie("Next.Val", "3") + n := new(Node) + utils.AssertEqual(t, nil, c.Bind().Cookie(n)) + utils.AssertEqual(t, 1, n.Value) + utils.AssertEqual(t, 3, n.Next.Value) + + c.Request().Header.DelCookie("Val") + n = new(Node) + utils.AssertEqual(t, "Val is empty", c.Bind().Cookie(n).Error()) + + c.Request().Header.SetCookie("Val", "3") + c.Request().Header.DelCookie("Next.Val") + c.Request().Header.SetCookie("Next.Value", "2") + n = new(Node) + n.Next = new(Node) + utils.AssertEqual(t, nil, c.Bind().Cookie(n)) + utils.AssertEqual(t, 3, n.Value) + utils.AssertEqual(t, 0, n.Next.Value) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Cookie -benchmem -count=4 +func Benchmark_Bind_Cookie(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Cookie struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.SetCookie("id", "1") + c.Request().Header.SetCookie("Name", "John Doe") + c.Request().Header.SetCookie("Hobby", "golang,fiber") + + q := new(Cookie) + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c.Bind().Cookie(q) + } + utils.AssertEqual(b, nil, c.Bind().Cookie(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Bind_Cookie_Map -benchmem -count=4 +func Benchmark_Bind_Cookie_Map(b *testing.B) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.SetCookie("id", "1") + c.Request().Header.SetCookie("Name", "John Doe") + c.Request().Header.SetCookie("Hobby", "golang,fiber") + + q := make(map[string][]string) + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c.Bind().Cookie(&q) + } + utils.AssertEqual(b, nil, c.Bind().Cookie(&q)) +} + +// custom binder for testing +type customBinder struct{} + +func (b *customBinder) Name() string { + return "custom" +} + +func (b *customBinder) MIMETypes() []string { + return []string{"test", "test2"} +} + +func (b *customBinder) Parse(c Ctx, out any) error { + return json.Unmarshal(c.Body(), out) +} + +// go test -run Test_Bind_CustomBinder +func Test_Bind_CustomBinder(t *testing.T) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + // Register binder + binder := &customBinder{} + app.RegisterCustomBinder(binder) + + type Demo struct { + Name string `json:"name"` + } + body := []byte(`{"name":"john"}`) + c.Request().SetBody(body) + c.Request().Header.SetContentType("test") + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + utils.AssertEqual(t, nil, c.Bind().Body(d)) + utils.AssertEqual(t, nil, c.Bind().Custom("custom", d)) + utils.AssertEqual(t, ErrCustomBinderNotFound, c.Bind().Custom("not_custom", d)) + utils.AssertEqual(t, "john", d.Name) +} + +// go test -run Test_Bind_Must +func Test_Bind_Must(t *testing.T) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type RequiredQuery struct { + Name string `query:"name,required"` + } + rq := new(RequiredQuery) + c.Request().URI().SetQueryString("") + err := c.Bind().Must().Query(rq) + utils.AssertEqual(t, StatusBadRequest, c.Response().StatusCode()) + utils.AssertEqual(t, "Bad request: name is empty", err.Error()) +} + +// simple struct validator for testing +type structValidator struct{} + +func (v *structValidator) Engine() any { + return "" +} + +func (v *structValidator) ValidateStruct(out any) error { + out = reflect.ValueOf(out).Elem().Interface() + sq := out.(simpleQuery) + + if sq.Name != "john" { + return errors.New("you should have entered right name!") + } + + return nil +} + +type simpleQuery struct { + Name string `query:"name"` +} + +// go test -run Test_Bind_StructValidator +func Test_Bind_StructValidator(t *testing.T) { + app := New(Config{StructValidator: &structValidator{}}) + c := app.NewCtx(&fasthttp.RequestCtx{}) + + rq := new(simpleQuery) + c.Request().URI().SetQueryString("name=efe") + utils.AssertEqual(t, "you should have entered right name!", c.Bind().Query(rq).Error()) + + rq = new(simpleQuery) + c.Request().URI().SetQueryString("name=john") + utils.AssertEqual(t, nil, c.Bind().Query(rq)) +} diff --git a/binder/README.md b/binder/README.md new file mode 100644 index 00000000..d40cc7e5 --- /dev/null +++ b/binder/README.md @@ -0,0 +1,194 @@ +# Fiber Binders + +Binder is new request/response binding feature for Fiber. By aganist old Fiber parsers, it supports custom binder registration, struct validation, **map[string]string**, **map[string][]string** and more. It's introduced in Fiber v3 and a replacement of: +- BodyParser +- ParamsParser +- GetReqHeaders +- GetRespHeaders +- AllParams +- QueryParser +- ReqHeaderParser + + +## Default Binders +- [Form](form.go) +- [Query](query.go) +- [URI](uri.go) +- [Header](header.go) +- [Response Header](resp_header.go) +- [Cookie](cookie.go) +- [JSON](json.go) +- [XML](xml.go) + +## Guides + +### Binding into the Struct +Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example for it: +```go +// Field names should start with an uppercase letter +type Person struct { + Name string `json:"name" xml:"name" form:"name"` + Pass string `json:"pass" xml:"pass" form:"pass"` +} + +app.Post("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().Body(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + + // ... +}) + +// Run tests with the following curl commands: + +// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 + +// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 + +// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 + +// curl -X POST -F name=john -F pass=doe http://localhost:3000 + +// curl -X POST "http://localhost:3000/?name=john&pass=doe" +``` + +### Binding into the Map +Fiber supports binding into the **map[string]string** or **map[string][]string**. Here's an example for it: +```go +app.Get("/", func(c fiber.Ctx) error { + p := make(map[string][]string) + + if err := c.Bind().Query(p); err != nil { + return err + } + + log.Println(p["name"]) // john + log.Println(p["pass"]) // doe + log.Println(p["products"]) // [shoe, hat] + + // ... +}) +// Run tests with the following curl command: + +// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" +``` +### Behaviors of Should/Must +Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`. + +If there's an error it'll return error and 400 as HTTP status. Here's an example for it: +```go +// Field names should start with an uppercase letter +type Person struct { + Name string `json:"name,required"` + Pass string `json:"pass"` +} + +app.Get("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().Must().JSON(p); err != nil { + return err + // Status code: 400 + // Response: Bad request: name is empty + } + + // ... +}) + +// Run tests with the following curl command: + +// curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000 +``` +### Defining Custom Binder +We didn't add much binder to make Fiber codebase minimal. But if you want to use your binders, it's easy to register and use them. Here's an example for TOML binder. +```go +type Person struct { + Name string `toml:"name"` + Pass string `toml:"pass"` +} + +type tomlBinding struct{} + +func (b *tomlBinding) Name() string { + return "toml" +} + +func (b *tomlBinding) MIMETypes() []string { + return []string{"application/toml"} +} + +func (b *tomlBinding) Parse(c fiber.Ctx, out any) error { + return toml.Unmarshal(c.Body(), out) +} + +func main() { + app := fiber.New() + app.RegisterCustomBinder(&tomlBinding{}) + + app.Get("/", func(c fiber.Ctx) error { + out := new(Person) + if err := c.Bind().Body(out); err != nil { + return err + } + + // or you can use like: + // if err := c.Bind().Custom("toml", out); err != nil { + // return err + // } + + return c.SendString(out.Pass) // test + }) + + app.Listen(":3000") +} + +// curl -X GET -H "Content-Type: application/toml" --data "name = 'bar' +// pass = 'test'" localhost:3000 +``` +### Defining Custom Validator +All Fiber binders supporting struct validation if you defined validator inside of the config. You can create own validator, or use [go-playground/validator](https://github.com/go-playground/validator), [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)... Here's an example of simple custom validator: +```go +type Query struct { + Name string `query:"name"` +} + +type structValidator struct{} + +func (v *structValidator) Engine() any { + return "" +} + +func (v *structValidator) ValidateStruct(out any) error { + out = reflect.ValueOf(out).Elem().Interface() + sq := out.(Query) + + if sq.Name != "john" { + return errors.New("you should have entered right name!") + } + + return nil +} + +func main() { + app := fiber.New(fiber.Config{StructValidator: &structValidator{}}) + + app.Get("/", func(c fiber.Ctx) error { + out := new(Query) + if err := c.Bind().Query(out); err != nil { + return err // you should have entered right name! + } + return c.SendString(out.Name) + }) + + app.Listen(":3000") +} + +// Run tests with the following curl command: + +// curl "http://localhost:3000/?name=efe" +``` \ No newline at end of file diff --git a/binder/binder.go b/binder/binder.go new file mode 100644 index 00000000..d3931790 --- /dev/null +++ b/binder/binder.go @@ -0,0 +1,19 @@ +package binder + +import "errors" + +// Binder errors +var ( + ErrSuitableContentNotFound = errors.New("binder: suitable content not found to parse body") + ErrMapNotConvertable = errors.New("binder: map is not convertable to map[string]string or map[string][]string") +) + +// Init default binders for Fiber +var HeaderBinder = &headerBinding{} +var RespHeaderBinder = &respHeaderBinding{} +var CookieBinder = &cookieBinding{} +var QueryBinder = &queryBinding{} +var FormBinder = &formBinding{} +var URIBinder = &uriBinding{} +var XMLBinder = &xmlBinding{} +var JSONBinder = &jsonBinding{} diff --git a/binder/cookie.go b/binder/cookie.go new file mode 100644 index 00000000..e761e477 --- /dev/null +++ b/binder/cookie.go @@ -0,0 +1,45 @@ +package binder + +import ( + "reflect" + "strings" + + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +type cookieBinding struct{} + +func (*cookieBinding) Name() string { + return "cookie" +} + +func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { + data := make(map[string][]string) + var err error + + reqCtx.Request.Header.VisitAllCookie(func(key, val []byte) { + if err != nil { + return + } + + k := utils.UnsafeString(key) + v := utils.UnsafeString(val) + + if 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]) + } + } else { + data[k] = append(data[k], v) + } + + }) + + if err != nil { + return err + } + + return parse(b.Name(), out, data) +} diff --git a/binder/form.go b/binder/form.go new file mode 100644 index 00000000..24983ccd --- /dev/null +++ b/binder/form.go @@ -0,0 +1,53 @@ +package binder + +import ( + "reflect" + "strings" + + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +type formBinding struct{} + +func (*formBinding) Name() string { + return "form" +} + +func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { + data := make(map[string][]string) + var err error + + reqCtx.PostArgs().VisitAll(func(key, val []byte) { + if err != nil { + return + } + + k := utils.UnsafeString(key) + v := utils.UnsafeString(val) + + if strings.Contains(k, "[") { + k, err = parseParamSquareBrackets(k) + } + + if 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]) + } + } else { + data[k] = append(data[k], v) + } + }) + + return parse(b.Name(), out, data) +} + +func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { + data, err := reqCtx.MultipartForm() + if err != nil { + return err + } + + return parse(b.Name(), out, data.Value) +} diff --git a/binder/header.go b/binder/header.go new file mode 100644 index 00000000..688a8113 --- /dev/null +++ b/binder/header.go @@ -0,0 +1,34 @@ +package binder + +import ( + "reflect" + "strings" + + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +type headerBinding struct{} + +func (*headerBinding) Name() string { + return "header" +} + +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) { + values := strings.Split(v, ",") + for i := 0; i < len(values); i++ { + data[k] = append(data[k], values[i]) + } + } else { + data[k] = append(data[k], v) + } + }) + + return parse(b.Name(), out, data) +} diff --git a/binder/json.go b/binder/json.go new file mode 100644 index 00000000..570a7f9b --- /dev/null +++ b/binder/json.go @@ -0,0 +1,15 @@ +package binder + +import ( + "github.com/gofiber/fiber/v3/utils" +) + +type jsonBinding struct{} + +func (*jsonBinding) Name() string { + return "json" +} + +func (b *jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error { + return jsonDecoder(body, out) +} diff --git a/binder/mapping.go b/binder/mapping.go new file mode 100644 index 00000000..d83dbd3b --- /dev/null +++ b/binder/mapping.go @@ -0,0 +1,201 @@ +package binder + +import ( + "reflect" + "strings" + "sync" + + "github.com/gofiber/fiber/v3/internal/schema" + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/bytebufferpool" +) + +// ParserConfig form decoder config for SetParserDecoder +type ParserConfig struct { + IgnoreUnknownKeys bool + SetAliasTag string + ParserType []ParserType + ZeroEmpty bool +} + +// ParserType require two element, type and converter for register. +// Use ParserType with BodyParser for parsing custom type in form data. +type ParserType struct { + Customtype any + Converter func(string) reflect.Value +} + +// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance +var decoderPool = &sync.Pool{New: func() any { + return decoderBuilder(ParserConfig{ + IgnoreUnknownKeys: true, + ZeroEmpty: true, + }) +}} + +// SetParserDecoder allow globally change the option of form decoder, update decoderPool +func SetParserDecoder(parserConfig ParserConfig) { + decoderPool = &sync.Pool{New: func() any { + return decoderBuilder(parserConfig) + }} +} + +func decoderBuilder(parserConfig ParserConfig) any { + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys) + if parserConfig.SetAliasTag != "" { + decoder.SetAliasTag(parserConfig.SetAliasTag) + } + for _, v := range parserConfig.ParserType { + decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter) + } + decoder.ZeroEmpty(parserConfig.ZeroEmpty) + return decoder +} + +// parse data into the map or struct +func parse(aliasTag string, out any, data map[string][]string) error { + ptrVal := reflect.ValueOf(out) + + // Get pointer value + var ptr any + if ptrVal.Kind() == reflect.Ptr { + ptrVal = ptrVal.Elem() + ptr = ptrVal.Interface() + } + + // Parse into the map + if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String { + return parseToMap(ptr, data) + } + + // Parse into the struct + return parseToStruct(aliasTag, out, data) +} + +// Parse data into the struct with gorilla/schema +func parseToStruct(aliasTag string, out any, data map[string][]string) error { + // Get decoder from pool + schemaDecoder := decoderPool.Get().(*schema.Decoder) + defer decoderPool.Put(schemaDecoder) + + // Set alias tag + schemaDecoder.SetAliasTag(aliasTag) + + return schemaDecoder.Decode(out, data) +} + +// 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 { + elem := reflect.TypeOf(ptr).Elem() + + // map[string][]string + if elem.Kind() == reflect.Slice { + newMap, ok := ptr.(map[string][]string) + if !ok { + return ErrMapNotConvertable + } + + for k, v := range data { + newMap[k] = v + } + + return nil + } + + // map[string]string + newMap, ok := ptr.(map[string]string) + if !ok { + return ErrMapNotConvertable + } + + for k, v := range data { + newMap[k] = v[len(v)-1] + } + + return nil +} + +func parseParamSquareBrackets(k string) (string, error) { + bb := bytebufferpool.Get() + defer bytebufferpool.Put(bb) + + kbytes := []byte(k) + + for i, b := range kbytes { + + if b == '[' && kbytes[i+1] != ']' { + if err := bb.WriteByte('.'); err != nil { + return "", err + } + } + + if b == '[' || b == ']' { + continue + } + + if err := bb.WriteByte(b); err != nil { + return "", err + } + } + + return bb.String(), nil +} + +func equalFieldType(out any, kind reflect.Kind, key string) bool { + // Get type of interface + outTyp := reflect.TypeOf(out).Elem() + key = utils.ToLower(key) + + // Support maps + if outTyp.Kind() == reflect.Map && outTyp.Key().Kind() == reflect.String { + return true + } + + // Must be a struct to match a field + if outTyp.Kind() != reflect.Struct { + return false + } + // Copy interface to an value to be used + outVal := reflect.ValueOf(out).Elem() + // Loop over each field + for i := 0; i < outTyp.NumField(); i++ { + // Get field value data + structField := outVal.Field(i) + // Can this field be changed? + if !structField.CanSet() { + continue + } + // Get field key data + typeField := outTyp.Field(i) + // Get type of field key + structFieldKind := structField.Kind() + // Does the field type equals input? + if structFieldKind != kind { + continue + } + // Get tag from field if exist + inputFieldName := typeField.Tag.Get(QueryBinder.Name()) + if inputFieldName == "" { + inputFieldName = typeField.Name + } else { + inputFieldName = strings.Split(inputFieldName, ",")[0] + } + // Compare field/tag with provided key + if utils.ToLower(inputFieldName) == key { + return true + } + } + return false +} + +// Get content type from content type header +func FilterFlags(content string) string { + for i, char := range content { + if char == ' ' || char == ';' { + return content[:i] + } + } + return content +} diff --git a/binder/mapping_test.go b/binder/mapping_test.go new file mode 100644 index 00000000..2c5d275b --- /dev/null +++ b/binder/mapping_test.go @@ -0,0 +1,31 @@ +package binder + +import ( + "reflect" + "testing" + + "github.com/gofiber/fiber/v3/utils" +) + +func Test_EqualFieldType(t *testing.T) { + var out int + utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key")) + + var dummy struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key")) + + var dummy2 struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f")) + + var user struct { + Name string + Address string `query:"address"` + Age int `query:"AGE"` + } + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name")) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name")) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address")) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address")) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE")) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age")) +} diff --git a/binder/query.go b/binder/query.go new file mode 100644 index 00000000..ce62e09d --- /dev/null +++ b/binder/query.go @@ -0,0 +1,49 @@ +package binder + +import ( + "reflect" + "strings" + + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +type queryBinding struct{} + +func (*queryBinding) Name() string { + return "query" +} + +func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { + data := make(map[string][]string) + var err error + + reqCtx.QueryArgs().VisitAll(func(key, val []byte) { + if err != nil { + return + } + + k := utils.UnsafeString(key) + v := utils.UnsafeString(val) + + if strings.Contains(k, "[") { + k, err = parseParamSquareBrackets(k) + } + + if 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]) + } + } else { + data[k] = append(data[k], v) + } + + }) + + if err != nil { + return err + } + + return parse(b.Name(), out, data) +} diff --git a/binder/resp_header.go b/binder/resp_header.go new file mode 100644 index 00000000..2b31710d --- /dev/null +++ b/binder/resp_header.go @@ -0,0 +1,34 @@ +package binder + +import ( + "reflect" + "strings" + + "github.com/gofiber/fiber/v3/utils" + "github.com/valyala/fasthttp" +) + +type respHeaderBinding struct{} + +func (*respHeaderBinding) Name() string { + return "respHeader" +} + +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) { + values := strings.Split(v, ",") + for i := 0; i < len(values); i++ { + data[k] = append(data[k], values[i]) + } + } else { + data[k] = append(data[k], v) + } + }) + + return parse(b.Name(), out, data) +} diff --git a/binder/uri.go b/binder/uri.go new file mode 100644 index 00000000..2759f7b4 --- /dev/null +++ b/binder/uri.go @@ -0,0 +1,16 @@ +package binder + +type uriBinding struct{} + +func (*uriBinding) Name() string { + return "uri" +} + +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)) + } + + return parse(b.Name(), out, data) +} diff --git a/binder/xml.go b/binder/xml.go new file mode 100644 index 00000000..29401abb --- /dev/null +++ b/binder/xml.go @@ -0,0 +1,15 @@ +package binder + +import ( + "encoding/xml" +) + +type xmlBinding struct{} + +func (*xmlBinding) Name() string { + return "xml" +} + +func (b *xmlBinding) Bind(body []byte, out any) error { + return xml.Unmarshal(body, out) +} diff --git a/ctx.go b/ctx.go index 642cca3d..499c4d8d 100644 --- a/ctx.go +++ b/ctx.go @@ -9,21 +9,18 @@ import ( "context" "encoding/json" "encoding/xml" - "errors" "fmt" "io" "mime/multipart" "net" "net/http" "path/filepath" - "reflect" "strconv" "strings" "sync" "text/template" "time" - "github.com/gofiber/fiber/v3/internal/schema" "github.com/gofiber/fiber/v3/utils" "github.com/savsgio/dictpool" "github.com/valyala/bytebufferpool" @@ -33,13 +30,6 @@ import ( // maxParams defines the maximum number of parameters per route. const maxParams = 30 -// Some constants for BodyParser, QueryParser and ReqHeaderParser. -const ( - queryTag = "query" - reqHeaderTag = "reqHeader" - bodyTag = "form" -) - // userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx const userContextKey = "__local_user_context__" @@ -61,6 +51,7 @@ type DefaultCtx struct { fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx matched bool // Non use route matched viewBindMap *dictpool.Dict // Default view map to bind template engine + bind *Bind // Default bind reference } // Range data for c.Range @@ -92,21 +83,6 @@ type Views interface { Render(io.Writer, string, any, ...string) error } -// ParserType require two element, type and converter for register. -// Use ParserType with BodyParser for parsing custom type in form data. -type ParserType struct { - Customtype any - Converter func(string) reflect.Value -} - -// ParserConfig form decoder config for SetParserDecoder -type ParserConfig struct { - IgnoreUnknownKeys bool - SetAliasTag string - ParserType []ParserType - ZeroEmpty bool -} - // Accepts checks if the specified extensions or content types are acceptable. func (c *DefaultCtx) Accepts(offers ...string) string { if len(offers) == 0 { @@ -259,91 +235,6 @@ func (c *DefaultCtx) Body() []byte { return body } -// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance -var decoderPool = &sync.Pool{New: func() any { - return decoderBuilder(ParserConfig{ - IgnoreUnknownKeys: true, - ZeroEmpty: true, - }) -}} - -// SetParserDecoder allow globally change the option of form decoder, update decoderPool -func SetParserDecoder(parserConfig ParserConfig) { - decoderPool = &sync.Pool{New: func() any { - return decoderBuilder(parserConfig) - }} -} - -func decoderBuilder(parserConfig ParserConfig) any { - decoder := schema.NewDecoder() - decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys) - if parserConfig.SetAliasTag != "" { - decoder.SetAliasTag(parserConfig.SetAliasTag) - } - for _, v := range parserConfig.ParserType { - decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter) - } - decoder.ZeroEmpty(parserConfig.ZeroEmpty) - return decoder -} - -// BodyParser binds the request body to a struct. -// 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 -// If none of the content types above are matched, it will return a ErrUnprocessableEntity error -func (c *DefaultCtx) BodyParser(out any) error { - // Get content-type - ctype := utils.ToLower(utils.UnsafeString(c.fasthttp.Request.Header.ContentType())) - - ctype = utils.ParseVendorSpecificContentType(ctype) - - // Parse body accordingly - if strings.HasPrefix(ctype, MIMEApplicationJSON) { - return c.app.config.JSONDecoder(c.Body(), out) - } - if strings.HasPrefix(ctype, MIMEApplicationForm) { - data := make(map[string][]string) - var err error - - c.fasthttp.PostArgs().VisitAll(func(key, val []byte) { - if err != nil { - return - } - - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if 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]) - } - } else { - data[k] = append(data[k], v) - } - - }) - - return c.parseToStruct(bodyTag, out, data) - } - if strings.HasPrefix(ctype, MIMEMultipartForm) { - data, err := c.fasthttp.MultipartForm() - if err != nil { - return err - } - return c.parseToStruct(bodyTag, out, data.Value) - } - if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) { - return xml.Unmarshal(c.Body(), out) - } - // No suitable content type found - return ErrUnprocessableEntity -} - // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. func (c *DefaultCtx) ClearCookie(key ...string) { @@ -571,30 +462,6 @@ func (c *DefaultCtx) GetRespHeader(key string, defaultValue ...string) string { return defaultString(c.app.getString(c.fasthttp.Response.Header.Peek(key)), defaultValue) } -// GetReqHeaders returns the HTTP request headers. -// Returned value is only valid within the handler. Do not store any references. -// Make copies or use the Immutable setting instead. -func (c *DefaultCtx) GetReqHeaders() map[string]string { - headers := make(map[string]string) - c.Request().Header.VisitAll(func(k, v []byte) { - headers[string(k)] = c.app.getString(v) - }) - - return headers -} - -// GetRespHeaders returns the HTTP response headers. -// Returned value is only valid within the handler. Do not store any references. -// Make copies or use the Immutable setting instead. -func (c *DefaultCtx) GetRespHeaders() map[string]string { - headers := make(map[string]string) - c.Response().Header.VisitAll(func(k, v []byte) { - headers[string(k)] = c.app.getString(v) - }) - - return headers -} - // Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. @@ -805,17 +672,6 @@ func (c *DefaultCtx) Params(key string, defaultValue ...string) string { return defaultString("", defaultValue) } -// Params is used to get all route parameters. -// Using Params method to get params. -func (c *DefaultCtx) AllParams() map[string]string { - params := make(map[string]string, len(c.route.Params)) - for _, param := range c.route.Params { - params[param] = c.Params(param) - } - - return params -} - // ParamsInt is used to get an integer from the route parameters // it defaults to zero if the parameter is not found or if the // parameter cannot be converted to an integer @@ -892,41 +748,6 @@ func (c *DefaultCtx) Query(key string, defaultValue ...string) string { return defaultString(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), defaultValue) } -// QueryParser binds the query string to a struct. -func (c *DefaultCtx) QueryParser(out any) error { - data := make(map[string][]string) - var err error - - c.fasthttp.QueryArgs().VisitAll(func(key, val []byte) { - if err != nil { - return - } - - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if 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]) - } - } else { - data[k] = append(data[k], v) - } - - }) - - if err != nil { - return err - } - - return c.parseToStruct(queryTag, out, data) -} - func parseParamSquareBrackets(k string) (string, error) { bb := bytebufferpool.Get() defer bytebufferpool.Put(bb) @@ -953,84 +774,6 @@ func parseParamSquareBrackets(k string) (string, error) { return bb.String(), nil } -// ReqHeaderParser binds the request header strings to a struct. -func (c *DefaultCtx) ReqHeaderParser(out any) error { - data := make(map[string][]string) - c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if 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]) - } - } else { - data[k] = append(data[k], v) - } - - }) - - return c.parseToStruct(reqHeaderTag, out, data) -} - -func (c *DefaultCtx) parseToStruct(aliasTag string, out any, data map[string][]string) error { - // Get decoder from pool - schemaDecoder := decoderPool.Get().(*schema.Decoder) - defer decoderPool.Put(schemaDecoder) - - // Set alias tag - schemaDecoder.SetAliasTag(aliasTag) - - return schemaDecoder.Decode(out, data) -} - -func equalFieldType(out any, kind reflect.Kind, key string) bool { - // Get type of interface - outTyp := reflect.TypeOf(out).Elem() - key = utils.ToLower(key) - // Must be a struct to match a field - if outTyp.Kind() != reflect.Struct { - return false - } - // Copy interface to an value to be used - outVal := reflect.ValueOf(out).Elem() - // Loop over each field - for i := 0; i < outTyp.NumField(); i++ { - // Get field value data - structField := outVal.Field(i) - // Can this field be changed? - if !structField.CanSet() { - continue - } - // Get field key data - typeField := outTyp.Field(i) - // Get type of field key - structFieldKind := structField.Kind() - // Does the field type equals input? - if structFieldKind != kind { - continue - } - // Get tag from field if exist - inputFieldName := typeField.Tag.Get(queryTag) - if inputFieldName == "" { - inputFieldName = typeField.Name - } else { - inputFieldName = strings.Split(inputFieldName, ",")[0] - } - // Compare field/tag with provided key - if utils.ToLower(inputFieldName) == key { - return true - } - } - return false -} - -var ( - ErrRangeMalformed = errors.New("range: malformed range header string") - ErrRangeUnsatisfiable = errors.New("range: unsatisfiable range") -) - // Range returns a struct containing the type and a slice of ranges. func (c *DefaultCtx) Range(size int) (rangeData Range, err error) { rangeStr := c.Get(HeaderRange) @@ -1095,7 +838,7 @@ func (c *DefaultCtx) Redirect(location string, status ...int) error { // Add vars to default view var map binding to template engine. // Variables are read by the Render method and may be overwritten. -func (c *DefaultCtx) Bind(vars Map) error { +func (c *DefaultCtx) BindVars(vars Map) error { // init viewBindMap - lazy map if c.viewBindMap == nil { c.viewBindMap = dictpool.AcquireDict() @@ -1576,3 +1319,16 @@ func (c *DefaultCtx) IsFromLocal() bool { } return c.isLocalHost(ips[0]) } + +// You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method. +// It gives custom binding support, detailed binding options and more. +// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser +func (c *DefaultCtx) Bind() *Bind { + if c.bind == nil { + c.bind = &Bind{ + ctx: c, + should: true, + } + } + return c.bind +} diff --git a/ctx_interface.go b/ctx_interface.go index 1edc5fe4..b537bae8 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -46,12 +46,6 @@ type Ctx interface { // Make copies or use the Immutable setting instead. Body() []byte - // BodyParser binds the request body to a struct. - // 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 - // If none of the content types above are matched, it will return a ErrUnprocessableEntity error - BodyParser(out any) error - // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. ClearCookie(key ...string) @@ -128,16 +122,6 @@ type Ctx interface { // Make copies or use the Immutable setting instead. GetRespHeader(key string, defaultValue ...string) string - // GetReqHeaders returns the HTTP request headers. - // Returned value is only valid within the handler. Do not store any references. - // Make copies or use the Immutable setting instead. - GetReqHeaders() map[string]string - - // GetRespHeaders returns the HTTP response headers. - // Returned value is only valid within the handler. Do not store any references. - // Make copies or use the Immutable setting instead. - GetRespHeaders() map[string]string - // Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. @@ -206,10 +190,6 @@ type Ctx interface { // Make copies or use the Immutable setting to use the value outside the Handler. Params(key string, defaultValue ...string) string - // Params is used to get all route parameters. - // Using Params method to get params. - AllParams() map[string]string - // ParamsInt is used to get an integer from the route parameters // it defaults to zero if the parameter is not found or if the // parameter cannot be converted to an integer @@ -235,12 +215,6 @@ type Ctx interface { // Make copies or use the Immutable setting to use the value outside the Handler. Query(key string, defaultValue ...string) string - // QueryParser binds the query string to a struct. - QueryParser(out any) error - - // ReqHeaderParser binds the request header strings to a struct. - ReqHeaderParser(out any) error - // Range returns a struct containing the type and a slice of ranges. Range(size int) (rangeData Range, err error) @@ -250,7 +224,7 @@ type Ctx interface { // Add vars to default view var map binding to template engine. // Variables are read by the Render method and may be overwritten. - Bind(vars Map) error + BindVars(vars Map) error // GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" GetRouteURL(routeName string, params Map) (string, error) @@ -350,6 +324,11 @@ type Ctx interface { // Reset is a method to reset context fields by given request when to use server handlers. Reset(fctx *fasthttp.RequestCtx) + // You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method. + // It gives custom binding support, detailed binding options and more. + // Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser + Bind() *Bind + // SetReq resets fields of context that is relating to request. setReq(fctx *fasthttp.RequestCtx) @@ -451,6 +430,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) { func (c *DefaultCtx) release() { c.route = nil c.fasthttp = nil + c.bind = nil if c.viewBindMap != nil { dictpool.ReleaseDict(c.viewBindMap) } diff --git a/ctx_test.go b/ctx_test.go index be1a2118..856ab9cc 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -20,7 +20,6 @@ import ( "net/url" "os" "path/filepath" - "reflect" "strconv" "strings" "testing" @@ -378,229 +377,6 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) { utils.AssertEqual(b, []byte("john=doe"), c.Body()) } -// go test -run Test_Ctx_BodyParser -func Test_Ctx_BodyParser(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `json:"name" xml:"name" form:"name" query:"name"` - } - - { - var gzipJSON bytes.Buffer - w := gzip.NewWriter(&gzipJSON) - _, _ = w.Write([]byte(`{"name":"john"}`)) - _ = w.Close() - - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.Set(HeaderContentEncoding, "gzip") - c.Request().SetBody(gzipJSON.Bytes()) - c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) - d := new(Demo) - utils.AssertEqual(t, nil, c.BodyParser(d)) - utils.AssertEqual(t, "john", d.Name) - c.Request().Header.Del(HeaderContentEncoding) - } - - testDecodeParser := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - utils.AssertEqual(t, nil, c.BodyParser(d)) - utils.AssertEqual(t, "john", d.Name) - } - - testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) - testDecodeParser(MIMEApplicationXML, `john`) - testDecodeParser(MIMEApplicationForm, "name=john") - testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - - testDecodeParserError := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - utils.AssertEqual(t, false, c.BodyParser(nil) == nil) - } - - testDecodeParserError("invalid-content-type", "") - testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b") - - type CollectionQuery struct { - Data []Demo `query:"data"` - } - - c.Request().Reset() - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe")) - c.Request().Header.SetContentLength(len(c.Body())) - cq := new(CollectionQuery) - utils.AssertEqual(t, nil, c.BodyParser(cq)) - utils.AssertEqual(t, 2, len(cq.Data)) - utils.AssertEqual(t, "john", cq.Data[0].Name) - utils.AssertEqual(t, "doe", cq.Data[1].Name) - - c.Request().Reset() - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe")) - c.Request().Header.SetContentLength(len(c.Body())) - cq = new(CollectionQuery) - utils.AssertEqual(t, nil, c.BodyParser(cq)) - utils.AssertEqual(t, 2, len(cq.Data)) - utils.AssertEqual(t, "john", cq.Data[0].Name) - utils.AssertEqual(t, "doe", cq.Data[1].Name) -} - -// go test -run Test_Ctx_BodyParser_WithSetParserDecoder -func Test_Ctx_BodyParser_WithSetParserDecoder(t *testing.T) { - type CustomTime time.Time - - timeConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - customTime := ParserType{ - Customtype: CustomTime{}, - Converter: timeConverter, - } - - SetParserDecoder(ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []ParserType{customTime}, - ZeroEmpty: true, - SetAliasTag: "form", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Date CustomTime `form:"date"` - Title string `form:"title"` - Body string `form:"body"` - } - - testDecodeParser := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - d := Demo{ - Title: "Existing title", - Body: "Existing Body", - } - utils.AssertEqual(t, nil, c.BodyParser(&d)) - date := fmt.Sprintf("%v", d.Date) - utils.AssertEqual(t, "{0 63743587200 }", date) - utils.AssertEqual(t, "", d.Title) - utils.AssertEqual(t, "New Body", d.Body) - } - - testDecodeParser(MIMEApplicationForm, "date=2020-12-15&title=&body=New Body") - testDecodeParser(MIMEMultipartForm+`; boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"date\"\r\n\r\n2020-12-15\r\n--b\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n\r\n--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nNew Body\r\n--b--") -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON -benchmem -count=4 -func Benchmark_Ctx_BodyParser_JSON(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `json:"name"` - } - body := []byte(`{"name":"john"}`) - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.BodyParser(d) - } - utils.AssertEqual(b, nil, c.BodyParser(d)) - utils.AssertEqual(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_XML -benchmem -count=4 -func Benchmark_Ctx_BodyParser_XML(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `xml:"name"` - } - body := []byte("john") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationXML) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.BodyParser(d) - } - utils.AssertEqual(b, nil, c.BodyParser(d)) - utils.AssertEqual(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_Form -benchmem -count=4 -func Benchmark_Ctx_BodyParser_Form(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `form:"name"` - } - body := []byte("name=john") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.BodyParser(d) - } - utils.AssertEqual(b, nil, c.BodyParser(d)) - utils.AssertEqual(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_MultipartForm -benchmem -count=4 -func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `form:"name"` - } - - body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.BodyParser(d) - } - utils.AssertEqual(b, nil, c.BodyParser(d)) - utils.AssertEqual(b, "john", d.Name) -} - // go test -run Test_Ctx_Context func Test_Ctx_Context(t *testing.T) { t.Parallel() @@ -1365,44 +1141,6 @@ func Test_Ctx_Params(t *testing.T) { utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") } -// go test -race -run Test_Ctx_AllParams -func Test_Ctx_AllParams(t *testing.T) { - t.Parallel() - app := New() - app.Get("/test/:user", func(c Ctx) error { - utils.AssertEqual(t, map[string]string{"user": "john"}, c.AllParams()) - return nil - }) - app.Get("/test2/*", func(c Ctx) error { - utils.AssertEqual(t, map[string]string{"*1": "im/a/cookie"}, c.AllParams()) - return nil - }) - app.Get("/test3/*/blafasel/*", func(c Ctx) error { - utils.AssertEqual(t, map[string]string{"*1": "1111", "*2": "2222"}, c.AllParams()) - return nil - }) - app.Get("/test4/:optional?", func(c Ctx) error { - utils.AssertEqual(t, map[string]string{"optional": ""}, c.AllParams()) - return nil - }) - - resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/im/a/cookie", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/1111/blafasel/2222", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") - - resp, err = app.Test(httptest.NewRequest(MethodGet, "/test4", nil)) - utils.AssertEqual(t, nil, err, "app.Test(req)") - utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") -} - // go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4 func Benchmark_Ctx_Params(b *testing.B) { app := New() @@ -1428,32 +1166,6 @@ func Benchmark_Ctx_Params(b *testing.B) { utils.AssertEqual(b, "awesome", res) } -// go test -v -run=^$ -bench=Benchmark_Ctx_AllParams -benchmem -count=4 -func Benchmark_Ctx_AllParams(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - - c.route = &Route{ - Params: []string{ - "param1", "param2", "param3", "param4", - }, - } - c.values = [maxParams]string{ - "john", "doe", "is", "awesome", - } - var res map[string]string - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - res = c.AllParams() - } - utils.AssertEqual(b, map[string]string{"param1": "john", - "param2": "doe", - "param3": "is", - "param4": "awesome"}, - res) -} - // go test -run Test_Ctx_Path func Test_Ctx_Path(t *testing.T) { t.Parallel() @@ -2444,13 +2156,13 @@ func Test_Ctx_RenderWithLocals(t *testing.T) { } -func Test_Ctx_RenderWithBind(t *testing.T) { +func Test_Ctx_RenderWithBindVars(t *testing.T) { t.Parallel() app := New() c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Bind(Map{ + c.BindVars(Map{ "Title": "Hello, World!", }) @@ -2465,7 +2177,7 @@ func Test_Ctx_RenderWithBind(t *testing.T) { } -func Test_Ctx_RenderWithBindLocals(t *testing.T) { +func Test_Ctx_RenderWithBindVarsLocals(t *testing.T) { t.Parallel() app := New(Config{ @@ -2474,7 +2186,7 @@ func Test_Ctx_RenderWithBindLocals(t *testing.T) { c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Bind(Map{ + c.BindVars(Map{ "Title": "Hello, World!", }) @@ -2507,7 +2219,7 @@ func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) { utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) } -func Benchmark_Ctx_RenderWithLocalsAndBinding(b *testing.B) { +func Benchmark_Ctx_RenderWithLocalsAndBindVars(b *testing.B) { engine := &testTemplateEngine{} err := engine.Load() utils.AssertEqual(b, nil, err) @@ -2517,7 +2229,7 @@ func Benchmark_Ctx_RenderWithLocalsAndBinding(b *testing.B) { }) c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Bind(Map{ + c.BindVars(Map{ "Title": "Hello, World!", }) c.Locals("Summary", "Test") @@ -2603,7 +2315,7 @@ func Benchmark_Ctx_RenderLocals(b *testing.B) { utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) } -func Benchmark_Ctx_RenderBind(b *testing.B) { +func Benchmark_Ctx_RenderBindVars(b *testing.B) { engine := &testTemplateEngine{} err := engine.Load() utils.AssertEqual(b, nil, err) @@ -2611,7 +2323,7 @@ func Benchmark_Ctx_RenderBind(b *testing.B) { app.config.Views = engine c := app.NewCtx(&fasthttp.RequestCtx{}) - c.Bind(Map{ + c.BindVars(Map{ "Title": "Hello, World!", }) @@ -3150,569 +2862,6 @@ func Benchmark_Ctx_SendString_B(b *testing.B) { utils.AssertEqual(b, []byte("Hello, world!"), c.Response().Body()) } -// go test -run Test_Ctx_QueryParser -v -func Test_Ctx_QueryParser(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := new(Query) - utils.AssertEqual(t, nil, c.QueryParser(q)) - utils.AssertEqual(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = new(Query) - utils.AssertEqual(t, nil, c.QueryParser(q)) - utils.AssertEqual(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = new(Query) - utils.AssertEqual(t, nil, c.QueryParser(q)) - utils.AssertEqual(t, 3, len(q.Hobby)) - - empty := new(Query) - c.Request().URI().SetQueryString("") - utils.AssertEqual(t, nil, c.QueryParser(empty)) - utils.AssertEqual(t, 0, len(empty.Hobby)) - - type Query2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") - q2 := new(Query2) - q2.Bool = true - q2.Name = "hello world" - utils.AssertEqual(t, nil, c.QueryParser(q2)) - utils.AssertEqual(t, "basketball,football", q2.Hobby) - utils.AssertEqual(t, true, q2.Bool) - utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten - utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) - var nilSlice []string - utils.AssertEqual(t, nilSlice, q2.Empty) - utils.AssertEqual(t, []string{""}, q2.Alloc) - utils.AssertEqual(t, []int64{1}, q2.No) - - type RequiredQuery struct { - Name string `query:"name,required"` - } - rq := new(RequiredQuery) - c.Request().URI().SetQueryString("") - utils.AssertEqual(t, "name is empty", c.QueryParser(rq).Error()) - - type ArrayQuery struct { - Data []string - } - aq := new(ArrayQuery) - c.Request().URI().SetQueryString("data[]=john&data[]=doe") - utils.AssertEqual(t, nil, c.QueryParser(aq)) - utils.AssertEqual(t, 2, len(aq.Data)) -} - -// go test -run Test_Ctx_QueryParser_WithSetParserDecoder -v -func Test_Ctx_QueryParser_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time - - NonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - nonRFCTime := ParserType{ - Customtype: NonRFCTime{}, - Converter: NonRFCConverter, - } - - SetParserDecoder(ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "query", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type NonRFCTimeInput struct { - Date NonRFCTime `query:"date"` - Title string `query:"title"` - Body string `query:"body"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - q := new(NonRFCTimeInput) - - c.Request().URI().SetQueryString("date=2021-04-10&title=CustomDateTest&Body=October") - utils.AssertEqual(t, nil, c.QueryParser(q)) - fmt.Println(q.Date, "q.Date") - utils.AssertEqual(t, "CustomDateTest", q.Title) - date := fmt.Sprintf("%v", q.Date) - utils.AssertEqual(t, "{0 63753609600 }", date) - utils.AssertEqual(t, "October", q.Body) - - c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October") - q = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - utils.AssertEqual(t, nil, c.QueryParser(q)) - utils.AssertEqual(t, "", q.Title) -} - -// go test -run Test_Ctx_QueryParser_Schema -v -func Test_Ctx_QueryParser_Schema(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query1 struct { - Name string `query:"name,required"` - Nested struct { - Age int `query:"age"` - } `query:"nested,required"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q := new(Query1) - utils.AssertEqual(t, nil, c.QueryParser(q)) - - c.Request().URI().SetQueryString("namex=tom&nested.age=10") - q = new(Query1) - utils.AssertEqual(t, "name is empty", c.QueryParser(q).Error()) - - c.Request().URI().SetQueryString("name=tom&nested.agex=10") - q = new(Query1) - utils.AssertEqual(t, nil, c.QueryParser(q)) - - c.Request().URI().SetQueryString("name=tom&test.age=10") - q = new(Query1) - utils.AssertEqual(t, "nested is empty", c.QueryParser(q).Error()) - - type Query2 struct { - Name string `query:"name"` - Nested struct { - Age int `query:"age,required"` - } `query:"nested"` - } - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q2 := new(Query2) - utils.AssertEqual(t, nil, c.QueryParser(q2)) - - c.Request().URI().SetQueryString("nested.age=10") - q2 = new(Query2) - utils.AssertEqual(t, nil, c.QueryParser(q2)) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - utils.AssertEqual(t, "nested.age is empty", c.QueryParser(q2).Error()) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - utils.AssertEqual(t, "nested.age is empty", c.QueryParser(q2).Error()) - - type Node struct { - Value int `query:"val,required"` - Next *Node `query:"next,required"` - } - c.Request().URI().SetQueryString("val=1&next.val=3") - n := new(Node) - utils.AssertEqual(t, nil, c.QueryParser(n)) - utils.AssertEqual(t, 1, n.Value) - utils.AssertEqual(t, 3, n.Next.Value) - - c.Request().URI().SetQueryString("next.val=2") - n = new(Node) - utils.AssertEqual(t, "val is empty", c.QueryParser(n).Error()) - - c.Request().URI().SetQueryString("val=3&next.value=2") - n = new(Node) - n.Next = new(Node) - utils.AssertEqual(t, nil, c.QueryParser(n)) - utils.AssertEqual(t, 3, n.Value) - utils.AssertEqual(t, 0, n.Next.Value) - - type Person struct { - Name string `query:"name"` - Age int `query:"age"` - } - - type CollectionQuery struct { - Data []Person `query:"data"` - } - - c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12") - cq := new(CollectionQuery) - utils.AssertEqual(t, nil, c.QueryParser(cq)) - utils.AssertEqual(t, 2, len(cq.Data)) - utils.AssertEqual(t, "john", cq.Data[0].Name) - utils.AssertEqual(t, 10, cq.Data[0].Age) - utils.AssertEqual(t, "doe", cq.Data[1].Name) - utils.AssertEqual(t, 12, cq.Data[1].Age) - - c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") - cq = new(CollectionQuery) - utils.AssertEqual(t, nil, c.QueryParser(cq)) - utils.AssertEqual(t, 2, len(cq.Data)) - utils.AssertEqual(t, "john", cq.Data[0].Name) - utils.AssertEqual(t, 10, cq.Data[0].Age) - utils.AssertEqual(t, "doe", cq.Data[1].Name) - utils.AssertEqual(t, 12, cq.Data[1].Age) -} - -// go test -run Test_Ctx_ReqHeaderParser -v -func Test_Ctx_ReqHeaderParser(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Header struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - q := new(Header) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - utils.AssertEqual(t, 2, len(q.Hobby)) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - utils.AssertEqual(t, 3, len(q.Hobby)) - - empty := new(Header) - c.Request().Header.Del("hobby") - utils.AssertEqual(t, nil, c.QueryParser(empty)) - utils.AssertEqual(t, 0, len(empty.Hobby)) - - type Header2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Request().Header.Add("id", "2") - c.Request().Header.Add("Name", "Jane Doe") - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "go,fiber") - c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi") - c.Request().Header.Add("alloc", "") - c.Request().Header.Add("no", "1") - - h2 := new(Header2) - h2.Bool = true - h2.Name = "hello world" - utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) - utils.AssertEqual(t, "go,fiber", h2.Hobby) - utils.AssertEqual(t, true, h2.Bool) - utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten - utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - utils.AssertEqual(t, nilSlice, h2.Empty) - utils.AssertEqual(t, []string{""}, h2.Alloc) - utils.AssertEqual(t, []int64{1}, h2.No) - - type RequiredHeader struct { - Name string `reqHeader:"name,required"` - } - rh := new(RequiredHeader) - c.Request().Header.Del("name") - utils.AssertEqual(t, "name is empty", c.ReqHeaderParser(rh).Error()) -} - -// go test -run Test_Ctx_ReqHeaderParser_WithSetParserDecoder -v -func Test_Ctx_ReqHeaderParser_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time - - NonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - nonRFCTime := ParserType{ - Customtype: NonRFCTime{}, - Converter: NonRFCConverter, - } - - SetParserDecoder(ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "req", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type NonRFCTimeInput struct { - Date NonRFCTime `req:"date"` - Title string `req:"title"` - Body string `req:"body"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - r := new(NonRFCTimeInput) - - c.Request().Header.Add("Date", "2021-04-10") - c.Request().Header.Add("Title", "CustomDateTest") - c.Request().Header.Add("Body", "October") - - utils.AssertEqual(t, nil, c.ReqHeaderParser(r)) - fmt.Println(r.Date, "q.Date") - utils.AssertEqual(t, "CustomDateTest", r.Title) - date := fmt.Sprintf("%v", r.Date) - utils.AssertEqual(t, "{0 63753609600 }", date) - utils.AssertEqual(t, "October", r.Body) - - c.Request().Header.Add("Title", "") - r = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - utils.AssertEqual(t, nil, c.ReqHeaderParser(r)) - utils.AssertEqual(t, "", r.Title) -} - -// go test -run Test_Ctx_ReqHeaderParser_Schema -v -func Test_Ctx_ReqHeaderParser_Schema(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Header1 struct { - Name string `reqHeader:"Name,required"` - Nested struct { - Age int `reqHeader:"Age"` - } `reqHeader:"Nested,required"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - q := new(Header1) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - - c.Request().Header.Del("Name") - q = new(Header1) - utils.AssertEqual(t, "Name is empty", c.ReqHeaderParser(q).Error()) - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Del("Nested.Age") - c.Request().Header.Add("Nested.Agex", "10") - q = new(Header1) - utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) - - c.Request().Header.Del("Nested.Agex") - q = new(Header1) - utils.AssertEqual(t, "Nested is empty", c.ReqHeaderParser(q).Error()) - - c.Request().Header.Del("Nested.Agex") - c.Request().Header.Del("Name") - - type Header2 struct { - Name string `reqHeader:"Name"` - Nested struct { - Age int `reqHeader:"age,required"` - } `reqHeader:"Nested"` - } - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - - h2 := new(Header2) - utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) - - c.Request().Header.Del("Name") - h2 = new(Header2) - utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) - - c.Request().Header.Del("Name") - c.Request().Header.Del("Nested.Age") - c.Request().Header.Add("Nested.Agex", "10") - h2 = new(Header2) - utils.AssertEqual(t, "Nested.age is empty", c.ReqHeaderParser(h2).Error()) - - type Node struct { - Value int `reqHeader:"Val,required"` - Next *Node `reqHeader:"Next,required"` - } - c.Request().Header.Add("Val", "1") - c.Request().Header.Add("Next.Val", "3") - n := new(Node) - utils.AssertEqual(t, nil, c.ReqHeaderParser(n)) - utils.AssertEqual(t, 1, n.Value) - utils.AssertEqual(t, 3, n.Next.Value) - - c.Request().Header.Del("Val") - n = new(Node) - utils.AssertEqual(t, "Val is empty", c.ReqHeaderParser(n).Error()) - - c.Request().Header.Add("Val", "3") - c.Request().Header.Del("Next.Val") - c.Request().Header.Add("Next.Value", "2") - n = new(Node) - n.Next = new(Node) - utils.AssertEqual(t, nil, c.ReqHeaderParser(n)) - utils.AssertEqual(t, 3, n.Value) - utils.AssertEqual(t, 0, n.Next.Value) -} - -func Test_Ctx_EqualFieldType(t *testing.T) { - var out int - utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key")) - - var dummy struct{ f string } - utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key")) - - var dummy2 struct{ f string } - utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f")) - - var user struct { - Name string - Address string `query:"address"` - Age int `query:"AGE"` - } - utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name")) - utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name")) - utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address")) - utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address")) - utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE")) - utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age")) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4 -func Benchmark_Ctx_QueryParser(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := new(Query) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.QueryParser(q) - } - utils.AssertEqual(b, nil, c.QueryParser(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_parseQuery -benchmem -count=4 -func Benchmark_Ctx_parseQuery(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Person struct { - Name string `query:"name"` - Age int `query:"age"` - } - - type CollectionQuery struct { - Data []Person `query:"data"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10") - cq := new(CollectionQuery) - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.QueryParser(cq) - } - - utils.AssertEqual(b, nil, c.QueryParser(cq)) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser_Comma -benchmem -count=4 -func Benchmark_Ctx_QueryParser_Comma(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - // c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q := new(Query) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.QueryParser(q) - } - utils.AssertEqual(b, nil, c.QueryParser(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParser -benchmem -count=4 -func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type ReqHeader struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - - q := new(ReqHeader) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.ReqHeaderParser(q) - } - utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) -} - // go test -run Test_Ctx_BodyStreamWriter func Test_Ctx_BodyStreamWriter(t *testing.T) { t.Parallel() @@ -3876,38 +3025,6 @@ func Test_Ctx_GetRespHeader(t *testing.T) { utils.AssertEqual(t, c.GetRespHeader(HeaderContentType), "application/json") } -// go test -run Test_Ctx_GetRespHeaders -func Test_Ctx_GetRespHeaders(t *testing.T) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Set("test", "Hello, World πŸ‘‹!") - c.Set("foo", "bar") - c.Response().Header.Set(HeaderContentType, "application/json") - - utils.AssertEqual(t, c.GetRespHeaders(), map[string]string{ - "Content-Type": "application/json", - "Foo": "bar", - "Test": "Hello, World πŸ‘‹!", - }) -} - -// go test -run Test_Ctx_GetReqHeaders -func Test_Ctx_GetReqHeaders(t *testing.T) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().Header.Set("test", "Hello, World πŸ‘‹!") - c.Request().Header.Set("foo", "bar") - c.Request().Header.Set(HeaderContentType, "application/json") - - utils.AssertEqual(t, c.GetReqHeaders(), map[string]string{ - "Content-Type": "application/json", - "Foo": "bar", - "Test": "Hello, World πŸ‘‹!", - }) -} - // go test -run Test_Ctx_IsFromLocal func Test_Ctx_IsFromLocal(t *testing.T) { t.Parallel() diff --git a/error.go b/error.go index a1b0d1e7..1de31cc7 100644 --- a/error.go +++ b/error.go @@ -2,10 +2,21 @@ package fiber import ( errors "encoding/json" + goErrors "errors" "github.com/gofiber/fiber/v3/internal/schema" ) +// Range errors +var ( + ErrRangeMalformed = goErrors.New("range: malformed range header string") + ErrRangeUnsatisfiable = goErrors.New("range: unsatisfiable range") +) + +// Binder errors +var ErrCustomBinderNotFound = goErrors.New("binder: custom binder not found, please be sure to enter the right name!") + +// gorilla/schema errors type ( // Conversion error exposes the internal schema.ConversionError for public use. ConversionError = schema.ConversionError @@ -17,6 +28,7 @@ type ( MultiError = schema.MultiError ) +// encoding/json errors type ( // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. // (The argument to Unmarshal must be a non-nil pointer.) diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 2bb2f007..8049081c 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -247,8 +247,13 @@ func New(config ...Config) fiber.Handler { case TagResBody: return buf.Write(c.Response().Body()) case TagReqHeaders: + out := make(map[string]string, 0) + if err := c.Bind().Header(&out); err != nil { + return 0, err + } + reqHeaders := make([]string, 0) - for k, v := range c.GetReqHeaders() { + for k, v := range out { reqHeaders = append(reqHeaders, k+"="+v) } return buf.Write([]byte(strings.Join(reqHeaders, "&")))