diff --git a/app.go b/app.go index 370cc5b2e3..378a3426b9 100644 --- a/app.go +++ b/app.go @@ -10,6 +10,8 @@ package fiber import ( "bufio" "bytes" + "encoding/json" + "encoding/xml" "errors" "fmt" "net" @@ -22,9 +24,6 @@ import ( "sync/atomic" "time" - "encoding/json" - "encoding/xml" - "github.com/gofiber/fiber/v3/utils" "github.com/valyala/fasthttp" ) @@ -116,8 +115,6 @@ type App struct { latestGroup *Group // newCtxFunc newCtxFunc func(app *App) CustomCtx - // custom binders - customBinders []CustomBinder // TLS handler tlsHandler *tlsHandler } @@ -375,12 +372,6 @@ type Config struct { // // Optional. Default: DefaultColors ColorScheme Colors `json:"color_scheme"` - - // 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. @@ -469,13 +460,12 @@ 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{}, - customBinders: []CustomBinder{}, + config: Config{}, + getBytes: utils.UnsafeBytes, + getString: utils.UnsafeString, + appList: make(map[string]*App), + latestRoute: &Route{}, + latestGroup: &Group{}, } // Create Ctx pool @@ -569,12 +559,6 @@ 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 deleted file mode 100644 index b390db2fda..0000000000 --- a/bind.go +++ /dev/null @@ -1,194 +0,0 @@ -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 NewError(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 deleted file mode 100644 index 090f76db04..0000000000 --- a/bind_test.go +++ /dev/null @@ -1,1544 +0,0 @@ -package fiber - -import ( - "bytes" - "compress/gzip" - "encoding/json" - "errors" - "fmt" - "net/http/httptest" - "reflect" - "testing" - "time" - - "github.com/gofiber/fiber/v3/binder" - "github.com/stretchr/testify/require" - "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) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = new(Query) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = new(Query) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Query) - c.Request().URI().SetQueryString("") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(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" - require.Nil(t, c.Bind().Query(q2)) - require.Equal(t, "basketball,football", q2.Hobby) - require.True(t, q2.Bool) - require.Equal(t, "tom", q2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, q2.Empty) - require.Equal(t, []string{""}, q2.Alloc) - require.Equal(t, []int64{1}, q2.No) - - type RequiredQuery struct { - Name string `query:"name,required"` - } - rq := new(RequiredQuery) - c.Request().URI().SetQueryString("") - require.Equal(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") - require.Nil(t, c.Bind().Query(aq)) - require.Equal(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) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 2, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = make(map[string][]string) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 2, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = make(map[string][]string) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 3, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer") - qq := make(map[string]string) - require.Nil(t, c.Bind().Query(&qq)) - require.Equal(t, "1", qq["id"]) - - empty := make(map[string][]string) - c.Request().URI().SetQueryString("") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(t, 0, len(empty["hobby"])) - - em := make(map[string][]int) - c.Request().URI().SetQueryString("") - require.Equal(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") - require.Nil(t, c.Bind().Query(q)) - fmt.Println(q.Date, "q.Date") - require.Equal(t, "CustomDateTest", q.Title) - date := fmt.Sprintf("%v", q.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", q.Body) - - c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October") - q = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Query(q)) - require.Equal(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) - require.Nil(t, c.Bind().Query(q)) - - c.Request().URI().SetQueryString("namex=tom&nested.age=10") - q = new(Query1) - require.Equal(t, "name is empty", c.Bind().Query(q).Error()) - - c.Request().URI().SetQueryString("name=tom&nested.agex=10") - q = new(Query1) - require.Nil(t, c.Bind().Query(q)) - - c.Request().URI().SetQueryString("name=tom&test.age=10") - q = new(Query1) - require.Equal(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) - require.Nil(t, c.Bind().Query(q2)) - - c.Request().URI().SetQueryString("nested.age=10") - q2 = new(Query2) - require.Nil(t, c.Bind().Query(q2)) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(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) - require.Nil(t, c.Bind().Query(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().URI().SetQueryString("next.val=2") - n = new(Node) - require.Equal(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) - require.Nil(t, c.Bind().Query(n)) - require.Equal(t, 3, n.Value) - require.Equal(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) - require.Nil(t, c.Bind().Query(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, 10, cq.Data[0].Age) - require.Equal(t, "doe", cq.Data[1].Name) - require.Equal(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) - require.Nil(t, c.Bind().Query(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, 10, cq.Data[0].Age) - require.Equal(t, "doe", cq.Data[1].Name) - require.Equal(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) - require.Nil(t, c.Bind().Header(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.Nil(t, c.Bind().Header(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Header) - c.Request().Header.Del("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(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" - require.Nil(t, c.Bind().Header(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredHeader struct { - Name string `header:"name,required"` - } - rh := new(RequiredHeader) - c.Request().Header.Del("name") - require.Equal(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) - require.Nil(t, c.Bind().Header(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.Nil(t, c.Bind().Header(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string, 0) - c.Request().Header.Del("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(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") - - require.Nil(t, c.Bind().Header(r)) - fmt.Println(r.Date, "q.Date") - require.Equal(t, "CustomDateTest", r.Title) - date := fmt.Sprintf("%v", r.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", r.Body) - - c.Request().Header.Add("Title", "") - r = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Header(r)) - require.Equal(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) - require.Nil(t, c.Bind().Header(q)) - - c.Request().Header.Del("Name") - q = new(Header1) - require.Equal(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) - require.Nil(t, c.Bind().Header(q)) - - c.Request().Header.Del("Nested.Agex") - q = new(Header1) - require.Equal(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) - require.Nil(t, c.Bind().Header(h2)) - - c.Request().Header.Del("Name") - h2 = new(Header2) - require.Nil(t, 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) - require.Equal(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) - require.Nil(t, c.Bind().Header(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().Header.Del("Val") - n = new(Node) - require.Equal(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) - require.Nil(t, c.Bind().Header(n)) - require.Equal(t, 3, n.Value) - require.Equal(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) - require.Nil(t, c.Bind().RespHeader(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.Nil(t, c.Bind().RespHeader(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Header) - c.Response().Header.Del("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(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" - require.Nil(t, c.Bind().RespHeader(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredHeader struct { - Name string `respHeader:"name,required"` - } - rh := new(RequiredHeader) - c.Response().Header.Del("name") - require.Equal(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) - require.Nil(t, c.Bind().RespHeader(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.Nil(t, c.Bind().RespHeader(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string, 0) - c.Response().Header.Del("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - } - - require.Nil(b, 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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - require.Nil(t, c.Bind().Body(d)) - require.Equal(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) - require.Nil(t, c.Bind().Body(d)) - require.Equal(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)) - require.False(t, 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) - require.Nil(t, c.Bind().Body(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(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) - require.Nil(t, c.Bind().Body(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(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", - } - require.Nil(t, c.Bind().Body(&d)) - date := fmt.Sprintf("%v", d.Date) - require.Equal(t, "{0 63743587200 }", date) - require.Equal(t, "", d.Title) - require.Equal(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) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(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) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4 -func Benchmark_Bind_Body_Form(b *testing.B) { - 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) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(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) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(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) - } - require.Nil(b, c.Bind().Body(&d)) - require.Equal(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) - } - require.Equal(t, uint(111), d.UserID) - require.Equal(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) - } - require.Equal(t, uint(111), d["userId"]) - require.Equal(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) - } - - require.Equal(b, "john", res.Param1) - require.Equal(b, "doe", res.Param2) - require.Equal(b, "is", res.Param3) - require.Equal(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) - } - - require.Equal(b, "john", res["param1"]) - require.Equal(b, "doe", res["param2"]) - require.Equal(b, "is", res["param3"]) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = new(Cookie) - require.Nil(t, c.Bind().Cookie(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Cookie) - c.Request().Header.DelCookie("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(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" - require.Nil(t, c.Bind().Cookie(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredCookie struct { - Name string `cookie:"name,required"` - } - rh := new(RequiredCookie) - c.Request().Header.DelCookie("name") - require.Equal(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) - require.Nil(t, c.Bind().Cookie(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = make(map[string][]string) - require.Nil(t, c.Bind().Cookie(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string) - c.Request().Header.DelCookie("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(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") - - require.Nil(t, c.Bind().Cookie(r)) - fmt.Println(r.Date, "q.Date") - require.Equal(t, "CustomDateTest", r.Title) - date := fmt.Sprintf("%v", r.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", r.Body) - - c.Request().Header.SetCookie("Title", "") - r = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Cookie(r)) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(q)) - - c.Request().Header.DelCookie("Name") - q = new(Cookie1) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(q)) - - c.Request().Header.DelCookie("Nested.Agex") - q = new(Cookie1) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(h2)) - - c.Request().Header.DelCookie("Name") - h2 = new(Cookie2) - require.Nil(t, 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) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().Header.DelCookie("Val") - n = new(Node) - require.Equal(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) - require.Nil(t, c.Bind().Cookie(n)) - require.Equal(t, 3, n.Value) - require.Equal(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) - } - require.Nil(b, 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) - } - require.Nil(b, 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) - - require.Nil(t, c.Bind().Body(d)) - require.Nil(t, c.Bind().Custom("custom", d)) - require.Equal(t, ErrCustomBinderNotFound, c.Bind().Custom("not_custom", d)) - require.Equal(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) - require.Equal(t, StatusBadRequest, c.Response().StatusCode()) - require.Equal(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") - require.Equal(t, "you should have entered right name!", c.Bind().Query(rq).Error()) - - rq = new(simpleQuery) - c.Request().URI().SetQueryString("name=john") - require.Nil(t, c.Bind().Query(rq)) -} diff --git a/binder/README.md b/binder/README.md deleted file mode 100644 index d40cc7e54e..0000000000 --- a/binder/README.md +++ /dev/null @@ -1,194 +0,0 @@ -# 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 deleted file mode 100644 index d393179011..0000000000 --- a/binder/binder.go +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index e761e4776c..0000000000 --- a/binder/cookie.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 24983ccdea..0000000000 --- a/binder/form.go +++ /dev/null @@ -1,53 +0,0 @@ -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 deleted file mode 100644 index 688a81136a..0000000000 --- a/binder/header.go +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 570a7f9b79..0000000000 --- a/binder/json.go +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index bec5634808..0000000000 --- a/binder/mapping.go +++ /dev/null @@ -1,199 +0,0 @@ -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 - if ptrVal.Kind() == reflect.Ptr { - ptrVal = ptrVal.Elem() - } - - // Parse into the map - if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String { - return parseToMap(ptrVal.Interface(), 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 deleted file mode 100644 index aec91ff2be..0000000000 --- a/binder/mapping_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package binder - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -func Test_EqualFieldType(t *testing.T) { - var out int - require.False(t, equalFieldType(&out, reflect.Int, "key")) - - var dummy struct{ f string } - require.False(t, equalFieldType(&dummy, reflect.String, "key")) - - var dummy2 struct{ f string } - require.False(t, equalFieldType(&dummy2, reflect.String, "f")) - - var user struct { - Name string - Address string `query:"address"` - Age int `query:"AGE"` - } - require.True(t, equalFieldType(&user, reflect.String, "name")) - require.True(t, equalFieldType(&user, reflect.String, "Name")) - require.True(t, equalFieldType(&user, reflect.String, "address")) - require.True(t, equalFieldType(&user, reflect.String, "Address")) - require.True(t, equalFieldType(&user, reflect.Int, "AGE")) - require.True(t, equalFieldType(&user, reflect.Int, "age")) -} diff --git a/binder/query.go b/binder/query.go deleted file mode 100644 index ce62e09d0f..0000000000 --- a/binder/query.go +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 2b31710d24..0000000000 --- a/binder/resp_header.go +++ /dev/null @@ -1,34 +0,0 @@ -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 deleted file mode 100644 index 2759f7b464..0000000000 --- a/binder/uri.go +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 29401abb77..0000000000 --- a/binder/xml.go +++ /dev/null @@ -1,15 +0,0 @@ -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 06fc2e67a1..9f88d1f9ed 100644 --- a/ctx.go +++ b/ctx.go @@ -51,7 +51,6 @@ 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 } // tlsHandle object @@ -1320,16 +1319,3 @@ 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 9f24a4c4b1..d18ae3d18b 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -329,11 +329,6 @@ 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 - // ClientHelloInfo return CHI from context ClientHelloInfo() *tls.ClientHelloInfo @@ -438,7 +433,6 @@ 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/error.go b/error.go index 87a6af38c9..d6aee39d99 100644 --- a/error.go +++ b/error.go @@ -1,10 +1,7 @@ package fiber import ( - errors "encoding/json" goErrors "errors" - - "github.com/gofiber/fiber/v3/internal/schema" ) // Range errors @@ -12,41 +9,3 @@ 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 - // UnknownKeyError error exposes the internal schema.UnknownKeyError for public use. - UnknownKeyError = schema.UnknownKeyError - // EmptyFieldError error exposes the internal schema.EmptyFieldError for public use. - EmptyFieldError = schema.EmptyFieldError - // MultiError error exposes the internal schema.MultiError for public use. - 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.) - InvalidUnmarshalError = errors.InvalidUnmarshalError - - // A MarshalerError represents an error from calling a MarshalJSON or MarshalText method. - MarshalerError = errors.MarshalerError - - // A SyntaxError is a description of a JSON syntax error. - SyntaxError = errors.SyntaxError - - // An UnmarshalTypeError describes a JSON value that was - // not appropriate for a value of a specific Go type. - UnmarshalTypeError = errors.UnmarshalTypeError - - // An UnsupportedTypeError is returned by Marshal when attempting - // to encode an unsupported value type. - UnsupportedTypeError = errors.UnsupportedTypeError - - UnsupportedValueError = errors.UnsupportedValueError -) diff --git a/error_test.go b/error_test.go deleted file mode 100644 index 7fce3c12aa..0000000000 --- a/error_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package fiber - -import ( - "errors" - "testing" - - jerrors "encoding/json" - - "github.com/gofiber/fiber/v3/internal/schema" - "github.com/stretchr/testify/require" -) - -func TestConversionError(t *testing.T) { - ok := errors.As(ConversionError{}, &schema.ConversionError{}) - require.True(t, ok) -} - -func TestUnknownKeyError(t *testing.T) { - ok := errors.As(UnknownKeyError{}, &schema.UnknownKeyError{}) - require.True(t, ok) -} - -func TestEmptyFieldError(t *testing.T) { - ok := errors.As(EmptyFieldError{}, &schema.EmptyFieldError{}) - require.True(t, ok) -} - -func TestMultiError(t *testing.T) { - ok := errors.As(MultiError{}, &schema.MultiError{}) - require.True(t, ok) -} - -func TestInvalidUnmarshalError(t *testing.T) { - var e *jerrors.InvalidUnmarshalError - ok := errors.As(&InvalidUnmarshalError{}, &e) - require.True(t, ok) -} - -func TestMarshalerError(t *testing.T) { - var e *jerrors.MarshalerError - ok := errors.As(&MarshalerError{}, &e) - require.True(t, ok) -} - -func TestSyntaxError(t *testing.T) { - var e *jerrors.SyntaxError - ok := errors.As(&SyntaxError{}, &e) - require.True(t, ok) -} - -func TestUnmarshalTypeError(t *testing.T) { - var e *jerrors.UnmarshalTypeError - ok := errors.As(&UnmarshalTypeError{}, &e) - require.True(t, ok) -} - -func TestUnsupportedTypeError(t *testing.T) { - var e *jerrors.UnsupportedTypeError - ok := errors.As(&UnsupportedTypeError{}, &e) - require.True(t, ok) -} - -func TestUnsupportedValeError(t *testing.T) { - var e *jerrors.UnsupportedValueError - ok := errors.As(&UnsupportedValueError{}, &e) - require.True(t, ok) -} diff --git a/internal/schema/LICENSE b/internal/schema/LICENSE deleted file mode 100644 index 0e5fb87280..0000000000 --- a/internal/schema/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/schema/cache.go b/internal/schema/cache.go deleted file mode 100644 index bf21697cf1..0000000000 --- a/internal/schema/cache.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "errors" - "reflect" - "strconv" - "strings" - "sync" -) - -var errInvalidPath = errors.New("schema: invalid path") - -// newCache returns a new cache. -func newCache() *cache { - c := cache{ - m: make(map[reflect.Type]*structInfo), - regconv: make(map[reflect.Type]Converter), - tag: "schema", - } - return &c -} - -// cache caches meta-data about a struct. -type cache struct { - l sync.RWMutex - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string -} - -// registerConverter registers a converter function for a custom type. -func (c *cache) registerConverter(value interface{}, converterFunc Converter) { - c.regconv[reflect.TypeOf(value)] = converterFunc -} - -// parsePath parses a path in dotted notation verifying that it is a valid -// path to a struct field. -// -// It returns "path parts" which contain indices to fields to be used by -// reflect.Value.FieldByString(). Multiple parts are required for slices of -// structs. -func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { - var struc *structInfo - var field *fieldInfo - var index64 int64 - var err error - parts := make([]pathPart, 0) - path := make([]string, 0) - keys := strings.Split(p, ".") - for i := 0; i < len(keys); i++ { - if t.Kind() != reflect.Struct { - return nil, errInvalidPath - } - if struc = c.get(t); struc == nil { - return nil, errInvalidPath - } - if field = struc.get(keys[i]); field == nil { - return nil, errInvalidPath - } - // Valid field. Append index. - path = append(path, field.name) - if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { - // Parse a special case: slices of structs. - // i+1 must be the slice index. - // - // Now that struct can implements TextUnmarshaler interface, - // we don't need to force the struct's fields to appear in the path. - // So checking i+2 is not necessary anymore. - i++ - if i+1 > len(keys) { - return nil, errInvalidPath - } - if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { - return nil, errInvalidPath - } - parts = append(parts, pathPart{ - path: path, - field: field, - index: int(index64), - }) - path = make([]string, 0) - - // Get the next struct type, dropping ptrs. - if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - if t.Kind() == reflect.Slice { - t = t.Elem() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - } - } else if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - } - // Add the remaining. - parts = append(parts, pathPart{ - path: path, - field: field, - index: -1, - }) - return parts, nil -} - -// get returns a cached structInfo, creating it if necessary. -func (c *cache) get(t reflect.Type) *structInfo { - c.l.RLock() - info := c.m[t] - c.l.RUnlock() - if info == nil { - info = c.create(t, "") - c.l.Lock() - c.m[t] = info - c.l.Unlock() - } - return info -} - -// create creates a structInfo with meta-data about a struct. -func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { - info := &structInfo{} - var anonymousInfos []*structInfo - for i := 0; i < t.NumField(); i++ { - if f := c.createField(t.Field(i), parentAlias); f != nil { - info.fields = append(info.fields, f) - if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { - anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) - } - } - } - for i, a := range anonymousInfos { - others := []*structInfo{info} - others = append(others, anonymousInfos[:i]...) - others = append(others, anonymousInfos[i+1:]...) - for _, f := range a.fields { - if !containsAlias(others, f.alias) { - info.fields = append(info.fields, f) - } - } - } - return info -} - -// createField creates a fieldInfo for the given field. -func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { - alias, options := fieldAlias(field, c.tag) - if alias == "-" { - // Ignore this field. - return nil - } - canonicalAlias := alias - if parentAlias != "" { - canonicalAlias = parentAlias + "." + alias - } - // Check if the type is supported and don't cache it if not. - // First let's get the basic type. - isSlice, isStruct := false, false - ft := field.Type - m := isTextUnmarshaler(reflect.Zero(ft)) - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - if isSlice = ft.Kind() == reflect.Slice; isSlice { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if ft.Kind() == reflect.Array { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if isStruct = ft.Kind() == reflect.Struct; !isStruct { - if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { - // Type is not supported. - return nil - } - } - - return &fieldInfo{ - typ: field.Type, - name: field.Name, - alias: alias, - canonicalAlias: canonicalAlias, - unmarshalerInfo: m, - isSliceOfStructs: isSlice && isStruct, - isAnonymous: field.Anonymous, - isRequired: options.Contains("required"), - } -} - -// converter returns the converter for a type. -func (c *cache) converter(t reflect.Type) Converter { - return c.regconv[t] -} - -// ---------------------------------------------------------------------------- - -type structInfo struct { - fields []*fieldInfo -} - -func (i *structInfo) get(alias string) *fieldInfo { - for _, field := range i.fields { - if strings.EqualFold(field.alias, alias) { - return field - } - } - return nil -} - -func containsAlias(infos []*structInfo, alias string) bool { - for _, info := range infos { - if info.get(alias) != nil { - return true - } - } - return false -} - -type fieldInfo struct { - typ reflect.Type - // name is the field name in the struct. - name string - alias string - // canonicalAlias is almost the same as the alias, but is prefixed with - // an embedded struct field alias in dotted notation if this field is - // promoted from the struct. - // For instance, if the alias is "N" and this field is an embedded field - // in a struct "X", canonicalAlias will be "X.N". - canonicalAlias string - // unmarshalerInfo contains information regarding the - // encoding.TextUnmarshaler implementation of the field type. - unmarshalerInfo unmarshaler - // isSliceOfStructs indicates if the field type is a slice of structs. - isSliceOfStructs bool - // isAnonymous indicates whether the field is embedded in the struct. - isAnonymous bool - isRequired bool -} - -func (f *fieldInfo) paths(prefix string) []string { - if f.alias == f.canonicalAlias { - return []string{prefix + f.alias} - } - return []string{prefix + f.alias, prefix + f.canonicalAlias} -} - -type pathPart struct { - field *fieldInfo - path []string // path to the field: walks structs using field names. - index int // struct index in slices of structs. -} - -// ---------------------------------------------------------------------------- - -func indirectType(typ reflect.Type) reflect.Type { - if typ.Kind() == reflect.Ptr { - return typ.Elem() - } - return typ -} - -// fieldAlias parses a field tag to get a field alias. -func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { - if tag := field.Tag.Get(tagName); tag != "" { - alias, options = parseTag(tag) - } - if alias == "" { - alias = field.Name - } - return alias, options -} - -// tagOptions is the string following a comma in a struct field's tag, or -// the empty string. It does not include the leading comma. -type tagOptions []string - -// parseTag splits a struct field's url tag into its name and comma-separated -// options. -func parseTag(tag string) (string, tagOptions) { - s := strings.Split(tag, ",") - return s[0], s[1:] -} - -// Contains checks whether the tagOptions contains the specified option. -func (o tagOptions) Contains(option string) bool { - for _, s := range o { - if s == option { - return true - } - } - return false -} diff --git a/internal/schema/converter.go b/internal/schema/converter.go deleted file mode 100644 index 4f2116a15e..0000000000 --- a/internal/schema/converter.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "reflect" - "strconv" -) - -type Converter func(string) reflect.Value - -var ( - invalidValue = reflect.Value{} - boolType = reflect.Bool - float32Type = reflect.Float32 - float64Type = reflect.Float64 - intType = reflect.Int - int8Type = reflect.Int8 - int16Type = reflect.Int16 - int32Type = reflect.Int32 - int64Type = reflect.Int64 - stringType = reflect.String - uintType = reflect.Uint - uint8Type = reflect.Uint8 - uint16Type = reflect.Uint16 - uint32Type = reflect.Uint32 - uint64Type = reflect.Uint64 -) - -// Default converters for basic types. -var builtinConverters = map[reflect.Kind]Converter{ - boolType: convertBool, - float32Type: convertFloat32, - float64Type: convertFloat64, - intType: convertInt, - int8Type: convertInt8, - int16Type: convertInt16, - int32Type: convertInt32, - int64Type: convertInt64, - stringType: convertString, - uintType: convertUint, - uint8Type: convertUint8, - uint16Type: convertUint16, - uint32Type: convertUint32, - uint64Type: convertUint64, -} - -func convertBool(value string) reflect.Value { - if value == "on" { - return reflect.ValueOf(true) - } else if v, err := strconv.ParseBool(value); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertFloat32(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 32); err == nil { - return reflect.ValueOf(float32(v)) - } - return invalidValue -} - -func convertFloat64(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertInt(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 0); err == nil { - return reflect.ValueOf(int(v)) - } - return invalidValue -} - -func convertInt8(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 8); err == nil { - return reflect.ValueOf(int8(v)) - } - return invalidValue -} - -func convertInt16(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 16); err == nil { - return reflect.ValueOf(int16(v)) - } - return invalidValue -} - -func convertInt32(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 32); err == nil { - return reflect.ValueOf(int32(v)) - } - return invalidValue -} - -func convertInt64(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertString(value string) reflect.Value { - return reflect.ValueOf(value) -} - -func convertUint(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 0); err == nil { - return reflect.ValueOf(uint(v)) - } - return invalidValue -} - -func convertUint8(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 8); err == nil { - return reflect.ValueOf(uint8(v)) - } - return invalidValue -} - -func convertUint16(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 16); err == nil { - return reflect.ValueOf(uint16(v)) - } - return invalidValue -} - -func convertUint32(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 32); err == nil { - return reflect.ValueOf(uint32(v)) - } - return invalidValue -} - -func convertUint64(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go deleted file mode 100644 index 9d44822202..0000000000 --- a/internal/schema/decoder.go +++ /dev/null @@ -1,534 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "encoding" - "errors" - "fmt" - "reflect" - "strings" -) - -// NewDecoder returns a new Decoder. -func NewDecoder() *Decoder { - return &Decoder{cache: newCache()} -} - -// Decoder decodes values from a map[string][]string to a struct. -type Decoder struct { - cache *cache - zeroEmpty bool - ignoreUnknownKeys bool -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (d *Decoder) SetAliasTag(tag string) { - d.cache.tag = tag -} - -// ZeroEmpty controls the behaviour when the decoder encounters empty values -// in a map. -// If z is true and a key in the map has the empty string as a value -// then the corresponding struct field is set to the zero value. -// If z is false then empty strings are ignored. -// -// The default value is false, that is empty values do not change -// the value of the struct field. -func (d *Decoder) ZeroEmpty(z bool) { - d.zeroEmpty = z -} - -// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown -// keys in the map. -// If i is true and an unknown field is encountered, it is ignored. This is -// similar to how unknown keys are handled by encoding/json. -// If i is false then Decode will return an error. Note that any valid keys -// will still be decoded in to the target struct. -// -// To preserve backwards compatibility, the default value is false. -func (d *Decoder) IgnoreUnknownKeys(i bool) { - d.ignoreUnknownKeys = i -} - -// RegisterConverter registers a converter function for a custom type. -func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { - d.cache.registerConverter(value, converterFunc) -} - -// Decode decodes a map[string][]string to a struct. -// -// The first parameter must be a pointer to a struct. -// -// The second parameter is a map, typically url.Values from an HTTP request. -// Keys are "paths" in dotted notation to the struct fields and nested structs. -// -// See the package documentation for a full explanation of the mechanics. -func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { - return errors.New("schema: interface must be a pointer to struct") - } - v = v.Elem() - t := v.Type() - errors := MultiError{} - for path, values := range src { - if parts, err := d.cache.parsePath(path, t); err == nil { - if err = d.decode(v, path, parts, values); err != nil { - errors[path] = err - } - } else if !d.ignoreUnknownKeys { - errors[path] = UnknownKeyError{Key: path} - } - } - errors.merge(d.checkRequired(t, src)) - if len(errors) > 0 { - return errors - } - return nil -} - -// checkRequired checks whether required fields are empty -// -// check type t recursively if t has struct fields. -// -// src is the source map for decoding, we use it here to see if those required fields are included in src -func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { - m, errs := d.findRequiredFields(t, "", "") - for key, fields := range m { - if isEmptyFields(fields, src) { - errs[key] = EmptyFieldError{Key: key} - } - } - return errs -} - -// findRequiredFields recursively searches the struct type t for required fields. -// -// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation -// for nested struct fields. canonicalPrefix is a complete path which never omits -// any embedded struct fields. searchPrefix is a user-friendly path which may omit -// some embedded struct fields to point promoted fields. -func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { - struc := d.cache.get(t) - if struc == nil { - // unexpect, cache.get never return nil - return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} - } - - m := map[string][]fieldWithPrefix{} - errs := MultiError{} - for _, f := range struc.fields { - if f.typ.Kind() == reflect.Struct { - fcprefix := canonicalPrefix + f.canonicalAlias + "." - for _, fspath := range f.paths(searchPrefix) { - fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") - for key, fields := range fm { - m[key] = append(m[key], fields...) - } - errs.merge(ferrs) - } - } - if f.isRequired { - key := canonicalPrefix + f.canonicalAlias - m[key] = append(m[key], fieldWithPrefix{ - fieldInfo: f, - prefix: searchPrefix, - }) - } - } - return m, errs -} - -type fieldWithPrefix struct { - *fieldInfo - prefix string -} - -// isEmptyFields returns true if all of specified fields are empty. -func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { - for _, f := range fields { - for _, path := range f.paths(f.prefix) { - v, ok := src[path] - if ok && !isEmpty(f.typ, v) { - return false - } - for key := range src { - // issue references: - // https://github.com/gofiber/fiber/issues/1414 - // https://github.com/gorilla/schema/issues/176 - nested := strings.IndexByte(key, '.') != -1 - - // for non required nested structs - c1 := strings.HasSuffix(f.prefix, ".") && key == path - - // for required nested structs - c2 := f.prefix == "" && nested && strings.HasPrefix(key, path) - - // for non nested fields - c3 := f.prefix == "" && !nested && key == path - if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) { - return false - } - } - } - } - return true -} - -// isEmpty returns true if value is empty for specific type -func isEmpty(t reflect.Type, value []string) bool { - if len(value) == 0 { - return true - } - switch t.Kind() { - case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: - return len(value[0]) == 0 - } - return false -} - -// decode fills a struct field using a parsed path. -func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { - // Get the field walking the struct fields by index. - for _, name := range parts[0].path { - if v.Type().Kind() == reflect.Ptr { - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - v = v.Elem() - } - - // alloc embedded structs - if v.Type().Kind() == reflect.Struct { - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { - field.Set(reflect.New(field.Type().Elem())) - } - } - } - - v = v.FieldByName(name) - } - // Don't even bother for unexported fields. - if !v.CanSet() { - return nil - } - - // Dereference if needed. - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - if v.IsNil() { - v.Set(reflect.New(t)) - } - v = v.Elem() - } - - // Slice of structs. Let's go recursive. - if len(parts) > 1 { - idx := parts[0].index - if v.IsNil() || v.Len() < idx+1 { - value := reflect.MakeSlice(t, idx+1, idx+1) - if v.Len() < idx+1 { - // Resize it. - reflect.Copy(value, v) - } - v.Set(value) - } - return d.decode(v.Index(idx), path, parts[1:], values) - } - - // Get the converter early in case there is one for a slice type. - conv := d.cache.converter(t) - m := isTextUnmarshaler(v) - if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { - var items []reflect.Value - elemT := t.Elem() - isPtrElem := elemT.Kind() == reflect.Ptr - if isPtrElem { - elemT = elemT.Elem() - } - - // Try to get a converter for the element type. - conv := d.cache.converter(elemT) - if conv == nil { - conv = builtinConverters[elemT.Kind()] - if conv == nil { - // As we are not dealing with slice of structs here, we don't need to check if the type - // implements TextUnmarshaler interface - return fmt.Errorf("schema: converter not found for %v", elemT) - } - } - - for key, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if m.IsValid { - u := reflect.New(elemT) - if m.IsSliceElementPtr { - u = reflect.New(reflect.PtrTo(elemT).Elem()) - } - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: key, - Err: err, - } - } - if m.IsSliceElementPtr { - items = append(items, u.Elem().Addr()) - } else if u.Kind() == reflect.Ptr { - items = append(items, u.Elem()) - } else { - items = append(items, u) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - if strings.Contains(value, ",") { - values := strings.Split(value, ",") - for _, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } - value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) - v.Set(value) - } else { - val := "" - // Use the last value provided if any values were provided - if len(values) > 0 { - val = values[len(values)-1] - } - - if conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else if m.IsValid { - if m.IsPtr { - u := reflect.New(v.Type()) - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - v.Set(reflect.Indirect(u)) - } else { - // If the value implements the encoding.TextUnmarshaler interface - // apply UnmarshalText as the converter - if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - } - } else if val == "" { - if d.zeroEmpty { - v.Set(reflect.Zero(t)) - } - } else if conv := builtinConverters[t.Kind()]; conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else { - return fmt.Errorf("schema: converter not found for %v", t) - } - } - return nil -} - -func isTextUnmarshaler(v reflect.Value) unmarshaler { - // Create a new unmarshaller instance - m := unmarshaler{} - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // As the UnmarshalText function should be applied to the pointer of the - // type, we check that type to see if it implements the necessary - // method. - if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { - m.IsPtr = true - return m - } - - // if v is []T or *[]T create new T - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - if t.Kind() == reflect.Slice { - // Check if the slice implements encoding.TextUnmarshaller - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // If t is a pointer slice, check if its elements implement - // encoding.TextUnmarshaler - m.IsSliceElement = true - if t = t.Elem(); t.Kind() == reflect.Ptr { - t = reflect.PtrTo(t.Elem()) - v = reflect.Zero(t) - m.IsSliceElementPtr = true - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m - } - } - - v = reflect.New(t) - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m -} - -// TextUnmarshaler helpers ---------------------------------------------------- -// unmarshaller contains information about a TextUnmarshaler type -type unmarshaler struct { - Unmarshaler encoding.TextUnmarshaler - // IsValid indicates whether the resolved type indicated by the other - // flags implements the encoding.TextUnmarshaler interface. - IsValid bool - // IsPtr indicates that the resolved type is the pointer of the original - // type. - IsPtr bool - // IsSliceElement indicates that the resolved type is a slice element of - // the original type. - IsSliceElement bool - // IsSliceElementPtr indicates that the resolved type is a pointer to a - // slice element of the original type. - IsSliceElementPtr bool -} - -// Errors --------------------------------------------------------------------- - -// ConversionError stores information about a failed conversion. -type ConversionError struct { - Key string // key from the source map. - Type reflect.Type // expected type of elem - Index int // index for multi-value fields; -1 for single-value fields. - Err error // low-level error (when it exists) -} - -func (e ConversionError) Error() string { - var output string - - if e.Index < 0 { - output = fmt.Sprintf("schema: error converting value for %q", e.Key) - } else { - output = fmt.Sprintf("schema: error converting value for index %d of %q", - e.Index, e.Key) - } - - if e.Err != nil { - output = fmt.Sprintf("%s. Details: %s", output, e.Err) - } - - return output -} - -// UnknownKeyError stores information about an unknown key in the source map. -type UnknownKeyError struct { - Key string // key from the source map. -} - -func (e UnknownKeyError) Error() string { - return fmt.Sprintf("schema: invalid path %q", e.Key) -} - -// EmptyFieldError stores information about an empty required field. -type EmptyFieldError struct { - Key string // required key in the source map. -} - -func (e EmptyFieldError) Error() string { - return fmt.Sprintf("%v is empty", e.Key) -} - -// MultiError stores multiple decoding errors. -// -// Borrowed from the App Engine SDK. -type MultiError map[string]error - -func (e MultiError) Error() string { - s := "" - for _, err := range e { - s = err.Error() - break - } - switch len(e) { - case 0: - return "(0 errors)" - case 1: - return s - case 2: - return s + " (and 1 other error)" - } - return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) -} - -func (e MultiError) merge(errors MultiError) { - for key, err := range errors { - if e[key] == nil { - e[key] = err - } - } -} diff --git a/internal/schema/doc.go b/internal/schema/doc.go deleted file mode 100644 index fff0fe7616..0000000000 --- a/internal/schema/doc.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package gorilla/schema fills a struct with form values. - -The basic usage is really simple. Given this struct: - - type Person struct { - Name string - Phone string - } - -...we can fill it passing a map to the Decode() function: - - values := map[string][]string{ - "Name": {"John"}, - "Phone": {"999-999-999"}, - } - person := new(Person) - decoder := schema.NewDecoder() - decoder.Decode(person, values) - -This is just a simple example and it doesn't make a lot of sense to create -the map manually. Typically it will come from a http.Request object and -will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: - - func MyHandler(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - - if err != nil { - // Handle error - } - - decoder := schema.NewDecoder() - // r.PostForm is a map of our POST form values - err := decoder.Decode(person, r.PostForm) - - if err != nil { - // Handle error - } - - // Do something with person.Name or person.Phone - } - -Note: it is a good idea to set a Decoder instance as a package global, -because it caches meta-data about structs, and an instance can be shared safely: - - var decoder = schema.NewDecoder() - -To define custom names for fields, use a struct tag "schema". To not populate -certain fields, use a dash for the name and it will be ignored: - - type Person struct { - Name string `schema:"name"` // custom name - Phone string `schema:"phone"` // custom name - Admin bool `schema:"-"` // this field is never set - } - -The supported field types in the destination struct are: - - - bool - - float variants (float32, float64) - - int variants (int, int8, int16, int32, int64) - - string - - uint variants (uint, uint8, uint16, uint32, uint64) - - struct - - a pointer to one of the above types - - a slice or a pointer to a slice of one of the above types - -Non-supported types are simply ignored, however custom types can be registered -to be converted. - -To fill nested structs, keys must use a dotted notation as the "path" for the -field. So for example, to fill the struct Person below: - - type Phone struct { - Label string - Number string - } - - type Person struct { - Name string - Phone Phone - } - -...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". -This means that an HTML form to fill a Person struct must look like this: - -
- - - -
- -Single values are filled using the first value for a key from the source map. -Slices are filled using all values for a key from the source map. So to fill -a Person with multiple Phone values, like: - - type Person struct { - Name string - Phones []Phone - } - -...an HTML form that accepts three Phone values would look like this: - -
- - - - - - - -
- -Notice that only for slices of structs the slice index is required. -This is needed for disambiguation: if the nested struct also had a slice -field, we could not translate multiple values to it if we did not use an -index for the parent struct. - -There's also the possibility to create a custom type that implements the -TextUnmarshaler interface, and in this case there's no need to register -a converter, like: - - type Person struct { - Emails []Email - } - - type Email struct { - *mail.Address - } - - func (e *Email) UnmarshalText(text []byte) (err error) { - e.Address, err = mail.ParseAddress(string(text)) - return - } - -...an HTML form that accepts three Email values would look like this: - -
- - - -
-*/ -package schema diff --git a/internal/schema/encoder.go b/internal/schema/encoder.go deleted file mode 100644 index f0ed631210..0000000000 --- a/internal/schema/encoder.go +++ /dev/null @@ -1,202 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "reflect" - "strconv" -) - -type encoderFunc func(reflect.Value) string - -// Encoder encodes values from a struct into url.Values. -type Encoder struct { - cache *cache - regenc map[reflect.Type]encoderFunc -} - -// NewEncoder returns a new Encoder with defaults. -func NewEncoder() *Encoder { - return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} -} - -// Encode encodes a struct into map[string][]string. -// -// Intended for use with url.Values. -func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { - v := reflect.ValueOf(src) - - return e.encode(v, dst) -} - -// RegisterEncoder registers a converter for encoding a custom type. -func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { - e.regenc[reflect.TypeOf(value)] = encoder -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (e *Encoder) SetAliasTag(tag string) { - e.cache.tag = tag -} - -// isValidStructPointer test if input value is a valid struct pointer. -func isValidStructPointer(v reflect.Value) bool { - return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct -} - -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Func: - case reflect.Map, reflect.Slice: - return v.IsNil() || v.Len() == 0 - case reflect.Array: - z := true - for i := 0; i < v.Len(); i++ { - z = z && isZero(v.Index(i)) - } - return z - case reflect.Struct: - type zero interface { - IsZero() bool - } - if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { - iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] - return iz.Interface().(bool) - } - z := true - for i := 0; i < v.NumField(); i++ { - z = z && isZero(v.Field(i)) - } - return z - } - // Compare other types directly: - z := reflect.Zero(v.Type()) - return v.Interface() == z.Interface() -} - -func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return errors.New("schema: interface must be a struct") - } - t := v.Type() - - errors := MultiError{} - - for i := 0; i < v.NumField(); i++ { - name, opts := fieldAlias(t.Field(i), e.cache.tag) - if name == "-" { - continue - } - - // Encode struct pointer types if the field is a valid pointer and a struct. - if isValidStructPointer(v.Field(i)) { - e.encode(v.Field(i).Elem(), dst) - continue - } - - encFunc := typeEncoder(v.Field(i).Type(), e.regenc) - - // Encode non-slice types and custom implementations immediately. - if encFunc != nil { - value := encFunc(v.Field(i)) - if opts.Contains("omitempty") && isZero(v.Field(i)) { - continue - } - - dst[name] = append(dst[name], value) - continue - } - - if v.Field(i).Type().Kind() == reflect.Struct { - e.encode(v.Field(i), dst) - continue - } - - if v.Field(i).Type().Kind() == reflect.Slice { - encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) - } - - if encFunc == nil { - errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) - continue - } - - // Encode a slice. - if v.Field(i).Len() == 0 && opts.Contains("omitempty") { - continue - } - - dst[name] = []string{} - for j := 0; j < v.Field(i).Len(); j++ { - dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) - } - } - - if len(errors) > 0 { - return errors - } - return nil -} - -func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { - if f, ok := reg[t]; ok { - return f - } - - switch t.Kind() { - case reflect.Bool: - return encodeBool - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return encodeInt - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return encodeUint - case reflect.Float32: - return encodeFloat32 - case reflect.Float64: - return encodeFloat64 - case reflect.Ptr: - f := typeEncoder(t.Elem(), reg) - return func(v reflect.Value) string { - if v.IsNil() { - return "null" - } - return f(v.Elem()) - } - case reflect.String: - return encodeString - default: - return nil - } -} - -func encodeBool(v reflect.Value) string { - return strconv.FormatBool(v.Bool()) -} - -func encodeInt(v reflect.Value) string { - return strconv.FormatInt(int64(v.Int()), 10) -} - -func encodeUint(v reflect.Value) string { - return strconv.FormatUint(uint64(v.Uint()), 10) -} - -func encodeFloat(v reflect.Value, bits int) string { - return strconv.FormatFloat(v.Float(), 'f', 6, bits) -} - -func encodeFloat32(v reflect.Value) string { - return encodeFloat(v, 32) -} - -func encodeFloat64(v reflect.Value) string { - return encodeFloat(v, 64) -} - -func encodeString(v reflect.Value) string { - return v.String() -} diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 72f7876463..74a1f43be4 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -237,15 +237,10 @@ 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 out { - reqHeaders = append(reqHeaders, k+"="+v) - } + c.Request().Header.VisitAll(func(k, v []byte) { + reqHeaders = append(reqHeaders, string(k)+"="+string(v)) + }) return buf.Write([]byte(strings.Join(reqHeaders, "&"))) case TagQueryStringParams: return buf.WriteString(c.Request().URI().QueryArgs().String())