From 2cb58a247b35d4cb0edb2bf866605860084f2898 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Tue, 23 Aug 2022 20:43:17 +0800 Subject: [PATCH 01/22] remove old binding --- app.go | 32 +- bind.go | 194 ----- bind_test.go | 1544 ---------------------------------- binder/README.md | 194 ----- binder/binder.go | 19 - binder/cookie.go | 45 - binder/form.go | 53 -- binder/header.go | 34 - binder/json.go | 15 - binder/mapping.go | 199 ----- binder/mapping_test.go | 31 - binder/query.go | 49 -- binder/resp_header.go | 34 - binder/uri.go | 16 - binder/xml.go | 15 - ctx.go | 14 - ctx_interface.go | 6 - error.go | 41 - error_test.go | 67 -- internal/schema/LICENSE | 27 - internal/schema/cache.go | 305 ------- internal/schema/converter.go | 145 ---- internal/schema/decoder.go | 534 ------------ internal/schema/doc.go | 148 ---- internal/schema/encoder.go | 202 ----- middleware/logger/logger.go | 11 +- 26 files changed, 11 insertions(+), 3963 deletions(-) delete mode 100644 bind.go delete mode 100644 bind_test.go delete mode 100644 binder/README.md delete mode 100644 binder/binder.go delete mode 100644 binder/cookie.go delete mode 100644 binder/form.go delete mode 100644 binder/header.go delete mode 100644 binder/json.go delete mode 100644 binder/mapping.go delete mode 100644 binder/mapping_test.go delete mode 100644 binder/query.go delete mode 100644 binder/resp_header.go delete mode 100644 binder/uri.go delete mode 100644 binder/xml.go delete mode 100644 error_test.go delete mode 100644 internal/schema/LICENSE delete mode 100644 internal/schema/cache.go delete mode 100644 internal/schema/converter.go delete mode 100644 internal/schema/decoder.go delete mode 100644 internal/schema/doc.go delete mode 100644 internal/schema/encoder.go diff --git a/app.go b/app.go index 370cc5b2e39..378a3426b96 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 b390db2fdad..00000000000 --- 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 090f76db042..00000000000 --- 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 d40cc7e54e0..00000000000 --- 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 d3931790114..00000000000 --- 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 e761e4776ca..00000000000 --- 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 24983ccdeab..00000000000 --- 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 688a81136a7..00000000000 --- 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 570a7f9b79e..00000000000 --- 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 bec5634808a..00000000000 --- 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 aec91ff2beb..00000000000 --- 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 ce62e09d0fe..00000000000 --- 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 2b31710d24b..00000000000 --- 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 2759f7b464b..00000000000 --- 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 29401abb772..00000000000 --- 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 06fc2e67a14..9f88d1f9edd 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 9f24a4c4b14..d18ae3d18b5 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 87a6af38c9e..d6aee39d991 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 7fce3c12aa5..00000000000 --- 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 0e5fb872800..00000000000 --- 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 bf21697cf19..00000000000 --- 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 4f2116a15ea..00000000000 --- 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 9d44822202e..00000000000 --- 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 fff0fe76168..00000000000 --- 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 f0ed6312100..00000000000 --- 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 72f7876463b..74a1f43be49 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()) From 3251afc8c98247cf498652808260a91653733ae1 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Tue, 23 Aug 2022 20:47:23 +0800 Subject: [PATCH 02/22] add new bind --- app.go | 18 ++ bind.go | 59 +++++ bind_readme.md | 172 ++++++++++++ bind_test.go | 331 ++++++++++++++++++++++++ binder.go | 124 +++++++++ binder_compile.go | 164 ++++++++++++ binder_slice.go | 76 ++++++ binder_test.go | 32 +++ client_test.go | 5 +- ctx.go | 29 ++- ctx_interface.go | 19 +- error.go | 31 ++- internal/bind/bool.go | 18 ++ internal/bind/compile.go | 49 ++++ internal/bind/int.go | 19 ++ internal/bind/string.go | 15 ++ internal/bind/text_unmarshaler.go | 27 ++ internal/bind/uint.go | 19 ++ internal/reflectunsafe/reflectunsafe.go | 12 + utils/xml.go | 6 + validate.go | 5 + 21 files changed, 1222 insertions(+), 8 deletions(-) create mode 100644 bind.go create mode 100644 bind_readme.md create mode 100644 bind_test.go create mode 100644 binder.go create mode 100644 binder_compile.go create mode 100644 binder_slice.go create mode 100644 binder_test.go create mode 100644 internal/bind/bool.go create mode 100644 internal/bind/compile.go create mode 100644 internal/bind/int.go create mode 100644 internal/bind/string.go create mode 100644 internal/bind/text_unmarshaler.go create mode 100644 internal/bind/uint.go create mode 100644 internal/reflectunsafe/reflectunsafe.go create mode 100644 validate.go diff --git a/app.go b/app.go index 378a3426b96..9b40b4084b2 100644 --- a/app.go +++ b/app.go @@ -117,6 +117,8 @@ type App struct { newCtxFunc func(app *App) CustomCtx // TLS handler tlsHandler *tlsHandler + // bind decoder cache + bindDecoderCache sync.Map } // Config is a struct holding the server settings. @@ -329,6 +331,17 @@ type Config struct { // Default: xml.Marshal XMLEncoder utils.XMLMarshal `json:"-"` + // XMLDecoder set by an external client of Fiber it will use the provided implementation of a + // XMLUnmarshal + // + // Allowing for flexibility in using another XML library for encoding + // Default: utils.XMLUnmarshal + XMLDecoder utils.XMLUnmarshal `json:"-"` + + // App validate. if nil, and context.EnableValidate will always return a error. + // Default: nil + Validator Validator + // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose. // @@ -513,9 +526,14 @@ func New(config ...Config) *App { if app.config.JSONDecoder == nil { app.config.JSONDecoder = json.Unmarshal } + if app.config.XMLEncoder == nil { app.config.XMLEncoder = xml.Marshal } + if app.config.XMLDecoder == nil { + app.config.XMLDecoder = xml.Unmarshal + } + if app.config.Network == "" { app.config.Network = NetworkTCP4 } diff --git a/bind.go b/bind.go new file mode 100644 index 00000000000..cce399203bf --- /dev/null +++ b/bind.go @@ -0,0 +1,59 @@ +package fiber + +import ( + "fmt" + "reflect" + + "github.com/gofiber/fiber/v3/internal/bind" +) + +type Binder interface { + UnmarshalFiberCtx(ctx Ctx) error +} + +// decoder should set a field on reqValue +// it's created with field index +type decoder interface { + Decode(ctx Ctx, reqValue reflect.Value) error +} + +type fieldCtxDecoder struct { + index int + fieldName string + fieldType reflect.Type +} + +func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + v := reflect.New(d.fieldType) + unmarshaler := v.Interface().(Binder) + + if err := unmarshaler.UnmarshalFiberCtx(ctx); err != nil { + return err + } + + reqValue.Field(d.index).Set(v.Elem()) + return nil +} + +type fieldTextDecoder struct { + index int + fieldName string + tag string // query,param,header,respHeader ... + reqField string + dec bind.TextDecoder + get func(c Ctx, key string, defaultValue ...string) string +} + +func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + text := d.get(ctx, d.reqField) + if text == "" { + return nil + } + + err := d.dec.UnmarshalString(text, reqValue.Field(d.index)) + if err != nil { + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err) + } + + return nil +} diff --git a/bind_readme.md b/bind_readme.md new file mode 100644 index 00000000000..77cc5773bca --- /dev/null +++ b/bind_readme.md @@ -0,0 +1,172 @@ +# Fiber Binders + +Bind is new request/response binding feature for Fiber. +By against old Fiber parsers, it supports custom binder registration, +struct validation with high performance and easy to use. + +It's introduced in Fiber v3 and a replacement of: + +- BodyParser +- ParamsParser +- GetReqHeaders +- GetRespHeaders +- AllParams +- QueryParser +- ReqHeaderParser + +## Guides + +### Binding basic request info + +Fiber supports binding basic request data into the struct: + +all tags you can use are: + +- respHeader +- header +- query +- param +- cookie + +(binding for Request/Response header are case in-sensitive) + +private and anonymous fields will be ignored. + +```go +package main + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "time" + + fiber "github.com/gofiber/fiber/v3" +) + +type Req struct { + ID int `param:"id"` + Q int `query:"q"` + Likes []int `query:"likes"` + T time.Time `header:"x-time"` + Token string `header:"x-auth"` +} + +func main() { + app := fiber.New() + + app.Get("/:id", func(c fiber.Ctx) error { + var req Req + if err := c.Bind().Req(&req).Err(); err != nil { + return err + } + return c.JSON(req) + }) + + req := httptest.NewRequest(http.MethodGet, "/1?&s=a,b,c&q=47&likes=1&likes=2", http.NoBody) + req.Header.Set("x-auth", "ttt") + req.Header.Set("x-time", "2022-08-08T08:11:39+08:00") + resp, err := app.Test(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + fmt.Println(resp.StatusCode, string(b)) + // Output: 200 {"ID":1,"S":["a","b","c"],"Q":47,"Likes":[1,2],"T":"2022-08-08T08:11:39+08:00","Token":"ttt"} +} + +``` + +### Defining Custom Binder + +We support 2 types of Custom Binder + +#### a `encoding.TextUnmarshaler` with basic tag config. + +like the `time.Time` field in the previous example, if a field implement `encoding.TextUnmarshaler`, it will be called +to +unmarshal raw string we get from request's query/header/... + +#### a `fiber.Binder` interface. + +You don't need to set a field tag and it's binding tag will be ignored. + +``` +type Binder interface { + UnmarshalFiberCtx(ctx fiber.Ctx) error +} +``` + +If your type implement `fiber.Binder`, bind will pass current request Context to your and you can unmarshal the info +you need. + +### Parse Request Body + +you can call `ctx.BodyJSON(v any) error` or `BodyXML(v any) error` + +These methods will check content-type HTTP header and call configured JSON or XML decoder to unmarshal. + +```golang +package main + +type Body struct { + ID int `json:"..."` + Q int `json:"..."` + Likes []int `json:"..."` + T time.Time `json:"..."` + Token string `json:"..."` +} + +func main() { + app := fiber.New() + + app.Get("/:id", func(c fiber.Ctx) error { + var data Body + if err := c.Bind().JSON(&data).Err(); err != nil { + return err + } + return c.JSON(data) + }) +} +``` + +### Bind With validation + +Normally, `bind` will only try to unmarshal data from request and pass it to request handler. + +you can call `.Validate()` to validate previous binding. + +And you will need to set a validator in app Config, otherwise it will always return an error. + +```go +package main + +type Validator struct{} + +func (validator *Validator) Validate(v any) error { + return nil +} + +func main() { + app := fiber.New(fiber.Config{ + Validator: &Validator{}, + }) + + app.Get("/:id", func(c fiber.Ctx) error { + var req struct{} + var body struct{} + if err := c.Bind().Req(&req).Validate().JSON(&body).Validate().Err(); err != nil { + return err + } + + return nil + }) +} +``` diff --git a/bind_test.go b/bind_test.go new file mode 100644 index 00000000000..1f1f4aca4d8 --- /dev/null +++ b/bind_test.go @@ -0,0 +1,331 @@ +package fiber + +import ( + "net/url" + "regexp" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +// go test -run Test_Bind_BasicType -v +func Test_Bind_BasicType(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Query struct { + Flag bool `query:"enable"` + + I8 int8 `query:"i8"` + I16 int16 `query:"i16"` + I32 int32 `query:"i32"` + I64 int64 `query:"i64"` + I int `query:"i"` + + U8 uint8 `query:"u8"` + U16 uint16 `query:"u16"` + U32 uint32 `query:"u32"` + U64 uint64 `query:"u64"` + U uint `query:"u"` + + S string `query:"s"` + } + + var q Query + + const qs = "i8=88&i16=166&i32=322&i64=644&i=101&u8=77&u16=165&u32=321&u64=643&u=99&s=john&enable=true" + c.Request().URI().SetQueryString(qs) + require.NoError(t, c.Bind().Req(&q).Err()) + + require.Equal(t, Query{ + Flag: true, + I8: 88, + I16: 166, + I32: 322, + I64: 644, + I: 101, + U8: 77, + U16: 165, + U32: 321, + U64: 643, + U: 99, + S: "john", + }, q) + + type Query2 struct { + Flag []bool `query:"enable"` + + I8 []int8 `query:"i8"` + I16 []int16 `query:"i16"` + I32 []int32 `query:"i32"` + I64 []int64 `query:"i64"` + I []int `query:"i"` + + U8 []uint8 `query:"u8"` + U16 []uint16 `query:"u16"` + U32 []uint32 `query:"u32"` + U64 []uint64 `query:"u64"` + U []uint `query:"u"` + + S []string `query:"s"` + } + + var q2 Query2 + + c.Request().URI().SetQueryString(qs) + require.NoError(t, c.Bind().Req(&q2).Err()) + + require.Equal(t, Query2{ + Flag: []bool{true}, + I8: []int8{88}, + I16: []int16{166}, + I32: []int32{322}, + I64: []int64{644}, + I: []int{101}, + U8: []uint8{77}, + U16: []uint16{165}, + U32: []uint32{321}, + U64: []uint64{643}, + U: []uint{99}, + S: []string{"john"}, + }, q2) + +} + +// 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 `query:"id"` + Name string `query:"name"` + Hobby []string `query:"hobby"` + } + + var q Query + + c.Request().SetBody([]byte{}) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 1, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("") + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 0, len(q.Hobby)) + + type Query2 struct { + Bool bool `query:"bool"` + ID int `query:"id"` + Name string `query:"name"` + Hobby string `query:"hobby"` + FavouriteDrinks string `query:"favouriteDrinks"` + Empty []string `query:"empty"` + Alloc []string `query:"alloc"` + No []int64 `query:"no"` + } + + var q2 Query2 + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + require.NoError(t, c.Bind().Req(&q2).Err()) + require.Equal(t, "basketball,football", q2.Hobby) + require.Equal(t, "tom", q2.Name) // check value get overwritten + require.Equal(t, "milo,coke,pepsi", q2.FavouriteDrinks) + require.Equal(t, []string{}, q2.Empty) + require.Equal(t, []string{""}, q2.Alloc) + require.Equal(t, []int64{1}, q2.No) + + type ArrayQuery struct { + Data []string `query:"data[]"` + } + var aq ArrayQuery + c.Request().URI().SetQueryString("data[]=john&data[]=doe") + require.NoError(t, c.Bind().Req(&aq).Err()) + require.Equal(t, ArrayQuery{Data: []string{"john", "doe"}}, aq) +} + +// go test -run Test_Bind_Resp_Header -v +func Test_Bind_Resp_Header(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type resHeader struct { + Key string `respHeader:"k"` + + Keys []string `respHeader:"keys"` + } + + c.Set("k", "vv") + c.Response().Header.Add("keys", "v1") + c.Response().Header.Add("keys", "v2") + + var q resHeader + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, "vv", q.Key) + require.Equal(t, []string{"v1", "v2"}, q.Keys) +} + +var _ Binder = (*userCtxUnmarshaler)(nil) + +type userCtxUnmarshaler struct { + V int +} + +func (u *userCtxUnmarshaler) UnmarshalFiberCtx(ctx Ctx) error { + u.V++ + return nil +} + +// go test -run Test_Bind_CustomizedUnmarshaler -v +func Test_Bind_CustomizedUnmarshaler(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Req struct { + Key userCtxUnmarshaler + } + + var r Req + require.NoError(t, c.Bind().Req(&r).Err()) + require.Equal(t, 1, r.Key.V) + + require.NoError(t, c.Bind().Req(&r).Err()) + require.Equal(t, 1, r.Key.V) +} + +// go test -run Test_Bind_TextUnmarshaler -v +func Test_Bind_TextUnmarshaler(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Req struct { + Time time.Time `query:"time"` + } + + now := time.Now() + + c.Request().URI().SetQueryString(url.Values{ + "time": []string{now.Format(time.RFC3339Nano)}, + }.Encode()) + + var q Req + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, false, q.Time.IsZero(), "time should not be zero") + require.Equal(t, true, q.Time.Before(now.Add(time.Second))) + require.Equal(t, true, q.Time.After(now.Add(-time.Second))) +} + +// go test -run Test_Bind_error_message -v +func Test_Bind_error_message(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + type Req struct { + Time time.Time `query:"time"` + } + + c.Request().URI().SetQueryString("time=john") + + err := c.Bind().Req(&Req{}).Err() + + require.Error(t, err) + require.Regexp(t, regexp.MustCompile(`unable to decode 'john' as time`), err.Error()) +} + +type Req struct { + ID int `query:"id"` + + I int `query:"I"` + J int `query:"j"` + K int `query:"k"` + + Token string `header:"x-auth"` +} + +func getCtx() Ctx { + app := New() + + // TODO: also bench params + ctx := app.NewCtx(&fasthttp.RequestCtx{}) + + var u = fasthttp.URI{} + u.SetQueryString("j=1&j=123&k=-1") + ctx.Request().SetURI(&u) + + ctx.Request().Header.Set("a-auth", "bearer tt") + + return ctx +} + +func Benchmark_Bind_by_hand(b *testing.B) { + ctx := getCtx() + for i := 0; i < b.N; i++ { + var req Req + var err error + if raw := ctx.Query("id"); raw != "" { + req.ID, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } + + if raw := ctx.Query("i"); raw != "" { + req.I, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } + + if raw := ctx.Query("j"); raw != "" { + req.J, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } + + if raw := ctx.Query("k"); raw != "" { + req.K, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } + + req.Token = ctx.Get("x-auth") + } +} + +func Benchmark_Bind(b *testing.B) { + ctx := getCtx() + for i := 0; i < b.N; i++ { + var v = Req{} + err := ctx.Bind().Req(&v) + if err != nil { + b.Error(err) + b.FailNow() + } + } +} diff --git a/binder.go b/binder.go new file mode 100644 index 00000000000..4ce2f1b6b7f --- /dev/null +++ b/binder.go @@ -0,0 +1,124 @@ +package fiber + +import ( + "bytes" + "net/http" + "reflect" + + "github.com/gofiber/fiber/v3/internal/reflectunsafe" + "github.com/gofiber/fiber/v3/utils" +) + +type Bind struct { + err error + ctx Ctx + val any // last decoded val +} + +func (b *Bind) setErr(err error) *Bind { + b.err = err + return b +} + +func (b *Bind) HTTPErr() error { + if b.err != nil { + if fe, ok := b.err.(*Error); ok { + return fe + } + + return NewError(http.StatusBadRequest, b.err.Error()) + } + + return nil +} + +func (b *Bind) Err() error { + return b.err +} + +// JSON unmarshal body as json +// unlike `ctx.BodyJSON`, this will also check "content-type" HTTP header. +func (b *Bind) JSON(v any) *Bind { + if b.err != nil { + return b + } + + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationJSON)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/json\"")) + } + + if err := b.ctx.BodyJSON(v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + +// XML unmarshal body as xml +// unlike `ctx.BodyXML`, this will also check "content-type" HTTP header. +func (b *Bind) XML(v any) *Bind { + if b.err != nil { + return b + } + + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationXML)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/xml\"")) + } + + if err := b.ctx.BodyXML(v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + +func (b *Bind) Req(v any) *Bind { + if b.err != nil { + return b + } + + if err := b.decode(v); err != nil { + return b.setErr(err) + } + return b +} + +func (b *Bind) Validate() *Bind { + if b.err != nil { + return b + } + + if b.val == nil { + return b + } + + if err := b.ctx.Validate(b.val); err != nil { + return b.setErr(err) + } + + return b +} + +func (b *Bind) decode(v any) error { + rv, typeID := reflectunsafe.ValueAndTypeID(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidBinderError{Type: reflect.TypeOf(v)} + } + + cached, ok := b.ctx.App().bindDecoderCache.Load(typeID) + if ok { + // cached decoder, fast path + decoder := cached.(Decoder) + return decoder(b.ctx, rv.Elem()) + } + + decoder, err := compileReqParser(rv.Type()) + if err != nil { + return err + } + + b.ctx.App().bindDecoderCache.Store(typeID, decoder) + return decoder(b.ctx, rv.Elem()) +} diff --git a/binder_compile.go b/binder_compile.go new file mode 100644 index 00000000000..68eb47a2e12 --- /dev/null +++ b/binder_compile.go @@ -0,0 +1,164 @@ +package fiber + +import ( + "bytes" + "encoding" + "errors" + "fmt" + "reflect" + "strconv" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/gofiber/fiber/v3/utils" +) + +type Decoder func(c Ctx, rv reflect.Value) error + +const bindTagRespHeader = "respHeader" +const bindTagHeader = "header" +const bindTagQuery = "query" +const bindTagParam = "param" +const bindTagCookie = "cookie" + +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() +var bindUnmarshalerType = reflect.TypeOf((*Binder)(nil)).Elem() + +func compileReqParser(rt reflect.Type) (Decoder, error) { + var decoders []decoder + + el := rt.Elem() + if el.Kind() != reflect.Struct { + panic("wrapped request need to struct") + } + + for i := 0; i < el.NumField(); i++ { + if !el.Field(i).IsExported() { + // ignore unexported field + continue + } + + dec, err := compileFieldDecoder(el.Field(i), i) + if err != nil { + return nil, err + } + + if dec != nil { + decoders = append(decoders, dec) + } + } + + return func(c Ctx, rv reflect.Value) error { + for _, decoder := range decoders { + err := decoder.Decode(c, rv) + if err != nil { + return err + } + } + + return nil + }, nil +} + +func compileFieldDecoder(field reflect.StructField, index int) (decoder, error) { + if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { + return &fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}, nil + } + + var tagScope = "" + for _, loopTagScope := range []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} { + if _, ok := field.Tag.Lookup(loopTagScope); ok { + tagScope = loopTagScope + break + } + } + + if tagScope == "" { + return nil, nil + } + + tagContent := field.Tag.Get(tagScope) + + if reflect.PtrTo(field.Type).Implements(textUnmarshalerType) { + return compileTextBasedDecoder(field, index, tagScope, tagContent) + } + + if field.Type.Kind() == reflect.Slice { + return compileSliceFieldTextBasedDecoder(field, index, tagScope, tagContent) + } + + return compileTextBasedDecoder(field, index, tagScope, tagContent) +} + +func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string) (decoder, error) { + var get func(ctx Ctx, key string, defaultValue ...string) string + switch tagScope { + case bindTagQuery: + get = Ctx.Query + case bindTagHeader: + get = Ctx.Get + case bindTagRespHeader: + get = Ctx.GetRespHeader + case bindTagParam: + get = Ctx.Params + case bindTagCookie: + get = Ctx.Cookies + default: + return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) + } + + textDecoder, err := bind.CompileTextDecoder(field.Type) + if err != nil { + return nil, err + } + + return &fieldTextDecoder{ + index: index, + fieldName: field.Name, + tag: tagScope, + reqField: tagContent, + dec: textDecoder, + get: get, + }, nil +} + +func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) (decoder, error) { + if field.Type.Kind() != reflect.Slice { + panic("BUG: unexpected type, expecting slice " + field.Type.String()) + } + + et := field.Type.Elem() + elementUnmarshaler, err := bind.CompileTextDecoder(et) + if err != nil { + return nil, fmt.Errorf("failed to build slice binder: %w", err) + } + + var eqBytes = bytes.Equal + var visitAll func(Ctx, func(key, value []byte)) + switch tagScope { + case bindTagQuery: + visitAll = visitQuery + case bindTagHeader: + visitAll = visitHeader + eqBytes = utils.EqualFold[[]byte] + case bindTagRespHeader: + visitAll = visitResHeader + eqBytes = utils.EqualFold[[]byte] + case bindTagCookie: + visitAll = visitCookie + case bindTagParam: + return nil, errors.New("using params with slice type is not supported") + default: + return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) + } + + return &fieldSliceDecoder{ + fieldIndex: index, + eqBytes: eqBytes, + fieldName: field.Name, + visitAll: visitAll, + reqKey: []byte(tagContent), + fieldType: field.Type, + elementType: et, + elementDecoder: elementUnmarshaler, + }, nil +} diff --git a/binder_slice.go b/binder_slice.go new file mode 100644 index 00000000000..c9031abfe98 --- /dev/null +++ b/binder_slice.go @@ -0,0 +1,76 @@ +package fiber + +import ( + "reflect" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/gofiber/fiber/v3/utils" +) + +var _ decoder = (*fieldSliceDecoder)(nil) + +type fieldSliceDecoder struct { + fieldIndex int + fieldName string + fieldType reflect.Type + reqKey []byte + // [utils.EqualFold] for headers and [bytes.Equal] for query/params. + eqBytes func([]byte, []byte) bool + elementType reflect.Type + elementDecoder bind.TextDecoder + visitAll func(Ctx, func(key []byte, value []byte)) +} + +func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + count := 0 + d.visitAll(ctx, func(key, value []byte) { + if d.eqBytes(key, d.reqKey) { + count++ + } + }) + + rv := reflect.MakeSlice(d.fieldType, 0, count) + + if count == 0 { + reqValue.Field(d.fieldIndex).Set(rv) + return nil + } + + var err error + d.visitAll(ctx, func(key, value []byte) { + if err != nil { + return + } + if d.eqBytes(key, d.reqKey) { + ev := reflect.New(d.elementType) + if ee := d.elementDecoder.UnmarshalString(utils.UnsafeString(value), ev.Elem()); ee != nil { + err = ee + } + + rv = reflect.Append(rv, ev.Elem()) + } + }) + + if err != nil { + return err + } + + reqValue.Field(d.fieldIndex).Set(rv) + return nil +} + +func visitQuery(ctx Ctx, f func(key []byte, value []byte)) { + ctx.Context().QueryArgs().VisitAll(f) +} + +func visitHeader(ctx Ctx, f func(key []byte, value []byte)) { + ctx.Request().Header.VisitAll(f) +} + +func visitResHeader(ctx Ctx, f func(key []byte, value []byte)) { + ctx.Response().Header.VisitAll(f) +} + +func visitCookie(ctx Ctx, f func(key []byte, value []byte)) { + ctx.Request().Header.VisitAllCookie(f) +} diff --git a/binder_test.go b/binder_test.go new file mode 100644 index 00000000000..862969a334c --- /dev/null +++ b/binder_test.go @@ -0,0 +1,32 @@ +package fiber + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +func Test_Binder(t *testing.T) { + t.Parallel() + app := New() + + ctx := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} + ctx.Request().SetBody([]byte(`{"name": "john doe"}`)) + ctx.Request().Header.Set("content-type", "application/json") + + var req struct { + ID string `param:"id"` + } + + var body struct { + Name string `json:"name"` + } + + err := ctx.Bind().Req(&req).JSON(&body).Err() + require.NoError(t, err) + require.Equal(t, "id string", req.ID) + require.Equal(t, "john doe", body.Name) +} diff --git a/client_test.go b/client_test.go index 987b0c3cbba..daf78c0bafc 100644 --- a/client_test.go +++ b/client_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/tls" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -16,8 +17,6 @@ import ( "testing" "time" - "encoding/json" - "github.com/gofiber/fiber/v3/internal/tlstest" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp/fasthttputil" @@ -431,7 +430,7 @@ func Test_Client_Agent_BasicAuth(t *testing.T) { handler := func(c Ctx) error { // Get authorization header auth := c.Get(HeaderAuthorization) - // Decode the header contents + // Req the header contents raw, err := base64.StdEncoding.DecodeString(auth[6:]) require.NoError(t, err) diff --git a/ctx.go b/ctx.go index 9f88d1f9edd..18873635523 100644 --- a/ctx.go +++ b/ctx.go @@ -213,6 +213,25 @@ func (c *DefaultCtx) BaseURL() string { return c.baseURI } +func (c *DefaultCtx) Bind() *Bind { + return &Bind{ctx: c} +} + +// func (c *DefaultCtx) BindWithValidate(v any) error { +// if err := c.Bind(v); err != nil { +// return err +// } +// +// return c.EnableValidate(v) +// } + +func (c *DefaultCtx) Validate(v any) error { + if c.app.config.Validator == nil { + return NilValidatorError{} + } + return c.app.config.Validator.Validate(v) +} + // Body contains the raw body submitted in a POST request. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. @@ -245,6 +264,14 @@ func (c *DefaultCtx) Body() []byte { return body } +func (c *DefaultCtx) BodyJSON(v any) error { + return c.app.config.JSONDecoder(c.Body(), v) +} + +func (c *DefaultCtx) BodyXML(v any) error { + return c.app.config.XMLDecoder(c.Body(), v) +} + // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. func (c *DefaultCtx) ClearCookie(key ...string) { @@ -836,7 +863,7 @@ func (c *DefaultCtx) Redirect(location string, status ...int) error { return nil } -// Add vars to default view var map binding to template engine. +// BindVars Add vars to default view var map binding to template engine. // Variables are read by the Render method and may be overwritten. func (c *DefaultCtx) BindVars(vars Map) error { // init viewBindMap - lazy map diff --git a/ctx_interface.go b/ctx_interface.go index d18ae3d18b5..98e9cd31224 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -42,11 +42,28 @@ type Ctx interface { // BaseURL returns (protocol + host + base path). BaseURL() string + // Bind unmarshal request data from context add assign to struct fields. + // You can bind cookie, headers etc. into basic type, slice, or any customized binders by + // implementing [encoding.TextUnmarshaler] or [bind.Unmarshaler]. + // Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser + Bind() *Bind + + // BindWithValidate is an alias for `context.Bind` and `context.EnableValidate` + // BindWithValidate(v any) error + + Validate(v any) error + // Body contains the raw body submitted in a POST request. // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. Body() []byte + // BodyJSON will unmarshal request body with Config.JSONDecoder + BodyJSON(v any) error + + // BodyXML will unmarshal request body with Config.XMLDecoder + BodyXML(v any) error + // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. ClearCookie(key ...string) @@ -227,7 +244,7 @@ type Ctx interface { // If status is not specified, status defaults to 302 Found. Redirect(location string, status ...int) error - // Add vars to default view var map binding to template engine. + // BindVars Add vars to default view var map binding to template engine. // Variables are read by the Render method and may be overwritten. BindVars(vars Map) error diff --git a/error.go b/error.go index d6aee39d991..965d7124505 100644 --- a/error.go +++ b/error.go @@ -1,11 +1,36 @@ package fiber import ( - goErrors "errors" + "errors" + "reflect" ) // Range errors var ( - ErrRangeMalformed = goErrors.New("range: malformed range header string") - ErrRangeUnsatisfiable = goErrors.New("range: unsatisfiable range") + ErrRangeMalformed = errors.New("range: malformed range header string") + ErrRangeUnsatisfiable = errors.New("range: unsatisfiable range") ) + +// NilValidatorError is the validate error when context.EnableValidate is called but no validator is set in config. +type NilValidatorError struct { +} + +func (n NilValidatorError) Error() string { + return "fiber: ctx.EnableValidate(v any) is called without validator" +} + +// InvalidBinderError is the error when try to bind unsupported type. +type InvalidBinderError struct { + Type reflect.Type +} + +func (e *InvalidBinderError) Error() string { + if e.Type == nil { + return "fiber: Bind(nil)" + } + + if e.Type.Kind() != reflect.Pointer { + return "fiber: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "fiber: Bind(nil " + e.Type.String() + ")" +} diff --git a/internal/bind/bool.go b/internal/bind/bool.go new file mode 100644 index 00000000000..a7f207cea38 --- /dev/null +++ b/internal/bind/bool.go @@ -0,0 +1,18 @@ +package bind + +import ( + "reflect" + "strconv" +) + +type boolDecoder struct { +} + +func (d *boolDecoder) UnmarshalString(s string, fieldValue reflect.Value) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + fieldValue.SetBool(v) + return nil +} diff --git a/internal/bind/compile.go b/internal/bind/compile.go new file mode 100644 index 00000000000..da5ca7ae660 --- /dev/null +++ b/internal/bind/compile.go @@ -0,0 +1,49 @@ +package bind + +import ( + "encoding" + "errors" + "reflect" +) + +type TextDecoder interface { + UnmarshalString(s string, fieldValue reflect.Value) error +} + +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + +func CompileTextDecoder(rt reflect.Type) (TextDecoder, error) { + // encoding.TextUnmarshaler + if reflect.PtrTo(rt).Implements(textUnmarshalerType) { + return &textUnmarshalEncoder{fieldType: rt}, nil + } + + switch rt.Kind() { + case reflect.Bool: + return &boolDecoder{}, nil + case reflect.Uint8: + return &uintDecoder{bitSize: 8}, nil + case reflect.Uint16: + return &uintDecoder{bitSize: 16}, nil + case reflect.Uint32: + return &uintDecoder{bitSize: 32}, nil + case reflect.Uint64: + return &uintDecoder{bitSize: 64}, nil + case reflect.Uint: + return &uintDecoder{}, nil + case reflect.Int8: + return &intDecoder{bitSize: 8}, nil + case reflect.Int16: + return &intDecoder{bitSize: 16}, nil + case reflect.Int32: + return &intDecoder{bitSize: 32}, nil + case reflect.Int64: + return &intDecoder{bitSize: 64}, nil + case reflect.Int: + return &intDecoder{}, nil + case reflect.String: + return &stringDecoder{}, nil + } + + return nil, errors.New("unsupported type " + rt.String()) +} diff --git a/internal/bind/int.go b/internal/bind/int.go new file mode 100644 index 00000000000..6b1cb4855d0 --- /dev/null +++ b/internal/bind/int.go @@ -0,0 +1,19 @@ +package bind + +import ( + "reflect" + "strconv" +) + +type intDecoder struct { + bitSize int +} + +func (d *intDecoder) UnmarshalString(s string, fieldValue reflect.Value) error { + v, err := strconv.ParseInt(s, 10, d.bitSize) + if err != nil { + return err + } + fieldValue.SetInt(v) + return nil +} diff --git a/internal/bind/string.go b/internal/bind/string.go new file mode 100644 index 00000000000..521b2277b79 --- /dev/null +++ b/internal/bind/string.go @@ -0,0 +1,15 @@ +package bind + +import ( + "reflect" + + "github.com/gofiber/fiber/v3/utils" +) + +type stringDecoder struct { +} + +func (d *stringDecoder) UnmarshalString(s string, fieldValue reflect.Value) error { + fieldValue.SetString(utils.CopyString(s)) + return nil +} diff --git a/internal/bind/text_unmarshaler.go b/internal/bind/text_unmarshaler.go new file mode 100644 index 00000000000..55b5b5811db --- /dev/null +++ b/internal/bind/text_unmarshaler.go @@ -0,0 +1,27 @@ +package bind + +import ( + "encoding" + "reflect" +) + +type textUnmarshalEncoder struct { + fieldType reflect.Type +} + +func (d *textUnmarshalEncoder) UnmarshalString(s string, fieldValue reflect.Value) error { + if s == "" { + return nil + } + + v := reflect.New(d.fieldType) + unmarshaler := v.Interface().(encoding.TextUnmarshaler) + + if err := unmarshaler.UnmarshalText([]byte(s)); err != nil { + return err + } + + fieldValue.Set(v.Elem()) + + return nil +} diff --git a/internal/bind/uint.go b/internal/bind/uint.go new file mode 100644 index 00000000000..8cccc953789 --- /dev/null +++ b/internal/bind/uint.go @@ -0,0 +1,19 @@ +package bind + +import ( + "reflect" + "strconv" +) + +type uintDecoder struct { + bitSize int +} + +func (d *uintDecoder) UnmarshalString(s string, fieldValue reflect.Value) error { + v, err := strconv.ParseUint(s, 10, d.bitSize) + if err != nil { + return err + } + fieldValue.SetUint(v) + return nil +} diff --git a/internal/reflectunsafe/reflectunsafe.go b/internal/reflectunsafe/reflectunsafe.go new file mode 100644 index 00000000000..7416da003be --- /dev/null +++ b/internal/reflectunsafe/reflectunsafe.go @@ -0,0 +1,12 @@ +package reflectunsafe + +import ( + "reflect" + "unsafe" +) + +func ValueAndTypeID(v any) (reflect.Value, uintptr) { + rv := reflect.ValueOf(v) + rt := rv.Type() + return rv, (*[2]uintptr)(unsafe.Pointer(&rt))[1] +} diff --git a/utils/xml.go b/utils/xml.go index 9cc23512b07..f205cb66330 100644 --- a/utils/xml.go +++ b/utils/xml.go @@ -2,3 +2,9 @@ package utils // XMLMarshal returns the XML encoding of v. type XMLMarshal func(v any) ([]byte, error) + +// XMLUnmarshal parses the XML-encoded data and stores the result in +// the value pointed to by v, which must be an arbitrary struct, +// slice, or string. Well-formed data that does not fit into v is +// discarded. +type XMLUnmarshal func([]byte, any) error diff --git a/validate.go b/validate.go new file mode 100644 index 00000000000..72dfee6ca90 --- /dev/null +++ b/validate.go @@ -0,0 +1,5 @@ +package fiber + +type Validator interface { + Validate(v any) error +} From a6696e5da14794577c6b04214bd2a74f31c69d83 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 31 Aug 2022 21:41:27 +0800 Subject: [PATCH 03/22] replace panic with returning error --- binder_compile.go | 2 +- error.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/binder_compile.go b/binder_compile.go index 68eb47a2e12..b111b21ac14 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -28,7 +28,7 @@ func compileReqParser(rt reflect.Type) (Decoder, error) { el := rt.Elem() if el.Kind() != reflect.Struct { - panic("wrapped request need to struct") + return nil, &UnsupportedBinderError{Type: rt} } for i := 0; i < el.NumField(); i++ { diff --git a/error.go b/error.go index 965d7124505..0e81e4ebae8 100644 --- a/error.go +++ b/error.go @@ -19,7 +19,7 @@ func (n NilValidatorError) Error() string { return "fiber: ctx.EnableValidate(v any) is called without validator" } -// InvalidBinderError is the error when try to bind unsupported type. +// InvalidBinderError is the error when try to bind invalid value. type InvalidBinderError struct { Type reflect.Type } @@ -34,3 +34,12 @@ func (e *InvalidBinderError) Error() string { } return "fiber: Bind(nil " + e.Type.String() + ")" } + +// UnsupportedBinderError is the error when try to bind unsupported type. +type UnsupportedBinderError struct { + Type reflect.Type +} + +func (e *UnsupportedBinderError) Error() string { + return "unsupported binder: ctx.Bind().Req(" + e.Type.String() + "), only binding struct is supported new" +} From b5eeaa427a70a5ecee5a020c3383017dd76a772f Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 31 Aug 2022 21:49:43 +0800 Subject: [PATCH 04/22] get typeID like stdlilb reflect --- internal/reflectunsafe/reflectunsafe.go | 10 ++++++++-- internal/reflectunsafe/reflectunsafe_test.go | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 internal/reflectunsafe/reflectunsafe_test.go diff --git a/internal/reflectunsafe/reflectunsafe.go b/internal/reflectunsafe/reflectunsafe.go index 7416da003be..85a906ca7a9 100644 --- a/internal/reflectunsafe/reflectunsafe.go +++ b/internal/reflectunsafe/reflectunsafe.go @@ -6,7 +6,13 @@ import ( ) func ValueAndTypeID(v any) (reflect.Value, uintptr) { + header := (*emptyInterface)(unsafe.Pointer(&v)) + rv := reflect.ValueOf(v) - rt := rv.Type() - return rv, (*[2]uintptr)(unsafe.Pointer(&rt))[1] + return rv, header.typeID +} + +type emptyInterface struct { + typeID uintptr + dataPtr unsafe.Pointer } diff --git a/internal/reflectunsafe/reflectunsafe_test.go b/internal/reflectunsafe/reflectunsafe_test.go new file mode 100644 index 00000000000..7532cc4e916 --- /dev/null +++ b/internal/reflectunsafe/reflectunsafe_test.go @@ -0,0 +1,16 @@ +package reflectunsafe_test + +import ( + "testing" + + "github.com/gofiber/fiber/v3/internal/reflectunsafe" + "github.com/stretchr/testify/require" +) + +func TestTypeID(t *testing.T) { + _, intType := reflectunsafe.ValueAndTypeID(int(1)) + _, uintType := reflectunsafe.ValueAndTypeID(uint(1)) + _, shouldBeIntType := reflectunsafe.ValueAndTypeID(int(1)) + require.NotEqual(t, intType, uintType) + require.Equal(t, intType, shouldBeIntType) +} From ffc1c41d4a2eecf9338446b51c44fc6095d6259d Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 31 Aug 2022 23:18:48 +0800 Subject: [PATCH 05/22] support form and multipart --- app.go | 4 ++ bind_test.go | 89 +++++++++++++++++++++++++++++++++++ binder.go | 115 ++++++++++++++++++++++++++++++++++++++++++++-- binder_compile.go | 47 +++++++++++++++++-- binder_slice.go | 17 +++++++ binder_test.go | 32 ------------- ctx.go | 4 -- 7 files changed, 264 insertions(+), 44 deletions(-) delete mode 100644 binder_test.go diff --git a/app.go b/app.go index 9b40b4084b2..c346f7075df 100644 --- a/app.go +++ b/app.go @@ -119,6 +119,10 @@ type App struct { tlsHandler *tlsHandler // bind decoder cache bindDecoderCache sync.Map + // form decoder cache + formDecoderCache sync.Map + // multipart decoder cache + multipartDecoderCache sync.Map } // Config is a struct holding the server settings. diff --git a/bind_test.go b/bind_test.go index 1f1f4aca4d8..1a680b3fb5e 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1,6 +1,9 @@ package fiber import ( + "bytes" + "fmt" + "mime/multipart" "net/url" "regexp" "strconv" @@ -11,6 +14,30 @@ import ( "github.com/valyala/fasthttp" ) +func Test_Binder(t *testing.T) { + t.Parallel() + app := New() + + ctx := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} + ctx.Request().SetBody([]byte(`{"name": "john doe"}`)) + ctx.Request().Header.Set("content-type", "application/json") + + var req struct { + ID string `param:"id"` + } + + var body struct { + Name string `json:"name"` + } + + err := ctx.Bind().Req(&req).JSON(&body).Err() + require.NoError(t, err) + require.Equal(t, "id string", req.ID) + require.Equal(t, "john doe", body.Name) +} + // go test -run Test_Bind_BasicType -v func Test_Bind_BasicType(t *testing.T) { t.Parallel() @@ -252,6 +279,68 @@ func Test_Bind_error_message(t *testing.T) { require.Regexp(t, regexp.MustCompile(`unable to decode 'john' as time`), err.Error()) } +func Test_Bind_Form(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + + c.Context().Request.Header.Set(HeaderContentType, MIMEApplicationForm) + c.Context().Request.SetBody([]byte(url.Values{ + "username": {"u"}, + "password": {"p"}, + "likes": {"apple", "banana"}, + }.Encode())) + + type Req struct { + Username string `form:"username"` + Password string `form:"password"` + Likes []string `form:"likes"` + } + + var r Req + err := c.Bind().Form(&r).Err() + + require.NoError(t, err) + require.Equal(t, "u", r.Username) + require.Equal(t, "p", r.Password) + require.Equal(t, []string{"apple", "banana"}, r.Likes) +} + +func Test_Bind_Multipart(t *testing.T) { + t.Parallel() + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + + buf := bytes.NewBuffer(nil) + boundary := multipart.NewWriter(nil).Boundary() + err := fasthttp.WriteMultipartForm(buf, &multipart.Form{ + Value: map[string][]string{ + "username": {"u"}, + "password": {"p"}, + "likes": {"apple", "banana"}, + }, + }, boundary) + + require.NoError(t, err) + + c.Context().Request.Header.Set(HeaderContentType, fmt.Sprintf("%s; boundary=%s", MIMEMultipartForm, boundary)) + c.Context().Request.SetBody(buf.Bytes()) + + type Req struct { + Username string `multipart:"username"` + Password string `multipart:"password"` + Likes []string `multipart:"likes"` + } + + var r Req + err = c.Bind().Multipart(&r).Err() + require.NoError(t, err) + + require.Equal(t, "u", r.Username) + require.Equal(t, "p", r.Password) + require.Equal(t, []string{"apple", "banana"}, r.Likes) +} + type Req struct { ID int `query:"id"` diff --git a/binder.go b/binder.go index 4ce2f1b6b7f..399c03a6e02 100644 --- a/binder.go +++ b/binder.go @@ -4,17 +4,28 @@ import ( "bytes" "net/http" "reflect" + "sync" "github.com/gofiber/fiber/v3/internal/reflectunsafe" "github.com/gofiber/fiber/v3/utils" ) +var binderPool = sync.Pool{New: func() any { + return &Bind{} +}} + type Bind struct { err error ctx Ctx val any // last decoded val } +func (c *DefaultCtx) Bind() *Bind { + b := binderPool.Get().(*Bind) + b.ctx = c + return b +} + func (b *Bind) setErr(err error) *Bind { b.err = err return b @@ -32,8 +43,21 @@ func (b *Bind) HTTPErr() error { return nil } +func (b *Bind) reset() { + b.ctx = nil + b.val = nil + b.err = nil +} + +// Err return binding error and put binder back to pool +// it's not safe to use after Err is called. func (b *Bind) Err() error { - return b.err + err := b.err + + b.reset() + binderPool.Put(b) + + return err } // JSON unmarshal body as json @@ -74,14 +98,53 @@ func (b *Bind) XML(v any) *Bind { return b } +// Form unmarshal body as form +func (b *Bind) Form(v any) *Bind { + if b.err != nil { + return b + } + + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationForm)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/x-www-form-urlencoded\"")) + } + + if err := b.formDecode(v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + +// Multipart unmarshal body as multipart/form-data +// TODO: handle multipart files. +func (b *Bind) Multipart(v any) *Bind { + if b.err != nil { + return b + } + + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEMultipartForm)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"multipart/form-data\"")) + } + + if err := b.multipartDecode(v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + func (b *Bind) Req(v any) *Bind { if b.err != nil { return b } - if err := b.decode(v); err != nil { + if err := b.reqDecode(v); err != nil { return b.setErr(err) } + + b.val = v return b } @@ -101,7 +164,7 @@ func (b *Bind) Validate() *Bind { return b } -func (b *Bind) decode(v any) error { +func (b *Bind) reqDecode(v any) error { rv, typeID := reflectunsafe.ValueAndTypeID(v) if rv.Kind() != reflect.Pointer || rv.IsNil() { return &InvalidBinderError{Type: reflect.TypeOf(v)} @@ -114,7 +177,7 @@ func (b *Bind) decode(v any) error { return decoder(b.ctx, rv.Elem()) } - decoder, err := compileReqParser(rv.Type()) + decoder, err := compileReqParser(rv.Type(), bindCompileOption{reqDecoder: true}) if err != nil { return err } @@ -122,3 +185,47 @@ func (b *Bind) decode(v any) error { b.ctx.App().bindDecoderCache.Store(typeID, decoder) return decoder(b.ctx, rv.Elem()) } + +func (b *Bind) formDecode(v any) error { + rv, typeID := reflectunsafe.ValueAndTypeID(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidBinderError{Type: reflect.TypeOf(v)} + } + + cached, ok := b.ctx.App().formDecoderCache.Load(typeID) + if ok { + // cached decoder, fast path + decoder := cached.(Decoder) + return decoder(b.ctx, rv.Elem()) + } + + decoder, err := compileReqParser(rv.Type(), bindCompileOption{bodyDecoder: true}) + if err != nil { + return err + } + + b.ctx.App().formDecoderCache.Store(typeID, decoder) + return decoder(b.ctx, rv.Elem()) +} + +func (b *Bind) multipartDecode(v any) error { + rv, typeID := reflectunsafe.ValueAndTypeID(v) + if rv.Kind() != reflect.Pointer || rv.IsNil() { + return &InvalidBinderError{Type: reflect.TypeOf(v)} + } + + cached, ok := b.ctx.App().multipartDecoderCache.Load(typeID) + if ok { + // cached decoder, fast path + decoder := cached.(Decoder) + return decoder(b.ctx, rv.Elem()) + } + + decoder, err := compileReqParser(rv.Type(), bindCompileOption{bodyDecoder: true}) + if err != nil { + return err + } + + b.ctx.App().multipartDecoderCache.Store(typeID, decoder) + return decoder(b.ctx, rv.Elem()) +} diff --git a/binder_compile.go b/binder_compile.go index b111b21ac14..59ae105a8d6 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -20,10 +20,18 @@ const bindTagQuery = "query" const bindTagParam = "param" const bindTagCookie = "cookie" +const bindTagForm = "form" +const bindTagMultipart = "multipart" + var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() var bindUnmarshalerType = reflect.TypeOf((*Binder)(nil)).Elem() -func compileReqParser(rt reflect.Type) (Decoder, error) { +type bindCompileOption struct { + bodyDecoder bool // to parse `form` or `multipart/form-data` + reqDecoder bool // to parse header/cookie/param/query/header/respHeader +} + +func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { var decoders []decoder el := rt.Elem() @@ -37,7 +45,7 @@ func compileReqParser(rt reflect.Type) (Decoder, error) { continue } - dec, err := compileFieldDecoder(el.Field(i), i) + dec, err := compileFieldDecoder(el.Field(i), i, opt) if err != nil { return nil, err } @@ -59,13 +67,18 @@ func compileReqParser(rt reflect.Type) (Decoder, error) { }, nil } -func compileFieldDecoder(field reflect.StructField, index int) (decoder, error) { +func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption) (decoder, error) { if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { return &fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}, nil } + var tags = []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} + if opt.bodyDecoder { + tags = []string{bindTagForm, bindTagMultipart} + } + var tagScope = "" - for _, loopTagScope := range []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} { + for _, loopTagScope := range tags { if _, ok := field.Tag.Lookup(loopTagScope); ok { tagScope = loopTagScope break @@ -89,6 +102,24 @@ func compileFieldDecoder(field reflect.StructField, index int) (decoder, error) return compileTextBasedDecoder(field, index, tagScope, tagContent) } +func formGetter(ctx Ctx, key string, defaultValue ...string) string { + return utils.UnsafeString(ctx.Request().PostArgs().Peek(key)) +} + +func multipartGetter(ctx Ctx, key string, defaultValue ...string) string { + f, err := ctx.Request().MultipartForm() + if err != nil { + return "" + } + + v, ok := f.Value[key] + if !ok { + return "" + } + + return v[0] +} + func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string) (decoder, error) { var get func(ctx Ctx, key string, defaultValue ...string) string switch tagScope { @@ -102,6 +133,10 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag get = Ctx.Params case bindTagCookie: get = Ctx.Cookies + case bindTagMultipart: + get = multipartGetter + case bindTagForm: + get = formGetter default: return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } @@ -145,6 +180,10 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag eqBytes = utils.EqualFold[[]byte] case bindTagCookie: visitAll = visitCookie + case bindTagForm: + visitAll = visitForm + case bindTagMultipart: + visitAll = visitMultipart case bindTagParam: return nil, errors.New("using params with slice type is not supported") default: diff --git a/binder_slice.go b/binder_slice.go index c9031abfe98..3f02f108d52 100644 --- a/binder_slice.go +++ b/binder_slice.go @@ -74,3 +74,20 @@ func visitResHeader(ctx Ctx, f func(key []byte, value []byte)) { func visitCookie(ctx Ctx, f func(key []byte, value []byte)) { ctx.Request().Header.VisitAllCookie(f) } + +func visitForm(ctx Ctx, f func(key []byte, value []byte)) { + ctx.Request().PostArgs().VisitAll(f) +} + +func visitMultipart(ctx Ctx, f func(key []byte, value []byte)) { + mp, err := ctx.Request().MultipartForm() + if err != nil { + return + } + + for key, values := range mp.Value { + for _, value := range values { + f(utils.UnsafeBytes(key), utils.UnsafeBytes(value)) + } + } +} diff --git a/binder_test.go b/binder_test.go deleted file mode 100644 index 862969a334c..00000000000 --- a/binder_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package fiber - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" -) - -func Test_Binder(t *testing.T) { - t.Parallel() - app := New() - - ctx := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - ctx.values = [maxParams]string{"id string"} - ctx.route = &Route{Params: []string{"id"}} - ctx.Request().SetBody([]byte(`{"name": "john doe"}`)) - ctx.Request().Header.Set("content-type", "application/json") - - var req struct { - ID string `param:"id"` - } - - var body struct { - Name string `json:"name"` - } - - err := ctx.Bind().Req(&req).JSON(&body).Err() - require.NoError(t, err) - require.Equal(t, "id string", req.ID) - require.Equal(t, "john doe", body.Name) -} diff --git a/ctx.go b/ctx.go index 18873635523..22939b6f092 100644 --- a/ctx.go +++ b/ctx.go @@ -213,10 +213,6 @@ func (c *DefaultCtx) BaseURL() string { return c.baseURI } -func (c *DefaultCtx) Bind() *Bind { - return &Bind{ctx: c} -} - // func (c *DefaultCtx) BindWithValidate(v any) error { // if err := c.Bind(v); err != nil { // return err From 9887ac5979d0134e8d6ed158d8b3b79dedf5d202 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 31 Aug 2022 23:26:23 +0800 Subject: [PATCH 06/22] move internal/reflectunsafe into internal/bind --- binder.go | 8 ++++---- .../reflectunsafe.go => bind/reflect.go} | 2 +- internal/bind/reflect_test.go | 16 ++++++++++++++++ internal/reflectunsafe/reflectunsafe_test.go | 16 ---------------- 4 files changed, 21 insertions(+), 21 deletions(-) rename internal/{reflectunsafe/reflectunsafe.go => bind/reflect.go} (92%) create mode 100644 internal/bind/reflect_test.go delete mode 100644 internal/reflectunsafe/reflectunsafe_test.go diff --git a/binder.go b/binder.go index 399c03a6e02..e651f323dc6 100644 --- a/binder.go +++ b/binder.go @@ -6,7 +6,7 @@ import ( "reflect" "sync" - "github.com/gofiber/fiber/v3/internal/reflectunsafe" + "github.com/gofiber/fiber/v3/internal/bind" "github.com/gofiber/fiber/v3/utils" ) @@ -165,7 +165,7 @@ func (b *Bind) Validate() *Bind { } func (b *Bind) reqDecode(v any) error { - rv, typeID := reflectunsafe.ValueAndTypeID(v) + rv, typeID := bind.ValueAndTypeID(v) if rv.Kind() != reflect.Pointer || rv.IsNil() { return &InvalidBinderError{Type: reflect.TypeOf(v)} } @@ -187,7 +187,7 @@ func (b *Bind) reqDecode(v any) error { } func (b *Bind) formDecode(v any) error { - rv, typeID := reflectunsafe.ValueAndTypeID(v) + rv, typeID := bind.ValueAndTypeID(v) if rv.Kind() != reflect.Pointer || rv.IsNil() { return &InvalidBinderError{Type: reflect.TypeOf(v)} } @@ -209,7 +209,7 @@ func (b *Bind) formDecode(v any) error { } func (b *Bind) multipartDecode(v any) error { - rv, typeID := reflectunsafe.ValueAndTypeID(v) + rv, typeID := bind.ValueAndTypeID(v) if rv.Kind() != reflect.Pointer || rv.IsNil() { return &InvalidBinderError{Type: reflect.TypeOf(v)} } diff --git a/internal/reflectunsafe/reflectunsafe.go b/internal/bind/reflect.go similarity index 92% rename from internal/reflectunsafe/reflectunsafe.go rename to internal/bind/reflect.go index 85a906ca7a9..bd4ee7ecdc3 100644 --- a/internal/reflectunsafe/reflectunsafe.go +++ b/internal/bind/reflect.go @@ -1,4 +1,4 @@ -package reflectunsafe +package bind import ( "reflect" diff --git a/internal/bind/reflect_test.go b/internal/bind/reflect_test.go new file mode 100644 index 00000000000..eec58bff01f --- /dev/null +++ b/internal/bind/reflect_test.go @@ -0,0 +1,16 @@ +package bind_test + +import ( + "testing" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/stretchr/testify/require" +) + +func TestTypeID(t *testing.T) { + _, intType := bind.ValueAndTypeID(int(1)) + _, uintType := bind.ValueAndTypeID(uint(1)) + _, shouldBeIntType := bind.ValueAndTypeID(int(1)) + require.NotEqual(t, intType, uintType) + require.Equal(t, intType, shouldBeIntType) +} diff --git a/internal/reflectunsafe/reflectunsafe_test.go b/internal/reflectunsafe/reflectunsafe_test.go deleted file mode 100644 index 7532cc4e916..00000000000 --- a/internal/reflectunsafe/reflectunsafe_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package reflectunsafe_test - -import ( - "testing" - - "github.com/gofiber/fiber/v3/internal/reflectunsafe" - "github.com/stretchr/testify/require" -) - -func TestTypeID(t *testing.T) { - _, intType := reflectunsafe.ValueAndTypeID(int(1)) - _, uintType := reflectunsafe.ValueAndTypeID(uint(1)) - _, shouldBeIntType := reflectunsafe.ValueAndTypeID(int(1)) - require.NotEqual(t, intType, uintType) - require.Equal(t, intType, shouldBeIntType) -} From c8bc2e44cadfef600fd75491b98edd77cfce46d2 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 31 Aug 2022 23:51:30 +0800 Subject: [PATCH 07/22] make content-type checking optional --- bind_readme.md | 15 ++++++++--- binder.go | 65 ++++++++++++++++++++++++++++++------------------ ctx.go | 8 ------ ctx_interface.go | 6 ----- 4 files changed, 53 insertions(+), 41 deletions(-) diff --git a/bind_readme.md b/bind_readme.md index 77cc5773bca..c9364696d20 100644 --- a/bind_readme.md +++ b/bind_readme.md @@ -109,9 +109,10 @@ you need. ### Parse Request Body -you can call `ctx.BodyJSON(v any) error` or `BodyXML(v any) error` +you can call `Bind().JSON(v any)` / `Bind().XML(v any)` / `Bind().Form(v any)` / `Bind().Multipart(v any)` +to unmarshal request Body. -These methods will check content-type HTTP header and call configured JSON or XML decoder to unmarshal. +use `Bind().Strict()` to enable content-type checking. ```golang package main @@ -127,13 +128,21 @@ type Body struct { func main() { app := fiber.New() - app.Get("/:id", func(c fiber.Ctx) error { + app.Get("/", func(c fiber.Ctx) error { var data Body if err := c.Bind().JSON(&data).Err(); err != nil { return err } return c.JSON(data) }) + + app.Get("/strict", func(c fiber.Ctx) error { + var data Body + if err := c.Bind().Strict().JSON(&data).Err(); err != nil { + return err + } + return c.JSON(data) + }) } ``` diff --git a/binder.go b/binder.go index e651f323dc6..1f2d651a720 100644 --- a/binder.go +++ b/binder.go @@ -15,9 +15,10 @@ var binderPool = sync.Pool{New: func() any { }} type Bind struct { - err error - ctx Ctx - val any // last decoded val + err error + ctx Ctx + val any // last decoded val + strict bool } func (c *DefaultCtx) Bind() *Bind { @@ -26,29 +27,39 @@ func (c *DefaultCtx) Bind() *Bind { return b } +func (b *Bind) Strict() *Bind { + b.strict = true + return b +} + func (b *Bind) setErr(err error) *Bind { b.err = err return b } +func (b *Bind) reset() { + b.ctx = nil + b.val = nil + b.err = nil + b.strict = false +} + +// HTTPErr return a wrapped fiber.Error for 400 http bad request. +// it's not safe to use after HTTPErr is called. func (b *Bind) HTTPErr() error { - if b.err != nil { - if fe, ok := b.err.(*Error); ok { + err := b.Err() + + if err != nil { + if fe, ok := err.(*Error); ok { return fe } - return NewError(http.StatusBadRequest, b.err.Error()) + return NewError(http.StatusBadRequest, err.Error()) } return nil } -func (b *Bind) reset() { - b.ctx = nil - b.val = nil - b.err = nil -} - // Err return binding error and put binder back to pool // it's not safe to use after Err is called. func (b *Bind) Err() error { @@ -61,17 +72,18 @@ func (b *Bind) Err() error { } // JSON unmarshal body as json -// unlike `ctx.BodyJSON`, this will also check "content-type" HTTP header. func (b *Bind) JSON(v any) *Bind { if b.err != nil { return b } - if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationJSON)) { - return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/json\"")) + if b.strict { + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationJSON)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/json\"")) + } } - if err := b.ctx.BodyJSON(v); err != nil { + if err := b.ctx.App().config.JSONDecoder(b.ctx.Body(), v); err != nil { return b.setErr(err) } @@ -80,17 +92,18 @@ func (b *Bind) JSON(v any) *Bind { } // XML unmarshal body as xml -// unlike `ctx.BodyXML`, this will also check "content-type" HTTP header. func (b *Bind) XML(v any) *Bind { if b.err != nil { return b } - if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationXML)) { - return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/xml\"")) + if b.strict { + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationXML)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/xml\"")) + } } - if err := b.ctx.BodyXML(v); err != nil { + if err := b.ctx.App().config.XMLDecoder(b.ctx.Body(), v); err != nil { return b.setErr(err) } @@ -104,8 +117,10 @@ func (b *Bind) Form(v any) *Bind { return b } - if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationForm)) { - return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/x-www-form-urlencoded\"")) + if b.strict { + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEApplicationForm)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"application/x-www-form-urlencoded\"")) + } } if err := b.formDecode(v); err != nil { @@ -123,8 +138,10 @@ func (b *Bind) Multipart(v any) *Bind { return b } - if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEMultipartForm)) { - return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"multipart/form-data\"")) + if b.strict { + if !bytes.HasPrefix(b.ctx.Request().Header.ContentType(), utils.UnsafeBytes(MIMEMultipartForm)) { + return b.setErr(NewError(http.StatusUnsupportedMediaType, "expecting content-type \"multipart/form-data\"")) + } } if err := b.multipartDecode(v); err != nil { diff --git a/ctx.go b/ctx.go index 22939b6f092..fc383c76443 100644 --- a/ctx.go +++ b/ctx.go @@ -260,14 +260,6 @@ func (c *DefaultCtx) Body() []byte { return body } -func (c *DefaultCtx) BodyJSON(v any) error { - return c.app.config.JSONDecoder(c.Body(), v) -} - -func (c *DefaultCtx) BodyXML(v any) error { - return c.app.config.XMLDecoder(c.Body(), v) -} - // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. func (c *DefaultCtx) ClearCookie(key ...string) { diff --git a/ctx_interface.go b/ctx_interface.go index 98e9cd31224..92e17d63a2c 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -58,12 +58,6 @@ type Ctx interface { // Make copies or use the Immutable setting instead. Body() []byte - // BodyJSON will unmarshal request body with Config.JSONDecoder - BodyJSON(v any) error - - // BodyXML will unmarshal request body with Config.XMLDecoder - BodyXML(v any) error - // ClearCookie expires a specific cookie by key on the client side. // If no key is provided it expires all cookies that came with the request. ClearCookie(key ...string) From 257e79156450ed30a76d134b621495265acb0f16 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 1 Sep 2022 00:28:46 +0800 Subject: [PATCH 08/22] add doc about chaining API --- bind_readme.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bind_readme.md b/bind_readme.md index c9364696d20..6e9b42feca4 100644 --- a/bind_readme.md +++ b/bind_readme.md @@ -109,7 +109,7 @@ you need. ### Parse Request Body -you can call `Bind().JSON(v any)` / `Bind().XML(v any)` / `Bind().Form(v any)` / `Bind().Multipart(v any)` +you can call `Bind().JSON(v any)` / `Bind().XML(v any)` / `Bind().Form(v any)` / `Bind().Multipart(v any)` to unmarshal request Body. use `Bind().Strict()` to enable content-type checking. @@ -179,3 +179,13 @@ func main() { }) } ``` + +### Chaining API + +Binder is expected to be called in chaining, and will do no-op after first error. + +If `ctx.Bind().XML/JSON/Req/Validate/...` meet any error, all calling will be ignored, +and `.Err()` will return the first error encountered. + +For example, if `ctx.Bind().Req(...).JSON(...).Err()` return a non-nil error in `Req(...)`, +binder won't try to decode body as JSON and `.Err()` will return error in `Req(...)` From 556956895d39c89f39a9004354ebef01e98d7392 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sun, 11 Sep 2022 22:50:51 +0800 Subject: [PATCH 09/22] no alloc req headers logger --- middleware/logger/default_logger.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index e5dd1190a39..44ddb56d61e 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -112,11 +112,17 @@ func defaultLogger(c fiber.Ctx, data *LoggerData, cfg Config) error { case TagResBody: return buf.Write(c.Response().Body()) case TagReqHeaders: - reqHeaders := make([]string, 0) + l := c.Request().Header.Len() + i := 0 c.Request().Header.VisitAll(func(k, v []byte) { - reqHeaders = append(reqHeaders, string(k)+"="+string(v)) + buf.Write(k) + buf.WriteString("=") + buf.Write(v) + i++ + if i != l { + buf.WriteString("&") + } }) - return buf.Write([]byte(strings.Join(reqHeaders, "&"))) case TagQueryStringParams: return buf.WriteString(c.Request().URI().QueryArgs().String()) case TagMethod: From d8d0e526e7efa5da09c9e7307bd0309925b294c9 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sun, 11 Sep 2022 23:11:12 +0800 Subject: [PATCH 10/22] handle error --- middleware/logger/default_logger.go | 47 ++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 44ddb56d61e..f44c0d8ab11 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -114,15 +114,23 @@ func defaultLogger(c fiber.Ctx, data *LoggerData, cfg Config) error { case TagReqHeaders: l := c.Request().Header.Len() i := 0 + ew := errWriter{w: buf} c.Request().Header.VisitAll(func(k, v []byte) { - buf.Write(k) - buf.WriteString("=") - buf.Write(v) + if ew.err != nil { + return + } + + ew.Write(k) + ew.WriteString("=") + ew.Write(v) + i++ if i != l { - buf.WriteString("&") + ew.WriteString("&") } }) + + return ew.n, ew.err case TagQueryStringParams: return buf.WriteString(c.Request().URI().QueryArgs().String()) case TagMethod: @@ -220,3 +228,34 @@ func appendInt(buf *bytebufferpool.ByteBuffer, v int) (int, error) { buf.B = fasthttp.AppendUint(buf.B, v) return len(buf.B) - old, nil } + +type errWriter struct { + n int + err error + w *bytebufferpool.ByteBuffer +} + +func (r errWriter) Write(p []byte) { + if r.err != nil { + return + } + + r.write(r.w.Write(p)) +} + +func (r errWriter) WriteString(p string) { + if r.err != nil { + return + } + + r.write(r.w.WriteString(p)) +} + +func (r errWriter) write(n int, err error) { + if err != nil { + r.err = err + return + } + + r.n += n +} From df19a9e5412b612e0c884a92d6deb04788630735 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sun, 11 Sep 2022 23:13:02 +0800 Subject: [PATCH 11/22] lint --- middleware/logger/default_logger.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index f44c0d8ab11..37bd66c291b 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -235,7 +235,7 @@ type errWriter struct { w *bytebufferpool.ByteBuffer } -func (r errWriter) Write(p []byte) { +func (r *errWriter) Write(p []byte) { if r.err != nil { return } @@ -243,7 +243,7 @@ func (r errWriter) Write(p []byte) { r.write(r.w.Write(p)) } -func (r errWriter) WriteString(p string) { +func (r *errWriter) WriteString(p string) { if r.err != nil { return } @@ -251,7 +251,7 @@ func (r errWriter) WriteString(p string) { r.write(r.w.WriteString(p)) } -func (r errWriter) write(n int, err error) { +func (r *errWriter) write(n int, err error) { if err != nil { r.err = err return From 2369002128e5addf492745007542992497fca772 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 22 Sep 2022 09:52:20 +0800 Subject: [PATCH 12/22] bench params --- bind_test.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/bind_test.go b/bind_test.go index 1a680b3fb5e..21cce7f5a0d 100644 --- a/bind_test.go +++ b/bind_test.go @@ -342,7 +342,7 @@ func Test_Bind_Multipart(t *testing.T) { } type Req struct { - ID int `query:"id"` + ID string `params:"id"` I int `query:"I"` J int `query:"j"` @@ -351,11 +351,12 @@ type Req struct { Token string `header:"x-auth"` } -func getCtx() Ctx { +func getBenchCtx() Ctx { app := New() - // TODO: also bench params - ctx := app.NewCtx(&fasthttp.RequestCtx{}) + ctx := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} var u = fasthttp.URI{} u.SetQueryString("j=1&j=123&k=-1") @@ -367,16 +368,13 @@ func getCtx() Ctx { } func Benchmark_Bind_by_hand(b *testing.B) { - ctx := getCtx() + ctx := getBenchCtx() for i := 0; i < b.N; i++ { var req Req var err error - if raw := ctx.Query("id"); raw != "" { - req.ID, err = strconv.Atoi(raw) - if err != nil { - b.Error(err) - b.FailNow() - } + + if raw := ctx.Params("id"); raw != "" { + req.ID = raw } if raw := ctx.Query("i"); raw != "" { @@ -408,10 +406,10 @@ func Benchmark_Bind_by_hand(b *testing.B) { } func Benchmark_Bind(b *testing.B) { - ctx := getCtx() + ctx := getBenchCtx() for i := 0; i < b.N; i++ { var v = Req{} - err := ctx.Bind().Req(&v) + err := ctx.Bind().Req(&v).Err() if err != nil { b.Error(err) b.FailNow() From 0883994468fada1bba9d407bedff6e0b0311d79c Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 22 Sep 2022 09:57:20 +0800 Subject: [PATCH 13/22] remove dead code --- ctx.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ctx.go b/ctx.go index 4cbbafc1c94..fb617adf041 100644 --- a/ctx.go +++ b/ctx.go @@ -213,14 +213,6 @@ func (c *DefaultCtx) BaseURL() string { return c.baseURI } -// func (c *DefaultCtx) BindWithValidate(v any) error { -// if err := c.Bind(v); err != nil { -// return err -// } -// -// return c.EnableValidate(v) -// } - func (c *DefaultCtx) Validate(v any) error { if c.app.config.Validator == nil { return NilValidatorError{} From 4ffac5004a78587446ae61ad64f3c20517568a0e Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 22 Sep 2022 10:22:34 +0800 Subject: [PATCH 14/22] add more doc --- bind_readme.md | 51 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/bind_readme.md b/bind_readme.md index 6e9b42feca4..341626e7af8 100644 --- a/bind_readme.md +++ b/bind_readme.md @@ -16,6 +16,13 @@ It's introduced in Fiber v3 and a replacement of: ## Guides +There are 2 kind of binder in fiber + +- request info binder for basic request, info including query,header,param,respHeader,cookie. +- request body binder, parsing request body like XML or JSON. + +underling fiber will call `app.config.*Decoder` to parse request body, so you need to find parsing details in their own document. + ### Binding basic request info Fiber supports binding basic request data into the struct: @@ -27,11 +34,19 @@ all tags you can use are: - query - param - cookie +- form +- multipart (binding for Request/Response header are case in-sensitive) private and anonymous fields will be ignored. +basically, you can bind all type `int8/int16...uint64/int/uint/float32/float64/string/bool`, you can also bind their slice for non `param` source. + +`int` and `uint`, float and `bool` are parsed by `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat` and `strconv.ParseBool`, if binder failed to parse input string, a error will be returned by binder. + +## Quick Start: + ```go package main @@ -49,7 +64,7 @@ type Req struct { ID int `param:"id"` Q int `query:"q"` Likes []int `query:"likes"` - T time.Time `header:"x-time"` + T time.Time `header:"x-time"` // by time.Time.UnmarshalText, will ben explained later Token string `header:"x-auth"` } @@ -81,7 +96,6 @@ func main() { fmt.Println(resp.StatusCode, string(b)) // Output: 200 {"ID":1,"S":["a","b","c"],"Q":47,"Likes":[1,2],"T":"2022-08-08T08:11:39+08:00","Token":"ttt"} } - ``` ### Defining Custom Binder @@ -90,9 +104,15 @@ We support 2 types of Custom Binder #### a `encoding.TextUnmarshaler` with basic tag config. -like the `time.Time` field in the previous example, if a field implement `encoding.TextUnmarshaler`, it will be called -to -unmarshal raw string we get from request's query/header/... +like the `time.Time` field in the previous example, if a field implement `encoding.TextUnmarshaler`, it will be called to parse raw string we get from request's query/header/... + +Example: + +```golang +type Req struct { + Start time.Time `query:"start_time"` // by time.Time.UnmarshalText, will ben explained later +} +``` #### a `fiber.Binder` interface. @@ -104,8 +124,21 @@ type Binder interface { } ``` -If your type implement `fiber.Binder`, bind will pass current request Context to your and you can unmarshal the info -you need. +If your type implement `fiber.Binder`, bind will pass current request Context to your and you can unmarshal the request info you need. + +Example: + +```golang +type MyBinder struct{} + +func (e *MyBinder) UnmarshalFiberCtx(ctx fiber.Ctx) error { + ... +} + +type Req struct { + Data MyBinder +} +``` ### Parse Request Body @@ -171,7 +204,9 @@ func main() { app.Get("/:id", func(c fiber.Ctx) error { var req struct{} var body struct{} - if err := c.Bind().Req(&req).Validate().JSON(&body).Validate().Err(); err != nil { + if err := c.Bind().Req(&req).Validate(). // will validate &req + JSON(&body).Validate(). // will validate &body + Err(); err != nil { return err } From befee12d5e44408f2644e5e3f27966231f94d1c9 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Fri, 23 Sep 2022 16:07:59 +0800 Subject: [PATCH 15/22] fix test --- redirect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirect.go b/redirect.go index 1148edbf485..ffc478c36d7 100644 --- a/redirect.go +++ b/redirect.go @@ -130,7 +130,7 @@ func filterFlags(content string) string { } func fasthttpArgsToMap(v *fasthttp.Args) map[string]string { - var u map[string]string + var u = make(map[string]string) v.VisitAll(func(key, value []byte) { u[string(key)] = string(value) }) From 6cb876a51bc6d2c4e237fa54b074f9b95bbf032c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Efe=20=C3=87etin?= Date: Thu, 17 Nov 2022 16:00:33 +0300 Subject: [PATCH 16/22] add basic nested binding support (not yet for slices) --- bind.go | 26 +++++++++++++----- bind_test.go | 26 ++++++++++++++++++ binder_compile.go | 68 +++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 102 insertions(+), 18 deletions(-) diff --git a/bind.go b/bind.go index cce399203bf..6d4d18ad9f4 100644 --- a/bind.go +++ b/bind.go @@ -36,12 +36,13 @@ func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { } type fieldTextDecoder struct { - index int - fieldName string - tag string // query,param,header,respHeader ... - reqField string - dec bind.TextDecoder - get func(c Ctx, key string, defaultValue ...string) string + index int + parentIndex []int + fieldName string + tag string // query,param,header,respHeader ... + reqField string + dec bind.TextDecoder + get func(c Ctx, key string, defaultValue ...string) string } func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { @@ -50,7 +51,18 @@ func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } - err := d.dec.UnmarshalString(text, reqValue.Field(d.index)) + var err error + if len(d.parentIndex) > 0 { + for _, i := range d.parentIndex { + reqValue = reqValue.Field(i) + } + + err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) + + } else { + err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) + } + if err != nil { return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err) } diff --git a/bind_test.go b/bind_test.go index 21cce7f5a0d..d3397099504 100644 --- a/bind_test.go +++ b/bind_test.go @@ -38,6 +38,32 @@ func Test_Binder(t *testing.T) { require.Equal(t, "john doe", body.Name) } +func Test_Binder_Nested(t *testing.T) { + t.Parallel() + app := New() + + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("name=tom&nested.and.age=10&nested.and.test=john") + + var req struct { + Name string `query:"name"` + Nested struct { + And struct { + Age int `query:"age"` + Test string `query:"test"` + } `query:"and"` + } `query:"nested"` + } + + err := c.Bind().Req(&req).Err() + require.NoError(t, err) + require.Equal(t, "tom", req.Name) + require.Equal(t, "john", req.Nested.And.Test) + require.Equal(t, 10, req.Nested.And.Age) +} + // go test -run Test_Bind_BasicType -v func Test_Bind_BasicType(t *testing.T) { t.Parallel() diff --git a/binder_compile.go b/binder_compile.go index 2085f506066..38e02b56e74 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -45,13 +45,13 @@ func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { continue } - dec, err := compileFieldDecoder(el.Field(i), i, opt) + dec, err := compileFieldDecoder(el.Field(i), i, opt, parentStruct{}) if err != nil { return nil, err } if dec != nil { - decoders = append(decoders, dec) + decoders = append(decoders, dec...) } } @@ -67,9 +67,14 @@ func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { }, nil } -func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption) (decoder, error) { +type parentStruct struct { + tag string + index []int +} + +func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent parentStruct) ([]decoder, error) { if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { - return &fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}, nil + return []decoder{&fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}}, nil } var tags = []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} @@ -91,6 +96,10 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp tagContent := field.Tag.Get(tagScope) + if parent.tag != "" { + tagContent = parent.tag + "." + tagContent + } + if reflect.PtrTo(field.Type).Implements(textUnmarshalerType) { return compileTextBasedDecoder(field, index, tagScope, tagContent) } @@ -99,7 +108,38 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp return compileSliceFieldTextBasedDecoder(field, index, tagScope, tagContent) } - return compileTextBasedDecoder(field, index, tagScope, tagContent) + // Nested binding support + if field.Type.Kind() == reflect.Struct { + var decoders []decoder + el := field.Type + + for i := 0; i < el.NumField(); i++ { + if !el.Field(i).IsExported() { + // ignore unexported field + continue + } + var indexes []int + if len(parent.index) > 0 { + indexes = append(indexes, parent.index...) + } + indexes = append(indexes, index) + dec, err := compileFieldDecoder(el.Field(i), i, opt, parentStruct{ + tag: tagContent, + index: indexes, + }) + if err != nil { + return nil, err + } + + if dec != nil { + decoders = append(decoders, dec...) + } + } + + return decoders, nil + } + + return compileTextBasedDecoder(field, index, tagScope, tagContent, parent.index) } func formGetter(ctx Ctx, key string, defaultValue ...string) string { @@ -120,7 +160,7 @@ func multipartGetter(ctx Ctx, key string, defaultValue ...string) string { return v[0] } -func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string) (decoder, error) { +func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string, parentIndex ...[]int) ([]decoder, error) { var get func(ctx Ctx, key string, defaultValue ...string) string switch tagScope { case bindTagQuery: @@ -146,17 +186,23 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return nil, err } - return &fieldTextDecoder{ + fieldDecoder := &fieldTextDecoder{ index: index, fieldName: field.Name, tag: tagScope, reqField: tagContent, dec: textDecoder, get: get, - }, nil + } + + if len(parentIndex) > 0 { + fieldDecoder.parentIndex = parentIndex[0] + } + + return []decoder{fieldDecoder}, nil } -func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) (decoder, error) { +func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) ([]decoder, error) { if field.Type.Kind() != reflect.Slice { panic("BUG: unexpected type, expecting slice " + field.Type.String()) } @@ -190,7 +236,7 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - return &fieldSliceDecoder{ + return []decoder{&fieldSliceDecoder{ fieldIndex: index, eqBytes: eqBytes, fieldName: field.Name, @@ -199,5 +245,5 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag fieldType: field.Type, elementType: et, elementDecoder: elementUnmarshaler, - }, nil + }}, nil } From 3661d336c89d8f83452c96d288a3a89a3b0fc362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Efe=20=C3=87etin?= Date: Sun, 20 Nov 2022 19:46:53 +0300 Subject: [PATCH 17/22] add support for queries like data[0][name] (not yet supporting deeper nested levels) --- bind_test.go | 27 ++++++++++++++ binder_compile.go | 70 +++++++++++++++++++++++++++++++------ binder_slice.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 11 deletions(-) diff --git a/bind_test.go b/bind_test.go index d3397099504..ae135ec83f1 100644 --- a/bind_test.go +++ b/bind_test.go @@ -64,6 +64,33 @@ func Test_Binder_Nested(t *testing.T) { require.Equal(t, 10, req.Nested.And.Age) } +func Test_Binder_Nested_Slice(t *testing.T) { + t.Parallel() + app := New() + + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("name=tom&data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12") + + var req struct { + Name string `query:"name"` + Data []struct { + Name string `query:"name"` + Age int `query:"age"` + } `query:"data"` + } + + err := c.Bind().Req(&req).Err() + require.NoError(t, err) + require.Equal(t, 2, len(req.Data)) + require.Equal(t, "john", req.Data[0].Name) + require.Equal(t, 10, req.Data[0].Age) + require.Equal(t, "doe", req.Data[1].Name) + require.Equal(t, 12, req.Data[1].Age) + require.Equal(t, "tom", req.Name) +} + // go test -run Test_Bind_BasicType -v func Test_Bind_BasicType(t *testing.T) { t.Parallel() diff --git a/binder_compile.go b/binder_compile.go index 38e02b56e74..4e0331a014b 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -72,17 +72,12 @@ type parentStruct struct { index []int } -func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent parentStruct) ([]decoder, error) { - if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { - return []decoder{&fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}}, nil - } - +func lookupTagScope(field reflect.StructField, opt bindCompileOption) (tagScope string) { var tags = []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} if opt.bodyDecoder { tags = []string{bindTagForm, bindTagMultipart} } - var tagScope = "" for _, loopTagScope := range tags { if _, ok := field.Tag.Lookup(loopTagScope); ok { tagScope = loopTagScope @@ -90,6 +85,15 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp } } + return +} + +func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent parentStruct) ([]decoder, error) { + if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { + return []decoder{&fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}}, nil + } + + tagScope := lookupTagScope(field, opt) if tagScope == "" { return nil, nil } @@ -202,15 +206,56 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return []decoder{fieldDecoder}, nil } +type subElem struct { + et reflect.Type + tag string + index int + elementDecoder bind.TextDecoder +} + func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) ([]decoder, error) { if field.Type.Kind() != reflect.Slice { panic("BUG: unexpected type, expecting slice " + field.Type.String()) } + var elems []subElem + var elementUnmarshaler bind.TextDecoder + var err error + et := field.Type.Elem() - elementUnmarshaler, err := bind.CompileTextDecoder(et) - if err != nil { - return nil, fmt.Errorf("failed to build slice binder: %w", err) + if et.Kind() == reflect.Struct { + elems = make([]subElem, et.NumField()) + for i := 0; i < et.NumField(); i++ { + if !et.Field(i).IsExported() { + // ignore unexported field + continue + } + + // Skip different tag scopes (main -> sub) + subScope := lookupTagScope(et.Field(i), bindCompileOption{}) + if subScope != tagScope { + continue + } + + elementUnmarshaler, err := bind.CompileTextDecoder(et.Field(i).Type) + if err != nil { + return nil, fmt.Errorf("failed to build slice binder: %w", err) + } + + elem := subElem{ + index: i, + tag: et.Field(i).Tag.Get(subScope), + et: et.Field(i).Type, + elementDecoder: elementUnmarshaler, + } + + elems = append(elems, elem) + } + } else { + elementUnmarshaler, err = bind.CompileTextDecoder(et) + if err != nil { + return nil, fmt.Errorf("failed to build slice binder: %w", err) + } } var eqBytes = bytes.Equal @@ -236,7 +281,8 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - return []decoder{&fieldSliceDecoder{ + fieldSliceDecoder := &fieldSliceDecoder{ + elems: elems, fieldIndex: index, eqBytes: eqBytes, fieldName: field.Name, @@ -245,5 +291,7 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag fieldType: field.Type, elementType: et, elementDecoder: elementUnmarshaler, - }}, nil + } + + return []decoder{fieldSliceDecoder}, nil } diff --git a/binder_slice.go b/binder_slice.go index e3eb828c0ea..409c19079c0 100644 --- a/binder_slice.go +++ b/binder_slice.go @@ -1,7 +1,9 @@ package fiber import ( + "bytes" "reflect" + "strconv" "github.com/gofiber/fiber/v3/internal/bind" "github.com/gofiber/utils/v2" @@ -11,6 +13,7 @@ var _ decoder = (*fieldSliceDecoder)(nil) type fieldSliceDecoder struct { fieldIndex int + elems []subElem fieldName string fieldType reflect.Type reqKey []byte @@ -22,6 +25,10 @@ type fieldSliceDecoder struct { } func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + if d.elementType.Kind() == reflect.Struct { + return d.decodeStruct(ctx, reqValue) + } + count := 0 d.visitAll(ctx, func(key, value []byte) { if d.eqBytes(key, d.reqKey) { @@ -59,6 +66,88 @@ func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } +func (d *fieldSliceDecoder) decodeStruct(ctx Ctx, reqValue reflect.Value) error { + var maxNum int + d.visitAll(ctx, func(key, value []byte) { + start := bytes.IndexByte(key, byte('[')) + end := bytes.IndexByte(key, byte(']')) + + if start != -1 || end != -1 { + num := utils.UnsafeString(key[start+1 : end]) + + if len(num) > 0 { + maxNum, _ = strconv.Atoi(num) + } + } + }) + + if maxNum != 0 { + maxNum += 1 + } + + rv := reflect.MakeSlice(d.fieldType, maxNum, maxNum) + if maxNum == 0 { + reqValue.Field(d.fieldIndex).Set(rv) + return nil + } + + var err error + d.visitAll(ctx, func(key, value []byte) { + if err != nil { + return + } + + if bytes.IndexByte(key, byte('[')) == -1 { + return + } + + // TODO: support queries like data[0][users][0][name] + ints := make([]int, 0) + elems := make([]string, 0) + + // nested + lookupKey := key + for { + start := bytes.IndexByte(lookupKey, byte('[')) + end := bytes.IndexByte(lookupKey, byte(']')) + + if start == -1 || end == -1 { + break + } + + content := utils.UnsafeString(lookupKey[start+1 : end]) + num, errElse := strconv.Atoi(content) + + if errElse == nil { + ints = append(ints, num) + } else { + elems = append(elems, content) + } + + lookupKey = lookupKey[end+1:] + } + + for _, elem := range d.elems { + if elems[0] == elem.tag { + ev := reflect.New(elem.et) + if ee := elem.elementDecoder.UnmarshalString(utils.UnsafeString(value), ev.Elem()); ee != nil { + err = ee + } + + i := rv.Index(ints[0]) + i.Field(elem.index).Set(ev.Elem()) + } + } + }) + + if err != nil { + return err + } + + reqValue.Field(d.fieldIndex).Set(rv) + return nil +} + func visitQuery(ctx Ctx, f func(key []byte, value []byte)) { ctx.Context().QueryArgs().VisitAll(f) } From 41830699d1d8d5e2eafa51a0365a7770fb60cf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Efe=20=C3=87etin?= Date: Sun, 27 Nov 2022 21:34:20 +0300 Subject: [PATCH 18/22] support pointer fields --- bind.go | 22 ++++++++++++++++------ bind_test.go | 36 ++++++++++++++++++++++++++++++++---- binder_compile.go | 13 ++++++++++++- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/bind.go b/bind.go index 6d4d18ad9f4..760d3cab83f 100644 --- a/bind.go +++ b/bind.go @@ -41,6 +41,7 @@ type fieldTextDecoder struct { fieldName string tag string // query,param,header,respHeader ... reqField string + et reflect.Type dec bind.TextDecoder get func(c Ctx, key string, defaultValue ...string) string } @@ -52,17 +53,26 @@ func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { } var err error - if len(d.parentIndex) > 0 { - for _, i := range d.parentIndex { - reqValue = reqValue.Field(i) + for _, i := range d.parentIndex { + reqValue = reqValue.Field(i) + } + + // Pointer support for struct elems + field := reqValue.Field(d.index) + if field.Kind() == reflect.Ptr { + elem := reflect.New(d.et) + err = d.dec.UnmarshalString(text, elem.Elem()) + if err != nil { + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err) } - err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) + field.Set(elem) - } else { - err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) + return nil } + // Non-pointer elems + err = d.dec.UnmarshalString(text, field) if err != nil { return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err) } diff --git a/bind_test.go b/bind_test.go index ae135ec83f1..b94ba4d5af8 100644 --- a/bind_test.go +++ b/bind_test.go @@ -25,7 +25,7 @@ func Test_Binder(t *testing.T) { ctx.Request().Header.Set("content-type", "application/json") var req struct { - ID string `param:"id"` + ID *string `param:"id"` } var body struct { @@ -34,7 +34,7 @@ func Test_Binder(t *testing.T) { err := ctx.Bind().Req(&req).JSON(&body).Err() require.NoError(t, err) - require.Equal(t, "id string", req.ID) + require.Equal(t, "id string", *req.ID) require.Equal(t, "john doe", body.Name) } @@ -47,11 +47,12 @@ func Test_Binder_Nested(t *testing.T) { c.Request().Header.SetContentType("") c.Request().URI().SetQueryString("name=tom&nested.and.age=10&nested.and.test=john") + // TODO: pointer support for structs var req struct { Name string `query:"name"` Nested struct { And struct { - Age int `query:"age"` + Age *int `query:"age"` Test string `query:"test"` } `query:"and"` } `query:"nested"` @@ -61,7 +62,7 @@ func Test_Binder_Nested(t *testing.T) { require.NoError(t, err) require.Equal(t, "tom", req.Name) require.Equal(t, "john", req.Nested.And.Test) - require.Equal(t, 10, req.Nested.And.Age) + require.Equal(t, 10, *req.Nested.And.Age) } func Test_Binder_Nested_Slice(t *testing.T) { @@ -91,6 +92,33 @@ func Test_Binder_Nested_Slice(t *testing.T) { require.Equal(t, "tom", req.Name) } +/*func Test_Binder_Nested_Deeper_Slice(t *testing.T) { + t.Parallel() + app := New() + + c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("data[0][users][0][name]=john&data[0][users][0][age]=10&data[1][users][0][name]=doe&data[1][users][0][age]=12") + + var req struct { + Data []struct { + Users []struct { + Name string `query:"name"` + Age int `query:"age"` + } `query:"subData"` + } `query:"data"` + } + + err := c.Bind().Req(&req).Err() + require.NoError(t, err) + require.Equal(t, 2, len(req.Data)) + require.Equal(t, "john", req.Data[0].Users[0].Name) + require.Equal(t, 10, req.Data[0].Users[0].Age) + require.Equal(t, "doe", req.Data[1].Users[0].Name) + require.Equal(t, 12, req.Data[1].Users[0].Age) +}*/ + // go test -run Test_Bind_BasicType -v func Test_Bind_BasicType(t *testing.T) { t.Parallel() diff --git a/binder_compile.go b/binder_compile.go index 4e0331a014b..3b091389e98 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -113,6 +113,9 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp } // Nested binding support + if field.Type.Kind() == reflect.Ptr { + field.Type = field.Type.Elem() + } if field.Type.Kind() == reflect.Struct { var decoders []decoder el := field.Type @@ -185,7 +188,12 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - textDecoder, err := bind.CompileTextDecoder(field.Type) + et := field.Type + if field.Type.Kind() == reflect.Ptr { + et = field.Type.Elem() + } + + textDecoder, err := bind.CompileTextDecoder(et) if err != nil { return nil, err } @@ -197,6 +205,7 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag reqField: tagContent, dec: textDecoder, get: get, + et: et, } if len(parentIndex) > 0 { @@ -206,11 +215,13 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return []decoder{fieldDecoder}, nil } +// TODO type subElem struct { et reflect.Type tag string index int elementDecoder bind.TextDecoder + //subElems []subElem } func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) ([]decoder, error) { From 7345517868a698498097b7252170ec3f218f7712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Efe=20=C3=87etin?= Date: Wed, 14 Dec 2022 19:22:39 +0300 Subject: [PATCH 19/22] add old methods --- ctx.go | 35 ++++++++++++++++++++++++ ctx_interface.go | 14 ++++++++++ ctx_test.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/ctx.go b/ctx.go index 72f446be1b0..e65377ced93 100644 --- a/ctx.go +++ b/ctx.go @@ -1427,3 +1427,38 @@ func (c *DefaultCtx) IsFromLocal() bool { } return c.isLocalHost(ips[0]) } + +// GetReqHeaders returns the HTTP request headers. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *DefaultCtx) GetReqHeaders() map[string]string { + headers := make(map[string]string) + c.Request().Header.VisitAll(func(k, v []byte) { + headers[string(k)] = c.app.getString(v) + }) + + return headers +} + +// GetRespHeaders returns the HTTP response headers. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *DefaultCtx) GetRespHeaders() map[string]string { + headers := make(map[string]string) + c.Response().Header.VisitAll(func(k, v []byte) { + headers[string(k)] = c.app.getString(v) + }) + + return headers +} + +// AllParams Params is used to get all route parameters. +// Using Params method to get params. +func (c *DefaultCtx) GetParams() map[string]string { + params := make(map[string]string, len(c.route.Params)) + for _, param := range c.route.Params { + params[param] = c.Params(param) + } + + return params +} diff --git a/ctx_interface.go b/ctx_interface.go index 04a224cd9a1..31c0de88bc4 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -342,6 +342,20 @@ type Ctx interface { // ClientHelloInfo return CHI from context ClientHelloInfo() *tls.ClientHelloInfo + // GetReqHeaders returns the HTTP request headers. + // Returned value is only valid within the handler. Do not store any references. + // Make copies or use the Immutable setting instead. + GetReqHeaders() map[string]string + + // GetRespHeaders returns the HTTP response headers. + // Returned value is only valid within the handler. Do not store any references. + // Make copies or use the Immutable setting instead. + GetRespHeaders() map[string]string + + // AllParams Params is used to get all route parameters. + // Using Params method to get params. + GetParams() map[string]string + // SetReq resets fields of context that is relating to request. setReq(fctx *fasthttp.RequestCtx) diff --git a/ctx_test.go b/ctx_test.go index b4c0c55d381..8b0aebdfeea 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -3358,3 +3358,73 @@ func Test_Ctx_IsFromLocal(t *testing.T) { require.False(t, c.IsFromLocal()) } } + +// go test -run Test_Ctx_GetRespHeaders +func Test_Ctx_GetRespHeaders(t *testing.T) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Set("test", "Hello, World πŸ‘‹!") + c.Set("foo", "bar") + c.Response().Header.Set(HeaderContentType, "application/json") + + require.Equal(t, c.GetRespHeaders(), map[string]string{ + "Content-Type": "application/json", + "Foo": "bar", + "Test": "Hello, World πŸ‘‹!", + }) +} + +// go test -run Test_Ctx_GetReqHeaders +func Test_Ctx_GetReqHeaders(t *testing.T) { + app := New() + c := app.NewCtx(&fasthttp.RequestCtx{}) + + c.Request().Header.Set("test", "Hello, World πŸ‘‹!") + c.Request().Header.Set("foo", "bar") + c.Request().Header.Set(HeaderContentType, "application/json") + + require.Equal(t, c.GetReqHeaders(), map[string]string{ + "Content-Type": "application/json", + "Foo": "bar", + "Test": "Hello, World πŸ‘‹!", + }) +} + +// go test -race -run Test_Ctx_GetParams +func Test_Ctx_GetParams(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test/:user", func(c Ctx) error { + require.Equal(t, map[string]string{"user": "john"}, c.GetParams()) + return nil + }) + app.Get("/test2/*", func(c Ctx) error { + require.Equal(t, map[string]string{"*1": "im/a/cookie"}, c.GetParams()) + return nil + }) + app.Get("/test3/*/blafasel/*", func(c Ctx) error { + require.Equal(t, map[string]string{"*1": "1111", "*2": "2222"}, c.GetParams()) + return nil + }) + app.Get("/test4/:optional?", func(c Ctx) error { + require.Equal(t, map[string]string{"optional": ""}, c.GetParams()) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/im/a/cookie", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/1111/blafasel/2222", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test4", nil)) + require.NoError(t, err, "app.Test(req)") + require.Equal(t, StatusOK, resp.StatusCode, "Status code") +} From 081809e8a69649fafb59fa11dd629e9d00117a7a Mon Sep 17 00:00:00 2001 From: fgy Date: Thu, 5 Jan 2023 12:30:11 +0800 Subject: [PATCH 20/22] feat: support float --- bind_test.go | 19 +++++++++++++++++++ internal/bind/compile.go | 4 ++++ internal/bind/float.go | 19 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 internal/bind/float.go diff --git a/bind_test.go b/bind_test.go index b94ba4d5af8..1dcd1b9e286 100644 --- a/bind_test.go +++ b/bind_test.go @@ -497,3 +497,22 @@ func Benchmark_Bind(b *testing.B) { } } } + +func Test_Binder_Float(t *testing.T) { + t.Parallel() + app := New() + + ctx := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"3.14"} + ctx.route = &Route{Params: []string{"id"}} + + var req struct { + ID1 float32 `param:"id"` + ID2 float64 `param:"id"` + } + + err := ctx.Bind().Req(&req).Err() + require.NoError(t, err) + require.Equal(t, float32(3.14), req.ID1) + require.Equal(t, float64(3.14), req.ID2) +} diff --git a/internal/bind/compile.go b/internal/bind/compile.go index da5ca7ae660..252afc6e439 100644 --- a/internal/bind/compile.go +++ b/internal/bind/compile.go @@ -43,6 +43,10 @@ func CompileTextDecoder(rt reflect.Type) (TextDecoder, error) { return &intDecoder{}, nil case reflect.String: return &stringDecoder{}, nil + case reflect.Float32: + return &floatDecoder{bitSize: 32}, nil + case reflect.Float64: + return &floatDecoder{bitSize: 64}, nil } return nil, errors.New("unsupported type " + rt.String()) diff --git a/internal/bind/float.go b/internal/bind/float.go new file mode 100644 index 00000000000..300cbf7b25c --- /dev/null +++ b/internal/bind/float.go @@ -0,0 +1,19 @@ +package bind + +import ( + "reflect" + "strconv" +) + +type floatDecoder struct { + bitSize int +} + +func (d *floatDecoder) UnmarshalString(s string, fieldValue reflect.Value) error { + v, err := strconv.ParseFloat(s, d.bitSize) + if err != nil { + return err + } + fieldValue.SetFloat(v) + return nil +} From 8e6b3bb2e49d875dd46d7a60ba7f6202e2d07991 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sun, 6 Aug 2023 22:56:49 +0300 Subject: [PATCH 21/22] fix mws --- app.go | 8 -------- middleware/idempotency/idempotency.go | 6 +----- middleware/logger/tags.go | 6 +----- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/app.go b/app.go index 51aad01c8fa..e7360678e71 100644 --- a/app.go +++ b/app.go @@ -385,12 +385,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 - // RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. // // Optional. Default: DefaultMethods @@ -725,8 +719,6 @@ func (app *App) Use(args ...any) Router { app.register([]string{methodUse}, prefix, nil, nil, handlers...) } - app.register([]string{methodUse}, prefix, nil, handlers...) - return app } diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 8c922484231..0bdb5eacfae 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -116,11 +116,7 @@ func New(config ...Config) fiber.Handler { Body: utils.CopyBytes(c.Response().Body()), } { - headers := make(map[string]string) - if err := c.Bind().RespHeader(headers); err != nil { - return fmt.Errorf("failed to bind to response headers: %w", err) - } - + headers := c.GetRespHeaders() if cfg.KeepResponseHeaders == nil { // Keep all res.Headers = headers diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index afc0e34ad42..743e796becd 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -102,11 +102,7 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.Write(c.Response().Body()) }, TagReqHeaders: func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (int, error) { - out := make(map[string]string, 0) - if err := c.Bind().Header(&out); err != nil { - return 0, err - } - + out := c.GetReqHeaders() reqHeaders := make([]string, 0) for k, v := range out { reqHeaders = append(reqHeaders, k+"="+v) From 8a772699293fd72cd8d81947e51689a46d67920c Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 16 Mar 2024 16:46:23 +0300 Subject: [PATCH 22/22] update somem methods --- ctx_interface.go | 10 ---------- middleware/logger/logger_test.go | 1 - 2 files changed, 11 deletions(-) diff --git a/ctx_interface.go b/ctx_interface.go index 5c6447fd28d..e5eec05a0c0 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -388,16 +388,6 @@ type Ctx interface { // ClientHelloInfo return CHI from context ClientHelloInfo() *tls.ClientHelloInfo - // GetReqHeaders returns the HTTP request headers. - // Returned value is only valid within the handler. Do not store any references. - // Make copies or use the Immutable setting instead. - GetReqHeaders() map[string]string - - // GetRespHeaders returns the HTTP response headers. - // Returned value is only valid within the handler. Do not store any references. - // Make copies or use the Immutable setting instead. - GetRespHeaders() map[string]string - // AllParams Params is used to get all route parameters. // Using Params method to get params. GetParams() map[string]string diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 0aa517bcf56..3c2c0774b2a 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -185,7 +185,6 @@ func Test_Logger_ErrorOutput(t *testing.T) { require.EqualValues(t, 2, *o) } -// go test -run Test_Logger_All func Test_Logger_All(t *testing.T) { t.Parallel() buf := bytebufferpool.Get()