From 2cb58a247b35d4cb0edb2bf866605860084f2898 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Tue, 23 Aug 2022 20:43:17 +0800 Subject: [PATCH 01/31] 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 370cc5b2e3..378a3426b9 100644 --- a/app.go +++ b/app.go @@ -10,6 +10,8 @@ package fiber import ( "bufio" "bytes" + "encoding/json" + "encoding/xml" "errors" "fmt" "net" @@ -22,9 +24,6 @@ import ( "sync/atomic" "time" - "encoding/json" - "encoding/xml" - "github.com/gofiber/fiber/v3/utils" "github.com/valyala/fasthttp" ) @@ -116,8 +115,6 @@ type App struct { latestGroup *Group // newCtxFunc newCtxFunc func(app *App) CustomCtx - // custom binders - customBinders []CustomBinder // TLS handler tlsHandler *tlsHandler } @@ -375,12 +372,6 @@ type Config struct { // // Optional. Default: DefaultColors ColorScheme Colors `json:"color_scheme"` - - // If you want to validate header/form/query... automatically when to bind, you can define struct validator. - // Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator. - // - // Default: nil - StructValidator StructValidator } // Static defines configuration options when defining static assets. @@ -469,13 +460,12 @@ func New(config ...Config) *App { stack: make([][]*Route, len(intMethod)), treeStack: make([]map[string][]*Route, len(intMethod)), // Create config - config: Config{}, - getBytes: utils.UnsafeBytes, - getString: utils.UnsafeString, - appList: make(map[string]*App), - latestRoute: &Route{}, - latestGroup: &Group{}, - customBinders: []CustomBinder{}, + config: Config{}, + getBytes: utils.UnsafeBytes, + getString: utils.UnsafeString, + appList: make(map[string]*App), + latestRoute: &Route{}, + latestGroup: &Group{}, } // Create Ctx pool @@ -569,12 +559,6 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) { app.newCtxFunc = function } -// You can register custom binders to use as Bind().Custom("name"). -// They should be compatible with CustomBinder interface. -func (app *App) RegisterCustomBinder(binder CustomBinder) { - app.customBinders = append(app.customBinders, binder) -} - // Mount attaches another app instance as a sub-router along a routing path. // It's very useful to split up a large API as many independent routers and // compose them as a single service using Mount. The fiber's error handler and diff --git a/bind.go b/bind.go deleted file mode 100644 index b390db2fda..0000000000 --- a/bind.go +++ /dev/null @@ -1,194 +0,0 @@ -package fiber - -import ( - "github.com/gofiber/fiber/v3/binder" - "github.com/gofiber/fiber/v3/utils" -) - -// An interface to register custom binders. -type CustomBinder interface { - Name() string - MIMETypes() []string - Parse(Ctx, any) error -} - -// An interface to register custom struct validator for binding. -type StructValidator interface { - Engine() any - ValidateStruct(any) error -} - -// Bind struct -type Bind struct { - ctx *DefaultCtx - should bool -} - -// To handle binder errors manually, you can prefer Should method. -// It's default behavior of binder. -func (b *Bind) Should() *Bind { - b.should = true - - return b -} - -// If you want to handle binder errors automatically, you can use Must. -// If there's an error it'll return error and 400 as HTTP status. -func (b *Bind) Must() *Bind { - b.should = false - - return b -} - -// Check Should/Must errors and return it by usage. -func (b *Bind) returnErr(err error) error { - if !b.should { - b.ctx.Status(StatusBadRequest) - return NewError(StatusBadRequest, "Bad request: "+err.Error()) - } - - return err -} - -// Struct validation. -func (b *Bind) validateStruct(out any) error { - validator := b.ctx.app.config.StructValidator - if validator != nil { - return validator.ValidateStruct(out) - } - - return nil -} - -// To use custom binders, you have to use this method. -// You can register them from RegisterCustomBinder method of Fiber instance. -// They're checked by name, if it's not found, it will return an error. -// NOTE: Should/Must is still valid for Custom binders. -func (b *Bind) Custom(name string, dest any) error { - binders := b.ctx.App().customBinders - for _, binder := range binders { - if binder.Name() == name { - return b.returnErr(binder.Parse(b.ctx, dest)) - } - } - - return ErrCustomBinderNotFound -} - -// Header binds the request header strings into the struct, map[string]string and map[string][]string. -func (b *Bind) Header(out any) error { - if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string. -func (b *Bind) RespHeader(out any) error { - if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// Cookie binds the requesr cookie strings into the struct, map[string]string and map[string][]string. -// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie. -func (b *Bind) Cookie(out any) error { - if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.Context(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// QueryParser binds the query string into the struct, map[string]string and map[string][]string. -func (b *Bind) Query(out any) error { - if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.Context(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// JSON binds the body string into the struct. -func (b *Bind) JSON(out any) error { - if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// XML binds the body string into the struct. -func (b *Bind) XML(out any) error { - if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// Form binds the form into the struct, map[string]string and map[string][]string. -func (b *Bind) Form(out any) error { - if err := b.returnErr(binder.FormBinder.Bind(b.ctx.Context(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// URI binds the route parameters into the struct, map[string]string and map[string][]string. -func (b *Bind) URI(out any) error { - if err := b.returnErr(binder.URIBinder.Bind(b.ctx.route.Params, b.ctx.Params, out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string. -func (b *Bind) MultipartForm(out any) error { - if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), out)); err != nil { - return err - } - - return b.validateStruct(out) -} - -// Body binds the request body into the struct, map[string]string and map[string][]string. -// It supports decoding the following content types based on the Content-Type header: -// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data -// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder. -// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error. -func (b *Bind) Body(out any) error { - // Get content-type - ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType())) - ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) - - // Parse body accordingly - switch ctype { - case MIMEApplicationJSON: - return b.JSON(out) - case MIMETextXML, MIMEApplicationXML: - return b.XML(out) - case MIMEApplicationForm: - return b.Form(out) - case MIMEMultipartForm: - return b.MultipartForm(out) - } - - // Check custom binders - binders := b.ctx.App().customBinders - for _, binder := range binders { - for _, mime := range binder.MIMETypes() { - if mime == ctype { - return b.returnErr(binder.Parse(b.ctx, out)) - } - } - } - - // No suitable content type found - return ErrUnprocessableEntity -} diff --git a/bind_test.go b/bind_test.go deleted file mode 100644 index 090f76db04..0000000000 --- a/bind_test.go +++ /dev/null @@ -1,1544 +0,0 @@ -package fiber - -import ( - "bytes" - "compress/gzip" - "encoding/json" - "errors" - "fmt" - "net/http/httptest" - "reflect" - "testing" - "time" - - "github.com/gofiber/fiber/v3/binder" - "github.com/stretchr/testify/require" - "github.com/valyala/fasthttp" -) - -// go test -run Test_Bind_Query -v -func Test_Bind_Query(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := new(Query) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = new(Query) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = new(Query) - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Query) - c.Request().URI().SetQueryString("") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(t, 0, len(empty.Hobby)) - - type Query2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") - q2 := new(Query2) - q2.Bool = true - q2.Name = "hello world" - require.Nil(t, c.Bind().Query(q2)) - require.Equal(t, "basketball,football", q2.Hobby) - require.True(t, q2.Bool) - require.Equal(t, "tom", q2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, q2.Empty) - require.Equal(t, []string{""}, q2.Alloc) - require.Equal(t, []int64{1}, q2.No) - - type RequiredQuery struct { - Name string `query:"name,required"` - } - rq := new(RequiredQuery) - c.Request().URI().SetQueryString("") - require.Equal(t, "name is empty", c.Bind().Query(rq).Error()) - - type ArrayQuery struct { - Data []string - } - aq := new(ArrayQuery) - c.Request().URI().SetQueryString("data[]=john&data[]=doe") - require.Nil(t, c.Bind().Query(aq)) - require.Equal(t, 2, len(aq.Data)) -} - -// go test -run Test_Bind_Query_Map -v -func Test_Bind_Query_Map(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := make(map[string][]string) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 2, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = make(map[string][]string) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 2, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = make(map[string][]string) - require.Nil(t, c.Bind().Query(&q)) - require.Equal(t, 3, len(q["hobby"])) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer") - qq := make(map[string]string) - require.Nil(t, c.Bind().Query(&qq)) - require.Equal(t, "1", qq["id"]) - - empty := make(map[string][]string) - c.Request().URI().SetQueryString("") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(t, 0, len(empty["hobby"])) - - em := make(map[string][]int) - c.Request().URI().SetQueryString("") - require.Equal(t, binder.ErrMapNotConvertable, c.Bind().Query(&em)) -} - -// go test -run Test_Bind_Query_WithSetParserDecoder -v -func Test_Bind_Query_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time - - NonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, - Converter: NonRFCConverter, - } - - binder.SetParserDecoder(binder.ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []binder.ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "query", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type NonRFCTimeInput struct { - Date NonRFCTime `query:"date"` - Title string `query:"title"` - Body string `query:"body"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - q := new(NonRFCTimeInput) - - c.Request().URI().SetQueryString("date=2021-04-10&title=CustomDateTest&Body=October") - require.Nil(t, c.Bind().Query(q)) - fmt.Println(q.Date, "q.Date") - require.Equal(t, "CustomDateTest", q.Title) - date := fmt.Sprintf("%v", q.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", q.Body) - - c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October") - q = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Query(q)) - require.Equal(t, "", q.Title) -} - -// go test -run Test_Bind_Query_Schema -v -func Test_Bind_Query_Schema(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query1 struct { - Name string `query:"name,required"` - Nested struct { - Age int `query:"age"` - } `query:"nested,required"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q := new(Query1) - require.Nil(t, c.Bind().Query(q)) - - c.Request().URI().SetQueryString("namex=tom&nested.age=10") - q = new(Query1) - require.Equal(t, "name is empty", c.Bind().Query(q).Error()) - - c.Request().URI().SetQueryString("name=tom&nested.agex=10") - q = new(Query1) - require.Nil(t, c.Bind().Query(q)) - - c.Request().URI().SetQueryString("name=tom&test.age=10") - q = new(Query1) - require.Equal(t, "nested is empty", c.Bind().Query(q).Error()) - - type Query2 struct { - Name string `query:"name"` - Nested struct { - Age int `query:"age,required"` - } `query:"nested"` - } - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q2 := new(Query2) - require.Nil(t, c.Bind().Query(q2)) - - c.Request().URI().SetQueryString("nested.age=10") - q2 = new(Query2) - require.Nil(t, c.Bind().Query(q2)) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) - - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) - - type Node struct { - Value int `query:"val,required"` - Next *Node `query:"next,required"` - } - c.Request().URI().SetQueryString("val=1&next.val=3") - n := new(Node) - require.Nil(t, c.Bind().Query(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().URI().SetQueryString("next.val=2") - n = new(Node) - require.Equal(t, "val is empty", c.Bind().Query(n).Error()) - - c.Request().URI().SetQueryString("val=3&next.value=2") - n = new(Node) - n.Next = new(Node) - require.Nil(t, c.Bind().Query(n)) - require.Equal(t, 3, n.Value) - require.Equal(t, 0, n.Next.Value) - - type Person struct { - Name string `query:"name"` - Age int `query:"age"` - } - - type CollectionQuery struct { - Data []Person `query:"data"` - } - - c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12") - cq := new(CollectionQuery) - require.Nil(t, c.Bind().Query(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, 10, cq.Data[0].Age) - require.Equal(t, "doe", cq.Data[1].Name) - require.Equal(t, 12, cq.Data[1].Age) - - c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") - cq = new(CollectionQuery) - require.Nil(t, c.Bind().Query(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, 10, cq.Data[0].Age) - require.Equal(t, "doe", cq.Data[1].Name) - require.Equal(t, 12, cq.Data[1].Age) -} - -// go test -run Test_Bind_Header -v -func Test_Bind_Header(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Header struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - q := new(Header) - require.Nil(t, c.Bind().Header(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.Nil(t, c.Bind().Header(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Header) - c.Request().Header.Del("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(t, 0, len(empty.Hobby)) - - type Header2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Request().Header.Add("id", "2") - c.Request().Header.Add("Name", "Jane Doe") - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "go,fiber") - c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi") - c.Request().Header.Add("alloc", "") - c.Request().Header.Add("no", "1") - - h2 := new(Header2) - h2.Bool = true - h2.Name = "hello world" - require.Nil(t, c.Bind().Header(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredHeader struct { - Name string `header:"name,required"` - } - rh := new(RequiredHeader) - c.Request().Header.Del("name") - require.Equal(t, "name is empty", c.Bind().Header(rh).Error()) -} - -// go test -run Test_Bind_Header_Map -v -func Test_Bind_Header_Map(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - q := make(map[string][]string, 0) - require.Nil(t, c.Bind().Header(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.Nil(t, c.Bind().Header(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string, 0) - c.Request().Header.Del("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(t, 0, len(empty["Hobby"])) -} - -// go test -run Test_Bind_Header_WithSetParserDecoder -v -func Test_Bind_Header_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time - - NonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, - Converter: NonRFCConverter, - } - - binder.SetParserDecoder(binder.ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []binder.ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "req", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type NonRFCTimeInput struct { - Date NonRFCTime `req:"date"` - Title string `req:"title"` - Body string `req:"body"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - r := new(NonRFCTimeInput) - - c.Request().Header.Add("Date", "2021-04-10") - c.Request().Header.Add("Title", "CustomDateTest") - c.Request().Header.Add("Body", "October") - - require.Nil(t, c.Bind().Header(r)) - fmt.Println(r.Date, "q.Date") - require.Equal(t, "CustomDateTest", r.Title) - date := fmt.Sprintf("%v", r.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", r.Body) - - c.Request().Header.Add("Title", "") - r = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Header(r)) - require.Equal(t, "", r.Title) -} - -// go test -run Test_Bind_Header_Schema -v -func Test_Bind_Header_Schema(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Header1 struct { - Name string `header:"Name,required"` - Nested struct { - Age int `header:"Age"` - } `header:"Nested,required"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - q := new(Header1) - require.Nil(t, c.Bind().Header(q)) - - c.Request().Header.Del("Name") - q = new(Header1) - require.Equal(t, "Name is empty", c.Bind().Header(q).Error()) - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Del("Nested.Age") - c.Request().Header.Add("Nested.Agex", "10") - q = new(Header1) - require.Nil(t, c.Bind().Header(q)) - - c.Request().Header.Del("Nested.Agex") - q = new(Header1) - require.Equal(t, "Nested is empty", c.Bind().Header(q).Error()) - - c.Request().Header.Del("Nested.Agex") - c.Request().Header.Del("Name") - - type Header2 struct { - Name string `header:"Name"` - Nested struct { - Age int `header:"age,required"` - } `header:"Nested"` - } - - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - - h2 := new(Header2) - require.Nil(t, c.Bind().Header(h2)) - - c.Request().Header.Del("Name") - h2 = new(Header2) - require.Nil(t, c.Bind().Header(h2)) - - c.Request().Header.Del("Name") - c.Request().Header.Del("Nested.Age") - c.Request().Header.Add("Nested.Agex", "10") - h2 = new(Header2) - require.Equal(t, "Nested.age is empty", c.Bind().Header(h2).Error()) - - type Node struct { - Value int `header:"Val,required"` - Next *Node `header:"Next,required"` - } - c.Request().Header.Add("Val", "1") - c.Request().Header.Add("Next.Val", "3") - n := new(Node) - require.Nil(t, c.Bind().Header(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().Header.Del("Val") - n = new(Node) - require.Equal(t, "Val is empty", c.Bind().Header(n).Error()) - - c.Request().Header.Add("Val", "3") - c.Request().Header.Del("Next.Val") - c.Request().Header.Add("Next.Value", "2") - n = new(Node) - n.Next = new(Node) - require.Nil(t, c.Bind().Header(n)) - require.Equal(t, 3, n.Value) - require.Equal(t, 0, n.Next.Value) -} - -// go test -run Test_Bind_Resp_Header -v -func Test_Bind_RespHeader(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Header struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") - q := new(Header) - require.Nil(t, c.Bind().RespHeader(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.Nil(t, c.Bind().RespHeader(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Header) - c.Response().Header.Del("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(t, 0, len(empty.Hobby)) - - type Header2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Response().Header.Add("id", "2") - c.Response().Header.Add("Name", "Jane Doe") - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "go,fiber") - c.Response().Header.Add("favouriteDrinks", "milo,coke,pepsi") - c.Response().Header.Add("alloc", "") - c.Response().Header.Add("no", "1") - - h2 := new(Header2) - h2.Bool = true - h2.Name = "hello world" - require.Nil(t, c.Bind().RespHeader(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredHeader struct { - Name string `respHeader:"name,required"` - } - rh := new(RequiredHeader) - c.Response().Header.Del("name") - require.Equal(t, "name is empty", c.Bind().RespHeader(rh).Error()) -} - -// go test -run Test_Bind_RespHeader_Map -v -func Test_Bind_RespHeader_Map(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") - q := make(map[string][]string, 0) - require.Nil(t, c.Bind().RespHeader(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.Nil(t, c.Bind().RespHeader(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string, 0) - c.Response().Header.Del("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(t, 0, len(empty["Hobby"])) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Query -benchmem -count=4 -func Benchmark_Bind_Query(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := new(Query) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Query(q) - } - require.Nil(b, c.Bind().Query(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Query_Map -benchmem -count=4 -func Benchmark_Bind_Query_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := make(map[string][]string) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Query(&q) - } - require.Nil(b, c.Bind().Query(&q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Query_WithParseParam -benchmem -count=4 -func Benchmark_Bind_Query_WithParseParam(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Person struct { - Name string `query:"name"` - Age int `query:"age"` - } - - type CollectionQuery struct { - Data []Person `query:"data"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10") - cq := new(CollectionQuery) - - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Query(cq) - } - - require.Nil(b, c.Bind().Query(cq)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Query_Comma -benchmem -count=4 -func Benchmark_Bind_Query_Comma(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Query struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - // c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q := new(Query) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Query(q) - } - require.Nil(b, c.Bind().Query(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Header -benchmem -count=4 -func Benchmark_Bind_Header(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type ReqHeader struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - - q := new(ReqHeader) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Header(q) - } - require.Nil(b, c.Bind().Header(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Header_Map -benchmem -count=4 -func Benchmark_Bind_Header_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - - q := make(map[string][]string) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().Header(&q) - } - require.Nil(b, c.Bind().Header(&q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader -benchmem -count=4 -func Benchmark_Bind_RespHeader(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type ReqHeader struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") - - q := new(ReqHeader) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().RespHeader(q) - } - require.Nil(b, c.Bind().RespHeader(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader_Map -benchmem -count=4 -func Benchmark_Bind_RespHeader_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") - - q := make(map[string][]string) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - c.Bind().RespHeader(&q) - } - require.Nil(b, c.Bind().RespHeader(&q)) -} - -// go test -run Test_Bind_Body -func Test_Bind_Body(t *testing.T) { - t.Parallel() - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `json:"name" xml:"name" form:"name" query:"name"` - } - - { - var gzipJSON bytes.Buffer - w := gzip.NewWriter(&gzipJSON) - _, _ = w.Write([]byte(`{"name":"john"}`)) - _ = w.Close() - - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.Set(HeaderContentEncoding, "gzip") - c.Request().SetBody(gzipJSON.Bytes()) - c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) - d := new(Demo) - require.Nil(t, c.Bind().Body(d)) - require.Equal(t, "john", d.Name) - c.Request().Header.Del(HeaderContentEncoding) - } - - testDecodeParser := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - require.Nil(t, c.Bind().Body(d)) - require.Equal(t, "john", d.Name) - } - - testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) - testDecodeParser(MIMEApplicationXML, `john`) - testDecodeParser(MIMEApplicationForm, "name=john") - testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - - testDecodeParserError := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - require.False(t, c.Bind().Body(nil) == nil) - } - - testDecodeParserError("invalid-content-type", "") - testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b") - - type CollectionQuery struct { - Data []Demo `query:"data"` - } - - c.Request().Reset() - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe")) - c.Request().Header.SetContentLength(len(c.Body())) - cq := new(CollectionQuery) - require.Nil(t, c.Bind().Body(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, "doe", cq.Data[1].Name) - - c.Request().Reset() - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe")) - c.Request().Header.SetContentLength(len(c.Body())) - cq = new(CollectionQuery) - require.Nil(t, c.Bind().Body(cq)) - require.Equal(t, 2, len(cq.Data)) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, "doe", cq.Data[1].Name) -} - -// go test -run Test_Bind_Body_WithSetParserDecoder -func Test_Bind_Body_WithSetParserDecoder(t *testing.T) { - type CustomTime time.Time - - timeConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - customTime := binder.ParserType{ - Customtype: CustomTime{}, - Converter: timeConverter, - } - - binder.SetParserDecoder(binder.ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []binder.ParserType{customTime}, - ZeroEmpty: true, - SetAliasTag: "form", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Date CustomTime `form:"date"` - Title string `form:"title"` - Body string `form:"body"` - } - - testDecodeParser := func(contentType, body string) { - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - d := Demo{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Body(&d)) - date := fmt.Sprintf("%v", d.Date) - require.Equal(t, "{0 63743587200 }", date) - require.Equal(t, "", d.Title) - require.Equal(t, "New Body", d.Body) - } - - testDecodeParser(MIMEApplicationForm, "date=2020-12-15&title=&body=New Body") - testDecodeParser(MIMEMultipartForm+`; boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"date\"\r\n\r\n2020-12-15\r\n--b\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n\r\n--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nNew Body\r\n--b--") -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_JSON -benchmem -count=4 -func Benchmark_Bind_Body_JSON(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `json:"name"` - } - body := []byte(`{"name":"john"}`) - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.Bind().Body(d) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_XML -benchmem -count=4 -func Benchmark_Bind_Body_XML(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `xml:"name"` - } - body := []byte("john") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationXML) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.Bind().Body(d) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4 -func Benchmark_Bind_Body_Form(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `form:"name"` - } - body := []byte("name=john") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.Bind().Body(d) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm -benchmem -count=4 -func Benchmark_Bind_Body_MultipartForm(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Demo struct { - Name string `form:"name"` - } - - body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.Bind().Body(d) - } - require.Nil(b, c.Bind().Body(d)) - require.Equal(b, "john", d.Name) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4 -func Benchmark_Bind_Body_Form_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - body := []byte("name=john") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().Header.SetContentLength(len(body)) - d := make(map[string]string) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - _ = c.Bind().Body(&d) - } - require.Nil(b, c.Bind().Body(&d)) - require.Equal(b, "john", d["name"]) -} - -// go test -run Test_Bind_URI -func Test_Bind_URI(t *testing.T) { - t.Parallel() - - app := New() - app.Get("/test1/userId/role/:roleId", func(c Ctx) error { - type Demo struct { - UserID uint `uri:"userId"` - RoleID uint `uri:"roleId"` - } - var ( - d = new(Demo) - ) - if err := c.Bind().URI(d); err != nil { - t.Fatal(err) - } - require.Equal(t, uint(111), d.UserID) - require.Equal(t, uint(222), d.RoleID) - return nil - }) - app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) - app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) -} - -// go test -run Test_Bind_URI_Map -func Test_Bind_URI_Map(t *testing.T) { - t.Parallel() - - app := New() - app.Get("/test1/userId/role/:roleId", func(c Ctx) error { - d := make(map[string]string) - - if err := c.Bind().URI(&d); err != nil { - t.Fatal(err) - } - require.Equal(t, uint(111), d["userId"]) - require.Equal(t, uint(222), d["roleId"]) - return nil - }) - app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) - app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_URI -benchmem -count=4 -func Benchmark_Bind_URI(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - - c.route = &Route{ - Params: []string{ - "param1", "param2", "param3", "param4", - }, - } - c.values = [maxParams]string{ - "john", "doe", "is", "awesome", - } - - var res struct { - Param1 string `uri:"param1"` - Param2 string `uri:"param2"` - Param3 string `uri:"param3"` - Param4 string `uri:"param4"` - } - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - c.Bind().URI(&res) - } - - require.Equal(b, "john", res.Param1) - require.Equal(b, "doe", res.Param2) - require.Equal(b, "is", res.Param3) - require.Equal(b, "awesome", res.Param4) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_URI_Map -benchmem -count=4 -func Benchmark_Bind_URI_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - - c.route = &Route{ - Params: []string{ - "param1", "param2", "param3", "param4", - }, - } - c.values = [maxParams]string{ - "john", "doe", "is", "awesome", - } - - res := make(map[string]string) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - c.Bind().URI(&res) - } - - require.Equal(b, "john", res["param1"]) - require.Equal(b, "doe", res["param2"]) - require.Equal(b, "is", res["param3"]) - require.Equal(b, "awesome", res["param4"]) -} - -// go test -run Test_Bind_Cookie -v -func Test_Bind_Cookie(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Cookie struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.SetCookie("id", "1") - c.Request().Header.SetCookie("Name", "John Doe") - c.Request().Header.SetCookie("Hobby", "golang,fiber") - q := new(Cookie) - require.Nil(t, c.Bind().Cookie(q)) - require.Equal(t, 2, len(q.Hobby)) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = new(Cookie) - require.Nil(t, c.Bind().Cookie(q)) - require.Equal(t, 3, len(q.Hobby)) - - empty := new(Cookie) - c.Request().Header.DelCookie("hobby") - require.Nil(t, c.Bind().Query(empty)) - require.Equal(t, 0, len(empty.Hobby)) - - type Cookie2 struct { - Bool bool - ID int - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - } - - c.Request().Header.SetCookie("id", "2") - c.Request().Header.SetCookie("Name", "Jane Doe") - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "go,fiber") - c.Request().Header.SetCookie("favouriteDrinks", "milo,coke,pepsi") - c.Request().Header.SetCookie("alloc", "") - c.Request().Header.SetCookie("no", "1") - - h2 := new(Cookie2) - h2.Bool = true - h2.Name = "hello world" - require.Nil(t, c.Bind().Cookie(h2)) - require.Equal(t, "go,fiber", h2.Hobby) - require.True(t, h2.Bool) - require.Equal(t, "Jane Doe", h2.Name) // check value get overwritten - require.Equal(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) - var nilSlice []string - require.Equal(t, nilSlice, h2.Empty) - require.Equal(t, []string{""}, h2.Alloc) - require.Equal(t, []int64{1}, h2.No) - - type RequiredCookie struct { - Name string `cookie:"name,required"` - } - rh := new(RequiredCookie) - c.Request().Header.DelCookie("name") - require.Equal(t, "name is empty", c.Bind().Cookie(rh).Error()) -} - -// go test -run Test_Bind_Cookie_Map -v -func Test_Bind_Cookie_Map(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.SetCookie("id", "1") - c.Request().Header.SetCookie("Name", "John Doe") - c.Request().Header.SetCookie("Hobby", "golang,fiber") - q := make(map[string][]string) - require.Nil(t, c.Bind().Cookie(&q)) - require.Equal(t, 2, len(q["Hobby"])) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = make(map[string][]string) - require.Nil(t, c.Bind().Cookie(&q)) - require.Equal(t, 3, len(q["Hobby"])) - - empty := make(map[string][]string) - c.Request().Header.DelCookie("hobby") - require.Nil(t, c.Bind().Query(&empty)) - require.Equal(t, 0, len(empty["Hobby"])) -} - -// go test -run Test_Bind_Cookie_WithSetParserDecoder -v -func Test_Bind_Cookie_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time - - NonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} - } - - nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, - Converter: NonRFCConverter, - } - - binder.SetParserDecoder(binder.ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []binder.ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "cerez", - }) - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type NonRFCTimeInput struct { - Date NonRFCTime `cerez:"date"` - Title string `cerez:"title"` - Body string `cerez:"body"` - } - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - r := new(NonRFCTimeInput) - - c.Request().Header.SetCookie("Date", "2021-04-10") - c.Request().Header.SetCookie("Title", "CustomDateTest") - c.Request().Header.SetCookie("Body", "October") - - require.Nil(t, c.Bind().Cookie(r)) - fmt.Println(r.Date, "q.Date") - require.Equal(t, "CustomDateTest", r.Title) - date := fmt.Sprintf("%v", r.Date) - require.Equal(t, "{0 63753609600 }", date) - require.Equal(t, "October", r.Body) - - c.Request().Header.SetCookie("Title", "") - r = &NonRFCTimeInput{ - Title: "Existing title", - Body: "Existing Body", - } - require.Nil(t, c.Bind().Cookie(r)) - require.Equal(t, "", r.Title) -} - -// go test -run Test_Bind_Cookie_Schema -v -func Test_Bind_Cookie_Schema(t *testing.T) { - t.Parallel() - - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Cookie1 struct { - Name string `cookie:"Name,required"` - Nested struct { - Age int `cookie:"Age"` - } `cookie:"Nested,required"` - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.SetCookie("Name", "tom") - c.Request().Header.SetCookie("Nested.Age", "10") - q := new(Cookie1) - require.Nil(t, c.Bind().Cookie(q)) - - c.Request().Header.DelCookie("Name") - q = new(Cookie1) - require.Equal(t, "Name is empty", c.Bind().Cookie(q).Error()) - - c.Request().Header.SetCookie("Name", "tom") - c.Request().Header.DelCookie("Nested.Age") - c.Request().Header.SetCookie("Nested.Agex", "10") - q = new(Cookie1) - require.Nil(t, c.Bind().Cookie(q)) - - c.Request().Header.DelCookie("Nested.Agex") - q = new(Cookie1) - require.Equal(t, "Nested is empty", c.Bind().Cookie(q).Error()) - - c.Request().Header.DelCookie("Nested.Agex") - c.Request().Header.DelCookie("Name") - - type Cookie2 struct { - Name string `cookie:"Name"` - Nested struct { - Age int `cookie:"Age,required"` - } `cookie:"Nested"` - } - - c.Request().Header.SetCookie("Name", "tom") - c.Request().Header.SetCookie("Nested.Age", "10") - - h2 := new(Cookie2) - require.Nil(t, c.Bind().Cookie(h2)) - - c.Request().Header.DelCookie("Name") - h2 = new(Cookie2) - require.Nil(t, c.Bind().Cookie(h2)) - - c.Request().Header.DelCookie("Name") - c.Request().Header.DelCookie("Nested.Age") - c.Request().Header.SetCookie("Nested.Agex", "10") - h2 = new(Cookie2) - require.Equal(t, "Nested.Age is empty", c.Bind().Cookie(h2).Error()) - - type Node struct { - Value int `cookie:"Val,required"` - Next *Node `cookie:"Next,required"` - } - c.Request().Header.SetCookie("Val", "1") - c.Request().Header.SetCookie("Next.Val", "3") - n := new(Node) - require.Nil(t, c.Bind().Cookie(n)) - require.Equal(t, 1, n.Value) - require.Equal(t, 3, n.Next.Value) - - c.Request().Header.DelCookie("Val") - n = new(Node) - require.Equal(t, "Val is empty", c.Bind().Cookie(n).Error()) - - c.Request().Header.SetCookie("Val", "3") - c.Request().Header.DelCookie("Next.Val") - c.Request().Header.SetCookie("Next.Value", "2") - n = new(Node) - n.Next = new(Node) - require.Nil(t, c.Bind().Cookie(n)) - require.Equal(t, 3, n.Value) - require.Equal(t, 0, n.Next.Value) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Cookie -benchmem -count=4 -func Benchmark_Bind_Cookie(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type Cookie struct { - ID int - Name string - Hobby []string - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.SetCookie("id", "1") - c.Request().Header.SetCookie("Name", "John Doe") - c.Request().Header.SetCookie("Hobby", "golang,fiber") - - q := new(Cookie) - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - c.Bind().Cookie(q) - } - require.Nil(b, c.Bind().Cookie(q)) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Cookie_Map -benchmem -count=4 -func Benchmark_Bind_Cookie_Map(b *testing.B) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - - c.Request().Header.SetCookie("id", "1") - c.Request().Header.SetCookie("Name", "John Doe") - c.Request().Header.SetCookie("Hobby", "golang,fiber") - - q := make(map[string][]string) - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - c.Bind().Cookie(&q) - } - require.Nil(b, c.Bind().Cookie(&q)) -} - -// custom binder for testing -type customBinder struct{} - -func (b *customBinder) Name() string { - return "custom" -} - -func (b *customBinder) MIMETypes() []string { - return []string{"test", "test2"} -} - -func (b *customBinder) Parse(c Ctx, out any) error { - return json.Unmarshal(c.Body(), out) -} - -// go test -run Test_Bind_CustomBinder -func Test_Bind_CustomBinder(t *testing.T) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - // Register binder - binder := &customBinder{} - app.RegisterCustomBinder(binder) - - type Demo struct { - Name string `json:"name"` - } - body := []byte(`{"name":"john"}`) - c.Request().SetBody(body) - c.Request().Header.SetContentType("test") - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - require.Nil(t, c.Bind().Body(d)) - require.Nil(t, c.Bind().Custom("custom", d)) - require.Equal(t, ErrCustomBinderNotFound, c.Bind().Custom("not_custom", d)) - require.Equal(t, "john", d.Name) -} - -// go test -run Test_Bind_Must -func Test_Bind_Must(t *testing.T) { - app := New() - c := app.NewCtx(&fasthttp.RequestCtx{}) - - type RequiredQuery struct { - Name string `query:"name,required"` - } - rq := new(RequiredQuery) - c.Request().URI().SetQueryString("") - err := c.Bind().Must().Query(rq) - require.Equal(t, StatusBadRequest, c.Response().StatusCode()) - require.Equal(t, "Bad request: name is empty", err.Error()) -} - -// simple struct validator for testing -type structValidator struct{} - -func (v *structValidator) Engine() any { - return "" -} - -func (v *structValidator) ValidateStruct(out any) error { - out = reflect.ValueOf(out).Elem().Interface() - sq := out.(simpleQuery) - - if sq.Name != "john" { - return errors.New("you should have entered right name!") - } - - return nil -} - -type simpleQuery struct { - Name string `query:"name"` -} - -// go test -run Test_Bind_StructValidator -func Test_Bind_StructValidator(t *testing.T) { - app := New(Config{StructValidator: &structValidator{}}) - c := app.NewCtx(&fasthttp.RequestCtx{}) - - rq := new(simpleQuery) - c.Request().URI().SetQueryString("name=efe") - require.Equal(t, "you should have entered right name!", c.Bind().Query(rq).Error()) - - rq = new(simpleQuery) - c.Request().URI().SetQueryString("name=john") - require.Nil(t, c.Bind().Query(rq)) -} diff --git a/binder/README.md b/binder/README.md deleted file mode 100644 index d40cc7e54e..0000000000 --- a/binder/README.md +++ /dev/null @@ -1,194 +0,0 @@ -# Fiber Binders - -Binder is new request/response binding feature for Fiber. By aganist old Fiber parsers, it supports custom binder registration, struct validation, **map[string]string**, **map[string][]string** and more. It's introduced in Fiber v3 and a replacement of: -- BodyParser -- ParamsParser -- GetReqHeaders -- GetRespHeaders -- AllParams -- QueryParser -- ReqHeaderParser - - -## Default Binders -- [Form](form.go) -- [Query](query.go) -- [URI](uri.go) -- [Header](header.go) -- [Response Header](resp_header.go) -- [Cookie](cookie.go) -- [JSON](json.go) -- [XML](xml.go) - -## Guides - -### Binding into the Struct -Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example for it: -```go -// Field names should start with an uppercase letter -type Person struct { - Name string `json:"name" xml:"name" form:"name"` - Pass string `json:"pass" xml:"pass" form:"pass"` -} - -app.Post("/", func(c fiber.Ctx) error { - p := new(Person) - - if err := c.Bind().Body(p); err != nil { - return err - } - - log.Println(p.Name) // john - log.Println(p.Pass) // doe - - // ... -}) - -// Run tests with the following curl commands: - -// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 - -// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 - -// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 - -// curl -X POST -F name=john -F pass=doe http://localhost:3000 - -// curl -X POST "http://localhost:3000/?name=john&pass=doe" -``` - -### Binding into the Map -Fiber supports binding into the **map[string]string** or **map[string][]string**. Here's an example for it: -```go -app.Get("/", func(c fiber.Ctx) error { - p := make(map[string][]string) - - if err := c.Bind().Query(p); err != nil { - return err - } - - log.Println(p["name"]) // john - log.Println(p["pass"]) // doe - log.Println(p["products"]) // [shoe, hat] - - // ... -}) -// Run tests with the following curl command: - -// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" -``` -### Behaviors of Should/Must -Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`. - -If there's an error it'll return error and 400 as HTTP status. Here's an example for it: -```go -// Field names should start with an uppercase letter -type Person struct { - Name string `json:"name,required"` - Pass string `json:"pass"` -} - -app.Get("/", func(c fiber.Ctx) error { - p := new(Person) - - if err := c.Bind().Must().JSON(p); err != nil { - return err - // Status code: 400 - // Response: Bad request: name is empty - } - - // ... -}) - -// Run tests with the following curl command: - -// curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000 -``` -### Defining Custom Binder -We didn't add much binder to make Fiber codebase minimal. But if you want to use your binders, it's easy to register and use them. Here's an example for TOML binder. -```go -type Person struct { - Name string `toml:"name"` - Pass string `toml:"pass"` -} - -type tomlBinding struct{} - -func (b *tomlBinding) Name() string { - return "toml" -} - -func (b *tomlBinding) MIMETypes() []string { - return []string{"application/toml"} -} - -func (b *tomlBinding) Parse(c fiber.Ctx, out any) error { - return toml.Unmarshal(c.Body(), out) -} - -func main() { - app := fiber.New() - app.RegisterCustomBinder(&tomlBinding{}) - - app.Get("/", func(c fiber.Ctx) error { - out := new(Person) - if err := c.Bind().Body(out); err != nil { - return err - } - - // or you can use like: - // if err := c.Bind().Custom("toml", out); err != nil { - // return err - // } - - return c.SendString(out.Pass) // test - }) - - app.Listen(":3000") -} - -// curl -X GET -H "Content-Type: application/toml" --data "name = 'bar' -// pass = 'test'" localhost:3000 -``` -### Defining Custom Validator -All Fiber binders supporting struct validation if you defined validator inside of the config. You can create own validator, or use [go-playground/validator](https://github.com/go-playground/validator), [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)... Here's an example of simple custom validator: -```go -type Query struct { - Name string `query:"name"` -} - -type structValidator struct{} - -func (v *structValidator) Engine() any { - return "" -} - -func (v *structValidator) ValidateStruct(out any) error { - out = reflect.ValueOf(out).Elem().Interface() - sq := out.(Query) - - if sq.Name != "john" { - return errors.New("you should have entered right name!") - } - - return nil -} - -func main() { - app := fiber.New(fiber.Config{StructValidator: &structValidator{}}) - - app.Get("/", func(c fiber.Ctx) error { - out := new(Query) - if err := c.Bind().Query(out); err != nil { - return err // you should have entered right name! - } - return c.SendString(out.Name) - }) - - app.Listen(":3000") -} - -// Run tests with the following curl command: - -// curl "http://localhost:3000/?name=efe" -``` \ No newline at end of file diff --git a/binder/binder.go b/binder/binder.go deleted file mode 100644 index d393179011..0000000000 --- a/binder/binder.go +++ /dev/null @@ -1,19 +0,0 @@ -package binder - -import "errors" - -// Binder errors -var ( - ErrSuitableContentNotFound = errors.New("binder: suitable content not found to parse body") - ErrMapNotConvertable = errors.New("binder: map is not convertable to map[string]string or map[string][]string") -) - -// Init default binders for Fiber -var HeaderBinder = &headerBinding{} -var RespHeaderBinder = &respHeaderBinding{} -var CookieBinder = &cookieBinding{} -var QueryBinder = &queryBinding{} -var FormBinder = &formBinding{} -var URIBinder = &uriBinding{} -var XMLBinder = &xmlBinding{} -var JSONBinder = &jsonBinding{} diff --git a/binder/cookie.go b/binder/cookie.go deleted file mode 100644 index e761e4776c..0000000000 --- a/binder/cookie.go +++ /dev/null @@ -1,45 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/fasthttp" -) - -type cookieBinding struct{} - -func (*cookieBinding) Name() string { - return "cookie" -} - -func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { - data := make(map[string][]string) - var err error - - reqCtx.Request.Header.VisitAllCookie(func(key, val []byte) { - if err != nil { - return - } - - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } - - }) - - if err != nil { - return err - } - - return parse(b.Name(), out, data) -} diff --git a/binder/form.go b/binder/form.go deleted file mode 100644 index 24983ccdea..0000000000 --- a/binder/form.go +++ /dev/null @@ -1,53 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/fasthttp" -) - -type formBinding struct{} - -func (*formBinding) Name() string { - return "form" -} - -func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { - data := make(map[string][]string) - var err error - - reqCtx.PostArgs().VisitAll(func(key, val []byte) { - if err != nil { - return - } - - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } - }) - - return parse(b.Name(), out, data) -} - -func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error { - data, err := reqCtx.MultipartForm() - if err != nil { - return err - } - - return parse(b.Name(), out, data.Value) -} diff --git a/binder/header.go b/binder/header.go deleted file mode 100644 index 688a81136a..0000000000 --- a/binder/header.go +++ /dev/null @@ -1,34 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/fasthttp" -) - -type headerBinding struct{} - -func (*headerBinding) Name() string { - return "header" -} - -func (b *headerBinding) Bind(req *fasthttp.Request, out any) error { - data := make(map[string][]string) - req.Header.VisitAll(func(key, val []byte) { - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } - }) - - return parse(b.Name(), out, data) -} diff --git a/binder/json.go b/binder/json.go deleted file mode 100644 index 570a7f9b79..0000000000 --- a/binder/json.go +++ /dev/null @@ -1,15 +0,0 @@ -package binder - -import ( - "github.com/gofiber/fiber/v3/utils" -) - -type jsonBinding struct{} - -func (*jsonBinding) Name() string { - return "json" -} - -func (b *jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error { - return jsonDecoder(body, out) -} diff --git a/binder/mapping.go b/binder/mapping.go deleted file mode 100644 index bec5634808..0000000000 --- a/binder/mapping.go +++ /dev/null @@ -1,199 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - "sync" - - "github.com/gofiber/fiber/v3/internal/schema" - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/bytebufferpool" -) - -// ParserConfig form decoder config for SetParserDecoder -type ParserConfig struct { - IgnoreUnknownKeys bool - SetAliasTag string - ParserType []ParserType - ZeroEmpty bool -} - -// ParserType require two element, type and converter for register. -// Use ParserType with BodyParser for parsing custom type in form data. -type ParserType struct { - Customtype any - Converter func(string) reflect.Value -} - -// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance -var decoderPool = &sync.Pool{New: func() any { - return decoderBuilder(ParserConfig{ - IgnoreUnknownKeys: true, - ZeroEmpty: true, - }) -}} - -// SetParserDecoder allow globally change the option of form decoder, update decoderPool -func SetParserDecoder(parserConfig ParserConfig) { - decoderPool = &sync.Pool{New: func() any { - return decoderBuilder(parserConfig) - }} -} - -func decoderBuilder(parserConfig ParserConfig) any { - decoder := schema.NewDecoder() - decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys) - if parserConfig.SetAliasTag != "" { - decoder.SetAliasTag(parserConfig.SetAliasTag) - } - for _, v := range parserConfig.ParserType { - decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter) - } - decoder.ZeroEmpty(parserConfig.ZeroEmpty) - return decoder -} - -// parse data into the map or struct -func parse(aliasTag string, out any, data map[string][]string) error { - ptrVal := reflect.ValueOf(out) - - // Get pointer value - if ptrVal.Kind() == reflect.Ptr { - ptrVal = ptrVal.Elem() - } - - // Parse into the map - if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String { - return parseToMap(ptrVal.Interface(), data) - } - - // Parse into the struct - return parseToStruct(aliasTag, out, data) -} - -// Parse data into the struct with gorilla/schema -func parseToStruct(aliasTag string, out any, data map[string][]string) error { - // Get decoder from pool - schemaDecoder := decoderPool.Get().(*schema.Decoder) - defer decoderPool.Put(schemaDecoder) - - // Set alias tag - schemaDecoder.SetAliasTag(aliasTag) - - return schemaDecoder.Decode(out, data) -} - -// Parse data into the map -// thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go -func parseToMap(ptr any, data map[string][]string) error { - elem := reflect.TypeOf(ptr).Elem() - - // map[string][]string - if elem.Kind() == reflect.Slice { - newMap, ok := ptr.(map[string][]string) - if !ok { - return ErrMapNotConvertable - } - - for k, v := range data { - newMap[k] = v - } - - return nil - } - - // map[string]string - newMap, ok := ptr.(map[string]string) - if !ok { - return ErrMapNotConvertable - } - - for k, v := range data { - newMap[k] = v[len(v)-1] - } - - return nil -} - -func parseParamSquareBrackets(k string) (string, error) { - bb := bytebufferpool.Get() - defer bytebufferpool.Put(bb) - - kbytes := []byte(k) - - for i, b := range kbytes { - - if b == '[' && kbytes[i+1] != ']' { - if err := bb.WriteByte('.'); err != nil { - return "", err - } - } - - if b == '[' || b == ']' { - continue - } - - if err := bb.WriteByte(b); err != nil { - return "", err - } - } - - return bb.String(), nil -} - -func equalFieldType(out any, kind reflect.Kind, key string) bool { - // Get type of interface - outTyp := reflect.TypeOf(out).Elem() - key = utils.ToLower(key) - - // Support maps - if outTyp.Kind() == reflect.Map && outTyp.Key().Kind() == reflect.String { - return true - } - - // Must be a struct to match a field - if outTyp.Kind() != reflect.Struct { - return false - } - // Copy interface to an value to be used - outVal := reflect.ValueOf(out).Elem() - // Loop over each field - for i := 0; i < outTyp.NumField(); i++ { - // Get field value data - structField := outVal.Field(i) - // Can this field be changed? - if !structField.CanSet() { - continue - } - // Get field key data - typeField := outTyp.Field(i) - // Get type of field key - structFieldKind := structField.Kind() - // Does the field type equals input? - if structFieldKind != kind { - continue - } - // Get tag from field if exist - inputFieldName := typeField.Tag.Get(QueryBinder.Name()) - if inputFieldName == "" { - inputFieldName = typeField.Name - } else { - inputFieldName = strings.Split(inputFieldName, ",")[0] - } - // Compare field/tag with provided key - if utils.ToLower(inputFieldName) == key { - return true - } - } - return false -} - -// Get content type from content type header -func FilterFlags(content string) string { - for i, char := range content { - if char == ' ' || char == ';' { - return content[:i] - } - } - return content -} diff --git a/binder/mapping_test.go b/binder/mapping_test.go deleted file mode 100644 index aec91ff2be..0000000000 --- a/binder/mapping_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package binder - -import ( - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -func Test_EqualFieldType(t *testing.T) { - var out int - require.False(t, equalFieldType(&out, reflect.Int, "key")) - - var dummy struct{ f string } - require.False(t, equalFieldType(&dummy, reflect.String, "key")) - - var dummy2 struct{ f string } - require.False(t, equalFieldType(&dummy2, reflect.String, "f")) - - var user struct { - Name string - Address string `query:"address"` - Age int `query:"AGE"` - } - require.True(t, equalFieldType(&user, reflect.String, "name")) - require.True(t, equalFieldType(&user, reflect.String, "Name")) - require.True(t, equalFieldType(&user, reflect.String, "address")) - require.True(t, equalFieldType(&user, reflect.String, "Address")) - require.True(t, equalFieldType(&user, reflect.Int, "AGE")) - require.True(t, equalFieldType(&user, reflect.Int, "age")) -} diff --git a/binder/query.go b/binder/query.go deleted file mode 100644 index ce62e09d0f..0000000000 --- a/binder/query.go +++ /dev/null @@ -1,49 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/fasthttp" -) - -type queryBinding struct{} - -func (*queryBinding) Name() string { - return "query" -} - -func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error { - data := make(map[string][]string) - var err error - - reqCtx.QueryArgs().VisitAll(func(key, val []byte) { - if err != nil { - return - } - - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(k, "[") { - k, err = parseParamSquareBrackets(k) - } - - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } - - }) - - if err != nil { - return err - } - - return parse(b.Name(), out, data) -} diff --git a/binder/resp_header.go b/binder/resp_header.go deleted file mode 100644 index 2b31710d24..0000000000 --- a/binder/resp_header.go +++ /dev/null @@ -1,34 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/fiber/v3/utils" - "github.com/valyala/fasthttp" -) - -type respHeaderBinding struct{} - -func (*respHeaderBinding) Name() string { - return "respHeader" -} - -func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error { - data := make(map[string][]string) - resp.Header.VisitAll(func(key, val []byte) { - k := utils.UnsafeString(key) - v := utils.UnsafeString(val) - - if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) { - values := strings.Split(v, ",") - for i := 0; i < len(values); i++ { - data[k] = append(data[k], values[i]) - } - } else { - data[k] = append(data[k], v) - } - }) - - return parse(b.Name(), out, data) -} diff --git a/binder/uri.go b/binder/uri.go deleted file mode 100644 index 2759f7b464..0000000000 --- a/binder/uri.go +++ /dev/null @@ -1,16 +0,0 @@ -package binder - -type uriBinding struct{} - -func (*uriBinding) Name() string { - return "uri" -} - -func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error { - data := make(map[string][]string, len(params)) - for _, param := range params { - data[param] = append(data[param], paramsFunc(param)) - } - - return parse(b.Name(), out, data) -} diff --git a/binder/xml.go b/binder/xml.go deleted file mode 100644 index 29401abb77..0000000000 --- a/binder/xml.go +++ /dev/null @@ -1,15 +0,0 @@ -package binder - -import ( - "encoding/xml" -) - -type xmlBinding struct{} - -func (*xmlBinding) Name() string { - return "xml" -} - -func (b *xmlBinding) Bind(body []byte, out any) error { - return xml.Unmarshal(body, out) -} diff --git a/ctx.go b/ctx.go index 06fc2e67a1..9f88d1f9ed 100644 --- a/ctx.go +++ b/ctx.go @@ -51,7 +51,6 @@ type DefaultCtx struct { fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx matched bool // Non use route matched viewBindMap *dictpool.Dict // Default view map to bind template engine - bind *Bind // Default bind reference } // tlsHandle object @@ -1320,16 +1319,3 @@ func (c *DefaultCtx) IsFromLocal() bool { } return c.isLocalHost(ips[0]) } - -// You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method. -// It gives custom binding support, detailed binding options and more. -// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser -func (c *DefaultCtx) Bind() *Bind { - if c.bind == nil { - c.bind = &Bind{ - ctx: c, - should: true, - } - } - return c.bind -} diff --git a/ctx_interface.go b/ctx_interface.go index 9f24a4c4b1..d18ae3d18b 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -329,11 +329,6 @@ type Ctx interface { // Reset is a method to reset context fields by given request when to use server handlers. Reset(fctx *fasthttp.RequestCtx) - // You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method. - // It gives custom binding support, detailed binding options and more. - // Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser - Bind() *Bind - // ClientHelloInfo return CHI from context ClientHelloInfo() *tls.ClientHelloInfo @@ -438,7 +433,6 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) { func (c *DefaultCtx) release() { c.route = nil c.fasthttp = nil - c.bind = nil if c.viewBindMap != nil { dictpool.ReleaseDict(c.viewBindMap) } diff --git a/error.go b/error.go index 87a6af38c9..d6aee39d99 100644 --- a/error.go +++ b/error.go @@ -1,10 +1,7 @@ package fiber import ( - errors "encoding/json" goErrors "errors" - - "github.com/gofiber/fiber/v3/internal/schema" ) // Range errors @@ -12,41 +9,3 @@ var ( ErrRangeMalformed = goErrors.New("range: malformed range header string") ErrRangeUnsatisfiable = goErrors.New("range: unsatisfiable range") ) - -// Binder errors -var ErrCustomBinderNotFound = goErrors.New("binder: custom binder not found, please be sure to enter the right name") - -// gorilla/schema errors -type ( - // Conversion error exposes the internal schema.ConversionError for public use. - ConversionError = schema.ConversionError - // UnknownKeyError error exposes the internal schema.UnknownKeyError for public use. - UnknownKeyError = schema.UnknownKeyError - // EmptyFieldError error exposes the internal schema.EmptyFieldError for public use. - EmptyFieldError = schema.EmptyFieldError - // MultiError error exposes the internal schema.MultiError for public use. - MultiError = schema.MultiError -) - -// encoding/json errors -type ( - // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. - // (The argument to Unmarshal must be a non-nil pointer.) - InvalidUnmarshalError = errors.InvalidUnmarshalError - - // A MarshalerError represents an error from calling a MarshalJSON or MarshalText method. - MarshalerError = errors.MarshalerError - - // A SyntaxError is a description of a JSON syntax error. - SyntaxError = errors.SyntaxError - - // An UnmarshalTypeError describes a JSON value that was - // not appropriate for a value of a specific Go type. - UnmarshalTypeError = errors.UnmarshalTypeError - - // An UnsupportedTypeError is returned by Marshal when attempting - // to encode an unsupported value type. - UnsupportedTypeError = errors.UnsupportedTypeError - - UnsupportedValueError = errors.UnsupportedValueError -) diff --git a/error_test.go b/error_test.go deleted file mode 100644 index 7fce3c12aa..0000000000 --- a/error_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package fiber - -import ( - "errors" - "testing" - - jerrors "encoding/json" - - "github.com/gofiber/fiber/v3/internal/schema" - "github.com/stretchr/testify/require" -) - -func TestConversionError(t *testing.T) { - ok := errors.As(ConversionError{}, &schema.ConversionError{}) - require.True(t, ok) -} - -func TestUnknownKeyError(t *testing.T) { - ok := errors.As(UnknownKeyError{}, &schema.UnknownKeyError{}) - require.True(t, ok) -} - -func TestEmptyFieldError(t *testing.T) { - ok := errors.As(EmptyFieldError{}, &schema.EmptyFieldError{}) - require.True(t, ok) -} - -func TestMultiError(t *testing.T) { - ok := errors.As(MultiError{}, &schema.MultiError{}) - require.True(t, ok) -} - -func TestInvalidUnmarshalError(t *testing.T) { - var e *jerrors.InvalidUnmarshalError - ok := errors.As(&InvalidUnmarshalError{}, &e) - require.True(t, ok) -} - -func TestMarshalerError(t *testing.T) { - var e *jerrors.MarshalerError - ok := errors.As(&MarshalerError{}, &e) - require.True(t, ok) -} - -func TestSyntaxError(t *testing.T) { - var e *jerrors.SyntaxError - ok := errors.As(&SyntaxError{}, &e) - require.True(t, ok) -} - -func TestUnmarshalTypeError(t *testing.T) { - var e *jerrors.UnmarshalTypeError - ok := errors.As(&UnmarshalTypeError{}, &e) - require.True(t, ok) -} - -func TestUnsupportedTypeError(t *testing.T) { - var e *jerrors.UnsupportedTypeError - ok := errors.As(&UnsupportedTypeError{}, &e) - require.True(t, ok) -} - -func TestUnsupportedValeError(t *testing.T) { - var e *jerrors.UnsupportedValueError - ok := errors.As(&UnsupportedValueError{}, &e) - require.True(t, ok) -} diff --git a/internal/schema/LICENSE b/internal/schema/LICENSE deleted file mode 100644 index 0e5fb87280..0000000000 --- a/internal/schema/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/schema/cache.go b/internal/schema/cache.go deleted file mode 100644 index bf21697cf1..0000000000 --- a/internal/schema/cache.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "errors" - "reflect" - "strconv" - "strings" - "sync" -) - -var errInvalidPath = errors.New("schema: invalid path") - -// newCache returns a new cache. -func newCache() *cache { - c := cache{ - m: make(map[reflect.Type]*structInfo), - regconv: make(map[reflect.Type]Converter), - tag: "schema", - } - return &c -} - -// cache caches meta-data about a struct. -type cache struct { - l sync.RWMutex - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string -} - -// registerConverter registers a converter function for a custom type. -func (c *cache) registerConverter(value interface{}, converterFunc Converter) { - c.regconv[reflect.TypeOf(value)] = converterFunc -} - -// parsePath parses a path in dotted notation verifying that it is a valid -// path to a struct field. -// -// It returns "path parts" which contain indices to fields to be used by -// reflect.Value.FieldByString(). Multiple parts are required for slices of -// structs. -func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { - var struc *structInfo - var field *fieldInfo - var index64 int64 - var err error - parts := make([]pathPart, 0) - path := make([]string, 0) - keys := strings.Split(p, ".") - for i := 0; i < len(keys); i++ { - if t.Kind() != reflect.Struct { - return nil, errInvalidPath - } - if struc = c.get(t); struc == nil { - return nil, errInvalidPath - } - if field = struc.get(keys[i]); field == nil { - return nil, errInvalidPath - } - // Valid field. Append index. - path = append(path, field.name) - if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { - // Parse a special case: slices of structs. - // i+1 must be the slice index. - // - // Now that struct can implements TextUnmarshaler interface, - // we don't need to force the struct's fields to appear in the path. - // So checking i+2 is not necessary anymore. - i++ - if i+1 > len(keys) { - return nil, errInvalidPath - } - if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { - return nil, errInvalidPath - } - parts = append(parts, pathPart{ - path: path, - field: field, - index: int(index64), - }) - path = make([]string, 0) - - // Get the next struct type, dropping ptrs. - if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - if t.Kind() == reflect.Slice { - t = t.Elem() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - } - } else if field.typ.Kind() == reflect.Ptr { - t = field.typ.Elem() - } else { - t = field.typ - } - } - // Add the remaining. - parts = append(parts, pathPart{ - path: path, - field: field, - index: -1, - }) - return parts, nil -} - -// get returns a cached structInfo, creating it if necessary. -func (c *cache) get(t reflect.Type) *structInfo { - c.l.RLock() - info := c.m[t] - c.l.RUnlock() - if info == nil { - info = c.create(t, "") - c.l.Lock() - c.m[t] = info - c.l.Unlock() - } - return info -} - -// create creates a structInfo with meta-data about a struct. -func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { - info := &structInfo{} - var anonymousInfos []*structInfo - for i := 0; i < t.NumField(); i++ { - if f := c.createField(t.Field(i), parentAlias); f != nil { - info.fields = append(info.fields, f) - if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { - anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) - } - } - } - for i, a := range anonymousInfos { - others := []*structInfo{info} - others = append(others, anonymousInfos[:i]...) - others = append(others, anonymousInfos[i+1:]...) - for _, f := range a.fields { - if !containsAlias(others, f.alias) { - info.fields = append(info.fields, f) - } - } - } - return info -} - -// createField creates a fieldInfo for the given field. -func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { - alias, options := fieldAlias(field, c.tag) - if alias == "-" { - // Ignore this field. - return nil - } - canonicalAlias := alias - if parentAlias != "" { - canonicalAlias = parentAlias + "." + alias - } - // Check if the type is supported and don't cache it if not. - // First let's get the basic type. - isSlice, isStruct := false, false - ft := field.Type - m := isTextUnmarshaler(reflect.Zero(ft)) - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - if isSlice = ft.Kind() == reflect.Slice; isSlice { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if ft.Kind() == reflect.Array { - ft = ft.Elem() - if ft.Kind() == reflect.Ptr { - ft = ft.Elem() - } - } - if isStruct = ft.Kind() == reflect.Struct; !isStruct { - if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { - // Type is not supported. - return nil - } - } - - return &fieldInfo{ - typ: field.Type, - name: field.Name, - alias: alias, - canonicalAlias: canonicalAlias, - unmarshalerInfo: m, - isSliceOfStructs: isSlice && isStruct, - isAnonymous: field.Anonymous, - isRequired: options.Contains("required"), - } -} - -// converter returns the converter for a type. -func (c *cache) converter(t reflect.Type) Converter { - return c.regconv[t] -} - -// ---------------------------------------------------------------------------- - -type structInfo struct { - fields []*fieldInfo -} - -func (i *structInfo) get(alias string) *fieldInfo { - for _, field := range i.fields { - if strings.EqualFold(field.alias, alias) { - return field - } - } - return nil -} - -func containsAlias(infos []*structInfo, alias string) bool { - for _, info := range infos { - if info.get(alias) != nil { - return true - } - } - return false -} - -type fieldInfo struct { - typ reflect.Type - // name is the field name in the struct. - name string - alias string - // canonicalAlias is almost the same as the alias, but is prefixed with - // an embedded struct field alias in dotted notation if this field is - // promoted from the struct. - // For instance, if the alias is "N" and this field is an embedded field - // in a struct "X", canonicalAlias will be "X.N". - canonicalAlias string - // unmarshalerInfo contains information regarding the - // encoding.TextUnmarshaler implementation of the field type. - unmarshalerInfo unmarshaler - // isSliceOfStructs indicates if the field type is a slice of structs. - isSliceOfStructs bool - // isAnonymous indicates whether the field is embedded in the struct. - isAnonymous bool - isRequired bool -} - -func (f *fieldInfo) paths(prefix string) []string { - if f.alias == f.canonicalAlias { - return []string{prefix + f.alias} - } - return []string{prefix + f.alias, prefix + f.canonicalAlias} -} - -type pathPart struct { - field *fieldInfo - path []string // path to the field: walks structs using field names. - index int // struct index in slices of structs. -} - -// ---------------------------------------------------------------------------- - -func indirectType(typ reflect.Type) reflect.Type { - if typ.Kind() == reflect.Ptr { - return typ.Elem() - } - return typ -} - -// fieldAlias parses a field tag to get a field alias. -func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { - if tag := field.Tag.Get(tagName); tag != "" { - alias, options = parseTag(tag) - } - if alias == "" { - alias = field.Name - } - return alias, options -} - -// tagOptions is the string following a comma in a struct field's tag, or -// the empty string. It does not include the leading comma. -type tagOptions []string - -// parseTag splits a struct field's url tag into its name and comma-separated -// options. -func parseTag(tag string) (string, tagOptions) { - s := strings.Split(tag, ",") - return s[0], s[1:] -} - -// Contains checks whether the tagOptions contains the specified option. -func (o tagOptions) Contains(option string) bool { - for _, s := range o { - if s == option { - return true - } - } - return false -} diff --git a/internal/schema/converter.go b/internal/schema/converter.go deleted file mode 100644 index 4f2116a15e..0000000000 --- a/internal/schema/converter.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "reflect" - "strconv" -) - -type Converter func(string) reflect.Value - -var ( - invalidValue = reflect.Value{} - boolType = reflect.Bool - float32Type = reflect.Float32 - float64Type = reflect.Float64 - intType = reflect.Int - int8Type = reflect.Int8 - int16Type = reflect.Int16 - int32Type = reflect.Int32 - int64Type = reflect.Int64 - stringType = reflect.String - uintType = reflect.Uint - uint8Type = reflect.Uint8 - uint16Type = reflect.Uint16 - uint32Type = reflect.Uint32 - uint64Type = reflect.Uint64 -) - -// Default converters for basic types. -var builtinConverters = map[reflect.Kind]Converter{ - boolType: convertBool, - float32Type: convertFloat32, - float64Type: convertFloat64, - intType: convertInt, - int8Type: convertInt8, - int16Type: convertInt16, - int32Type: convertInt32, - int64Type: convertInt64, - stringType: convertString, - uintType: convertUint, - uint8Type: convertUint8, - uint16Type: convertUint16, - uint32Type: convertUint32, - uint64Type: convertUint64, -} - -func convertBool(value string) reflect.Value { - if value == "on" { - return reflect.ValueOf(true) - } else if v, err := strconv.ParseBool(value); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertFloat32(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 32); err == nil { - return reflect.ValueOf(float32(v)) - } - return invalidValue -} - -func convertFloat64(value string) reflect.Value { - if v, err := strconv.ParseFloat(value, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertInt(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 0); err == nil { - return reflect.ValueOf(int(v)) - } - return invalidValue -} - -func convertInt8(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 8); err == nil { - return reflect.ValueOf(int8(v)) - } - return invalidValue -} - -func convertInt16(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 16); err == nil { - return reflect.ValueOf(int16(v)) - } - return invalidValue -} - -func convertInt32(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 32); err == nil { - return reflect.ValueOf(int32(v)) - } - return invalidValue -} - -func convertInt64(value string) reflect.Value { - if v, err := strconv.ParseInt(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} - -func convertString(value string) reflect.Value { - return reflect.ValueOf(value) -} - -func convertUint(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 0); err == nil { - return reflect.ValueOf(uint(v)) - } - return invalidValue -} - -func convertUint8(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 8); err == nil { - return reflect.ValueOf(uint8(v)) - } - return invalidValue -} - -func convertUint16(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 16); err == nil { - return reflect.ValueOf(uint16(v)) - } - return invalidValue -} - -func convertUint32(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 32); err == nil { - return reflect.ValueOf(uint32(v)) - } - return invalidValue -} - -func convertUint64(value string) reflect.Value { - if v, err := strconv.ParseUint(value, 10, 64); err == nil { - return reflect.ValueOf(v) - } - return invalidValue -} diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go deleted file mode 100644 index 9d44822202..0000000000 --- a/internal/schema/decoder.go +++ /dev/null @@ -1,534 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package schema - -import ( - "encoding" - "errors" - "fmt" - "reflect" - "strings" -) - -// NewDecoder returns a new Decoder. -func NewDecoder() *Decoder { - return &Decoder{cache: newCache()} -} - -// Decoder decodes values from a map[string][]string to a struct. -type Decoder struct { - cache *cache - zeroEmpty bool - ignoreUnknownKeys bool -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (d *Decoder) SetAliasTag(tag string) { - d.cache.tag = tag -} - -// ZeroEmpty controls the behaviour when the decoder encounters empty values -// in a map. -// If z is true and a key in the map has the empty string as a value -// then the corresponding struct field is set to the zero value. -// If z is false then empty strings are ignored. -// -// The default value is false, that is empty values do not change -// the value of the struct field. -func (d *Decoder) ZeroEmpty(z bool) { - d.zeroEmpty = z -} - -// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown -// keys in the map. -// If i is true and an unknown field is encountered, it is ignored. This is -// similar to how unknown keys are handled by encoding/json. -// If i is false then Decode will return an error. Note that any valid keys -// will still be decoded in to the target struct. -// -// To preserve backwards compatibility, the default value is false. -func (d *Decoder) IgnoreUnknownKeys(i bool) { - d.ignoreUnknownKeys = i -} - -// RegisterConverter registers a converter function for a custom type. -func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { - d.cache.registerConverter(value, converterFunc) -} - -// Decode decodes a map[string][]string to a struct. -// -// The first parameter must be a pointer to a struct. -// -// The second parameter is a map, typically url.Values from an HTTP request. -// Keys are "paths" in dotted notation to the struct fields and nested structs. -// -// See the package documentation for a full explanation of the mechanics. -func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { - return errors.New("schema: interface must be a pointer to struct") - } - v = v.Elem() - t := v.Type() - errors := MultiError{} - for path, values := range src { - if parts, err := d.cache.parsePath(path, t); err == nil { - if err = d.decode(v, path, parts, values); err != nil { - errors[path] = err - } - } else if !d.ignoreUnknownKeys { - errors[path] = UnknownKeyError{Key: path} - } - } - errors.merge(d.checkRequired(t, src)) - if len(errors) > 0 { - return errors - } - return nil -} - -// checkRequired checks whether required fields are empty -// -// check type t recursively if t has struct fields. -// -// src is the source map for decoding, we use it here to see if those required fields are included in src -func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { - m, errs := d.findRequiredFields(t, "", "") - for key, fields := range m { - if isEmptyFields(fields, src) { - errs[key] = EmptyFieldError{Key: key} - } - } - return errs -} - -// findRequiredFields recursively searches the struct type t for required fields. -// -// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation -// for nested struct fields. canonicalPrefix is a complete path which never omits -// any embedded struct fields. searchPrefix is a user-friendly path which may omit -// some embedded struct fields to point promoted fields. -func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { - struc := d.cache.get(t) - if struc == nil { - // unexpect, cache.get never return nil - return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} - } - - m := map[string][]fieldWithPrefix{} - errs := MultiError{} - for _, f := range struc.fields { - if f.typ.Kind() == reflect.Struct { - fcprefix := canonicalPrefix + f.canonicalAlias + "." - for _, fspath := range f.paths(searchPrefix) { - fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") - for key, fields := range fm { - m[key] = append(m[key], fields...) - } - errs.merge(ferrs) - } - } - if f.isRequired { - key := canonicalPrefix + f.canonicalAlias - m[key] = append(m[key], fieldWithPrefix{ - fieldInfo: f, - prefix: searchPrefix, - }) - } - } - return m, errs -} - -type fieldWithPrefix struct { - *fieldInfo - prefix string -} - -// isEmptyFields returns true if all of specified fields are empty. -func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { - for _, f := range fields { - for _, path := range f.paths(f.prefix) { - v, ok := src[path] - if ok && !isEmpty(f.typ, v) { - return false - } - for key := range src { - // issue references: - // https://github.com/gofiber/fiber/issues/1414 - // https://github.com/gorilla/schema/issues/176 - nested := strings.IndexByte(key, '.') != -1 - - // for non required nested structs - c1 := strings.HasSuffix(f.prefix, ".") && key == path - - // for required nested structs - c2 := f.prefix == "" && nested && strings.HasPrefix(key, path) - - // for non nested fields - c3 := f.prefix == "" && !nested && key == path - if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) { - return false - } - } - } - } - return true -} - -// isEmpty returns true if value is empty for specific type -func isEmpty(t reflect.Type, value []string) bool { - if len(value) == 0 { - return true - } - switch t.Kind() { - case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: - return len(value[0]) == 0 - } - return false -} - -// decode fills a struct field using a parsed path. -func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { - // Get the field walking the struct fields by index. - for _, name := range parts[0].path { - if v.Type().Kind() == reflect.Ptr { - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - v = v.Elem() - } - - // alloc embedded structs - if v.Type().Kind() == reflect.Struct { - for i := 0; i < v.NumField(); i++ { - field := v.Field(i) - if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { - field.Set(reflect.New(field.Type().Elem())) - } - } - } - - v = v.FieldByName(name) - } - // Don't even bother for unexported fields. - if !v.CanSet() { - return nil - } - - // Dereference if needed. - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - if v.IsNil() { - v.Set(reflect.New(t)) - } - v = v.Elem() - } - - // Slice of structs. Let's go recursive. - if len(parts) > 1 { - idx := parts[0].index - if v.IsNil() || v.Len() < idx+1 { - value := reflect.MakeSlice(t, idx+1, idx+1) - if v.Len() < idx+1 { - // Resize it. - reflect.Copy(value, v) - } - v.Set(value) - } - return d.decode(v.Index(idx), path, parts[1:], values) - } - - // Get the converter early in case there is one for a slice type. - conv := d.cache.converter(t) - m := isTextUnmarshaler(v) - if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { - var items []reflect.Value - elemT := t.Elem() - isPtrElem := elemT.Kind() == reflect.Ptr - if isPtrElem { - elemT = elemT.Elem() - } - - // Try to get a converter for the element type. - conv := d.cache.converter(elemT) - if conv == nil { - conv = builtinConverters[elemT.Kind()] - if conv == nil { - // As we are not dealing with slice of structs here, we don't need to check if the type - // implements TextUnmarshaler interface - return fmt.Errorf("schema: converter not found for %v", elemT) - } - } - - for key, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if m.IsValid { - u := reflect.New(elemT) - if m.IsSliceElementPtr { - u = reflect.New(reflect.PtrTo(elemT).Elem()) - } - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: key, - Err: err, - } - } - if m.IsSliceElementPtr { - items = append(items, u.Elem().Addr()) - } else if u.Kind() == reflect.Ptr { - items = append(items, u.Elem()) - } else { - items = append(items, u) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - if strings.Contains(value, ",") { - values := strings.Split(value, ",") - for _, value := range values { - if value == "" { - if d.zeroEmpty { - items = append(items, reflect.Zero(elemT)) - } - } else if item := conv(value); item.IsValid() { - if isPtrElem { - ptr := reflect.New(elemT) - ptr.Elem().Set(item) - item = ptr - } - if item.Type() != elemT && !isPtrElem { - item = item.Convert(elemT) - } - items = append(items, item) - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } else { - return ConversionError{ - Key: path, - Type: elemT, - Index: key, - } - } - } - } - value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) - v.Set(value) - } else { - val := "" - // Use the last value provided if any values were provided - if len(values) > 0 { - val = values[len(values)-1] - } - - if conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else if m.IsValid { - if m.IsPtr { - u := reflect.New(v.Type()) - if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - v.Set(reflect.Indirect(u)) - } else { - // If the value implements the encoding.TextUnmarshaler interface - // apply UnmarshalText as the converter - if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - Err: err, - } - } - } - } else if val == "" { - if d.zeroEmpty { - v.Set(reflect.Zero(t)) - } - } else if conv := builtinConverters[t.Kind()]; conv != nil { - if value := conv(val); value.IsValid() { - v.Set(value.Convert(t)) - } else { - return ConversionError{ - Key: path, - Type: t, - Index: -1, - } - } - } else { - return fmt.Errorf("schema: converter not found for %v", t) - } - } - return nil -} - -func isTextUnmarshaler(v reflect.Value) unmarshaler { - // Create a new unmarshaller instance - m := unmarshaler{} - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // As the UnmarshalText function should be applied to the pointer of the - // type, we check that type to see if it implements the necessary - // method. - if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { - m.IsPtr = true - return m - } - - // if v is []T or *[]T create new T - t := v.Type() - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - if t.Kind() == reflect.Slice { - // Check if the slice implements encoding.TextUnmarshaller - if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { - return m - } - // If t is a pointer slice, check if its elements implement - // encoding.TextUnmarshaler - m.IsSliceElement = true - if t = t.Elem(); t.Kind() == reflect.Ptr { - t = reflect.PtrTo(t.Elem()) - v = reflect.Zero(t) - m.IsSliceElementPtr = true - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m - } - } - - v = reflect.New(t) - m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) - return m -} - -// TextUnmarshaler helpers ---------------------------------------------------- -// unmarshaller contains information about a TextUnmarshaler type -type unmarshaler struct { - Unmarshaler encoding.TextUnmarshaler - // IsValid indicates whether the resolved type indicated by the other - // flags implements the encoding.TextUnmarshaler interface. - IsValid bool - // IsPtr indicates that the resolved type is the pointer of the original - // type. - IsPtr bool - // IsSliceElement indicates that the resolved type is a slice element of - // the original type. - IsSliceElement bool - // IsSliceElementPtr indicates that the resolved type is a pointer to a - // slice element of the original type. - IsSliceElementPtr bool -} - -// Errors --------------------------------------------------------------------- - -// ConversionError stores information about a failed conversion. -type ConversionError struct { - Key string // key from the source map. - Type reflect.Type // expected type of elem - Index int // index for multi-value fields; -1 for single-value fields. - Err error // low-level error (when it exists) -} - -func (e ConversionError) Error() string { - var output string - - if e.Index < 0 { - output = fmt.Sprintf("schema: error converting value for %q", e.Key) - } else { - output = fmt.Sprintf("schema: error converting value for index %d of %q", - e.Index, e.Key) - } - - if e.Err != nil { - output = fmt.Sprintf("%s. Details: %s", output, e.Err) - } - - return output -} - -// UnknownKeyError stores information about an unknown key in the source map. -type UnknownKeyError struct { - Key string // key from the source map. -} - -func (e UnknownKeyError) Error() string { - return fmt.Sprintf("schema: invalid path %q", e.Key) -} - -// EmptyFieldError stores information about an empty required field. -type EmptyFieldError struct { - Key string // required key in the source map. -} - -func (e EmptyFieldError) Error() string { - return fmt.Sprintf("%v is empty", e.Key) -} - -// MultiError stores multiple decoding errors. -// -// Borrowed from the App Engine SDK. -type MultiError map[string]error - -func (e MultiError) Error() string { - s := "" - for _, err := range e { - s = err.Error() - break - } - switch len(e) { - case 0: - return "(0 errors)" - case 1: - return s - case 2: - return s + " (and 1 other error)" - } - return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) -} - -func (e MultiError) merge(errors MultiError) { - for key, err := range errors { - if e[key] == nil { - e[key] = err - } - } -} diff --git a/internal/schema/doc.go b/internal/schema/doc.go deleted file mode 100644 index fff0fe7616..0000000000 --- a/internal/schema/doc.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package gorilla/schema fills a struct with form values. - -The basic usage is really simple. Given this struct: - - type Person struct { - Name string - Phone string - } - -...we can fill it passing a map to the Decode() function: - - values := map[string][]string{ - "Name": {"John"}, - "Phone": {"999-999-999"}, - } - person := new(Person) - decoder := schema.NewDecoder() - decoder.Decode(person, values) - -This is just a simple example and it doesn't make a lot of sense to create -the map manually. Typically it will come from a http.Request object and -will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: - - func MyHandler(w http.ResponseWriter, r *http.Request) { - err := r.ParseForm() - - if err != nil { - // Handle error - } - - decoder := schema.NewDecoder() - // r.PostForm is a map of our POST form values - err := decoder.Decode(person, r.PostForm) - - if err != nil { - // Handle error - } - - // Do something with person.Name or person.Phone - } - -Note: it is a good idea to set a Decoder instance as a package global, -because it caches meta-data about structs, and an instance can be shared safely: - - var decoder = schema.NewDecoder() - -To define custom names for fields, use a struct tag "schema". To not populate -certain fields, use a dash for the name and it will be ignored: - - type Person struct { - Name string `schema:"name"` // custom name - Phone string `schema:"phone"` // custom name - Admin bool `schema:"-"` // this field is never set - } - -The supported field types in the destination struct are: - - - bool - - float variants (float32, float64) - - int variants (int, int8, int16, int32, int64) - - string - - uint variants (uint, uint8, uint16, uint32, uint64) - - struct - - a pointer to one of the above types - - a slice or a pointer to a slice of one of the above types - -Non-supported types are simply ignored, however custom types can be registered -to be converted. - -To fill nested structs, keys must use a dotted notation as the "path" for the -field. So for example, to fill the struct Person below: - - type Phone struct { - Label string - Number string - } - - type Person struct { - Name string - Phone Phone - } - -...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". -This means that an HTML form to fill a Person struct must look like this: - -
- - - -
- -Single values are filled using the first value for a key from the source map. -Slices are filled using all values for a key from the source map. So to fill -a Person with multiple Phone values, like: - - type Person struct { - Name string - Phones []Phone - } - -...an HTML form that accepts three Phone values would look like this: - -
- - - - - - - -
- -Notice that only for slices of structs the slice index is required. -This is needed for disambiguation: if the nested struct also had a slice -field, we could not translate multiple values to it if we did not use an -index for the parent struct. - -There's also the possibility to create a custom type that implements the -TextUnmarshaler interface, and in this case there's no need to register -a converter, like: - - type Person struct { - Emails []Email - } - - type Email struct { - *mail.Address - } - - func (e *Email) UnmarshalText(text []byte) (err error) { - e.Address, err = mail.ParseAddress(string(text)) - return - } - -...an HTML form that accepts three Email values would look like this: - -
- - - -
-*/ -package schema diff --git a/internal/schema/encoder.go b/internal/schema/encoder.go deleted file mode 100644 index f0ed631210..0000000000 --- a/internal/schema/encoder.go +++ /dev/null @@ -1,202 +0,0 @@ -package schema - -import ( - "errors" - "fmt" - "reflect" - "strconv" -) - -type encoderFunc func(reflect.Value) string - -// Encoder encodes values from a struct into url.Values. -type Encoder struct { - cache *cache - regenc map[reflect.Type]encoderFunc -} - -// NewEncoder returns a new Encoder with defaults. -func NewEncoder() *Encoder { - return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} -} - -// Encode encodes a struct into map[string][]string. -// -// Intended for use with url.Values. -func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { - v := reflect.ValueOf(src) - - return e.encode(v, dst) -} - -// RegisterEncoder registers a converter for encoding a custom type. -func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { - e.regenc[reflect.TypeOf(value)] = encoder -} - -// SetAliasTag changes the tag used to locate custom field aliases. -// The default tag is "schema". -func (e *Encoder) SetAliasTag(tag string) { - e.cache.tag = tag -} - -// isValidStructPointer test if input value is a valid struct pointer. -func isValidStructPointer(v reflect.Value) bool { - return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct -} - -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Func: - case reflect.Map, reflect.Slice: - return v.IsNil() || v.Len() == 0 - case reflect.Array: - z := true - for i := 0; i < v.Len(); i++ { - z = z && isZero(v.Index(i)) - } - return z - case reflect.Struct: - type zero interface { - IsZero() bool - } - if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { - iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] - return iz.Interface().(bool) - } - z := true - for i := 0; i < v.NumField(); i++ { - z = z && isZero(v.Field(i)) - } - return z - } - // Compare other types directly: - z := reflect.Zero(v.Type()) - return v.Interface() == z.Interface() -} - -func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { - if v.Kind() == reflect.Ptr { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return errors.New("schema: interface must be a struct") - } - t := v.Type() - - errors := MultiError{} - - for i := 0; i < v.NumField(); i++ { - name, opts := fieldAlias(t.Field(i), e.cache.tag) - if name == "-" { - continue - } - - // Encode struct pointer types if the field is a valid pointer and a struct. - if isValidStructPointer(v.Field(i)) { - e.encode(v.Field(i).Elem(), dst) - continue - } - - encFunc := typeEncoder(v.Field(i).Type(), e.regenc) - - // Encode non-slice types and custom implementations immediately. - if encFunc != nil { - value := encFunc(v.Field(i)) - if opts.Contains("omitempty") && isZero(v.Field(i)) { - continue - } - - dst[name] = append(dst[name], value) - continue - } - - if v.Field(i).Type().Kind() == reflect.Struct { - e.encode(v.Field(i), dst) - continue - } - - if v.Field(i).Type().Kind() == reflect.Slice { - encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) - } - - if encFunc == nil { - errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) - continue - } - - // Encode a slice. - if v.Field(i).Len() == 0 && opts.Contains("omitempty") { - continue - } - - dst[name] = []string{} - for j := 0; j < v.Field(i).Len(); j++ { - dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) - } - } - - if len(errors) > 0 { - return errors - } - return nil -} - -func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { - if f, ok := reg[t]; ok { - return f - } - - switch t.Kind() { - case reflect.Bool: - return encodeBool - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return encodeInt - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return encodeUint - case reflect.Float32: - return encodeFloat32 - case reflect.Float64: - return encodeFloat64 - case reflect.Ptr: - f := typeEncoder(t.Elem(), reg) - return func(v reflect.Value) string { - if v.IsNil() { - return "null" - } - return f(v.Elem()) - } - case reflect.String: - return encodeString - default: - return nil - } -} - -func encodeBool(v reflect.Value) string { - return strconv.FormatBool(v.Bool()) -} - -func encodeInt(v reflect.Value) string { - return strconv.FormatInt(int64(v.Int()), 10) -} - -func encodeUint(v reflect.Value) string { - return strconv.FormatUint(uint64(v.Uint()), 10) -} - -func encodeFloat(v reflect.Value, bits int) string { - return strconv.FormatFloat(v.Float(), 'f', 6, bits) -} - -func encodeFloat32(v reflect.Value) string { - return encodeFloat(v, 32) -} - -func encodeFloat64(v reflect.Value) string { - return encodeFloat(v, 64) -} - -func encodeString(v reflect.Value) string { - return v.String() -} diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 72f7876463..74a1f43be4 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -237,15 +237,10 @@ func New(config ...Config) fiber.Handler { case TagResBody: return buf.Write(c.Response().Body()) case TagReqHeaders: - out := make(map[string]string, 0) - if err := c.Bind().Header(&out); err != nil { - return 0, err - } - reqHeaders := make([]string, 0) - for k, v := range out { - reqHeaders = append(reqHeaders, k+"="+v) - } + c.Request().Header.VisitAll(func(k, v []byte) { + reqHeaders = append(reqHeaders, string(k)+"="+string(v)) + }) return buf.Write([]byte(strings.Join(reqHeaders, "&"))) case TagQueryStringParams: return buf.WriteString(c.Request().URI().QueryArgs().String()) From 3251afc8c98247cf498652808260a91653733ae1 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Tue, 23 Aug 2022 20:47:23 +0800 Subject: [PATCH 02/31] 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 378a3426b9..9b40b4084b 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 0000000000..cce399203b --- /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 0000000000..77cc5773bc --- /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 0000000000..1f1f4aca4d --- /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 0000000000..4ce2f1b6b7 --- /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 0000000000..68eb47a2e1 --- /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 0000000000..c9031abfe9 --- /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 0000000000..862969a334 --- /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 987b0c3cbb..daf78c0baf 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 9f88d1f9ed..1887363552 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 d18ae3d18b..98e9cd3122 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 d6aee39d99..965d712450 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 0000000000..a7f207cea3 --- /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 0000000000..da5ca7ae66 --- /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 0000000000..6b1cb4855d --- /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 0000000000..521b2277b7 --- /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 0000000000..55b5b5811d --- /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 0000000000..8cccc95378 --- /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 0000000000..7416da003b --- /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 9cc23512b0..f205cb6633 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 0000000000..72dfee6ca9 --- /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/31] 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 68eb47a2e1..b111b21ac1 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 965d712450..0e81e4ebae 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/31] 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 7416da003b..85a906ca7a 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 0000000000..7532cc4e91 --- /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/31] 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 9b40b4084b..c346f7075d 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 1f1f4aca4d..1a680b3fb5 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 4ce2f1b6b7..399c03a6e0 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 b111b21ac1..59ae105a8d 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 c9031abfe9..3f02f108d5 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 862969a334..0000000000 --- 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 1887363552..22939b6f09 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/31] 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 399c03a6e0..e651f323dc 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 85a906ca7a..bd4ee7ecdc 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 0000000000..eec58bff01 --- /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 7532cc4e91..0000000000 --- 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/31] 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 77cc5773bc..c9364696d2 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 e651f323dc..1f2d651a72 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 22939b6f09..fc383c7644 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 98e9cd3122..92e17d63a2 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/31] 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 c9364696d2..6e9b42feca 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/31] 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 e5dd1190a3..44ddb56d61 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/31] 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 44ddb56d61..f44c0d8ab1 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/31] 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 f44c0d8ab1..37bd66c291 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/31] bench params --- bind_test.go | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/bind_test.go b/bind_test.go index 1a680b3fb5..21cce7f5a0 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/31] remove dead code --- ctx.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ctx.go b/ctx.go index 4cbbafc1c9..fb617adf04 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/31] 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 6e9b42feca..341626e7af 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/31] fix test --- redirect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirect.go b/redirect.go index 1148edbf48..ffc478c36d 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/31] 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 cce399203b..6d4d18ad9f 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 21cce7f5a0..d339709950 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 2085f50606..38e02b56e7 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/31] 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 d339709950..ae135ec83f 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 38e02b56e7..4e0331a014 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 e3eb828c0e..409c19079c 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/31] 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 6d4d18ad9f..760d3cab83 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 ae135ec83f..b94ba4d5af 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 4e0331a014..3b091389e9 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/31] 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 72f446be1b..e65377ced9 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 04a224cd9a..31c0de88bc 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 b4c0c55d38..8b0aebdfee 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/31] 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 b94ba4d5af..1dcd1b9e28 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 da5ca7ae66..252afc6e43 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 0000000000..300cbf7b25 --- /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/31] 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 51aad01c8f..e7360678e7 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 8c92248423..0bdb5eacfa 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 afc0e34ad4..743e796bec 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/31] 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 5c6447fd28..e5eec05a0c 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 0aa517bcf5..3c2c0774b2 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() From e7545b2fb6975d4dd4eda4f7e44a8a2b38b7c087 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 30 Aug 2024 19:54:09 +0300 Subject: [PATCH 23/31] update --- binder.go | 6 ------ ctx_interface_gen.go | 12 ++++++------ middleware/logger/tags.go | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/binder.go b/binder.go index ef8ccce776..86bf2bd4cd 100644 --- a/binder.go +++ b/binder.go @@ -21,12 +21,6 @@ type Bind struct { strict bool } -func (c *DefaultCtx) Bind() *Bind { - b := binderPool.Get().(*Bind) - b.ctx = c - return b -} - func (b *Bind) Strict() *Bind { b.strict = true return b diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index 7709f7c929..f5f35cd880 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -11,8 +11,7 @@ import ( "github.com/valyala/fasthttp" ) -// Ctx represents the Context which hold the HTTP request and response. -// It has methods for the request query string, parameters, body, HTTP headers and so on. +// Ctx represents the Context which hold the HTTP request and response.\nIt has methods for the request query string, parameters, body, HTTP headers and so on. type Ctx interface { // Accepts checks if the specified extensions or content types are acceptable. Accepts(offers ...string) string @@ -31,6 +30,8 @@ type Ctx interface { Attachment(filename ...string) // BaseURL returns (protocol + host + base path). BaseURL() string + Bind() *Bind + Validate(v any) error // BodyRaw 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. @@ -322,10 +323,9 @@ type Ctx interface { isLocalHost(address string) bool // IsFromLocal will return true if request came from local. IsFromLocal() bool - // Bind 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 + // AllParams Params is used to get all route parameters. + // Using Params method to get params. + GetParams() map[string]string // Reset is a method to reset context fields by given request when to use server handlers. Reset(fctx *fasthttp.RequestCtx) // Release is a method to reset context fields when to use ReleaseCtx() diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 7fc014c789..3679745446 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -99,9 +99,9 @@ func createTagMap(cfg *Config) map[string]LogFunc { return output.Write(c.Response().Body()) }, TagReqHeaders: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { - out := c.GetRespHeaders() - fmt.Print(out) + out := c.GetReqHeaders() reqHeaders := make([]string, 0) + for k, v := range out { reqHeaders = append(reqHeaders, k+"="+strings.Join(v, ",")) } From 857fdc023ef38daec5ba62c239192e9ffba0e01e Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 30 Aug 2024 21:05:20 +0300 Subject: [PATCH 24/31] Revert "support pointer fields" This reverts commit 41830699d1d8d5e2eafa51a0365a7770fb60cf86. --- bind.go | 22 ++++++---------------- bind_test.go | 36 ++++-------------------------------- binder_compile.go | 13 +------------ 3 files changed, 11 insertions(+), 60 deletions(-) diff --git a/bind.go b/bind.go index 760d3cab83..6d4d18ad9f 100644 --- a/bind.go +++ b/bind.go @@ -41,7 +41,6 @@ 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 } @@ -53,26 +52,17 @@ func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { } var err error - 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) + if len(d.parentIndex) > 0 { + for _, i := range d.parentIndex { + reqValue = reqValue.Field(i) } - field.Set(elem) + err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) - return nil + } else { + err = d.dec.UnmarshalString(text, reqValue.Field(d.index)) } - // 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 993a4ba59b..d13f2da0e8 100644 --- a/bind_test.go +++ b/bind_test.go @@ -26,7 +26,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 { @@ -35,7 +35,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) } @@ -48,12 +48,11 @@ 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"` @@ -63,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) { @@ -93,33 +92,6 @@ 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.AcquireCtx(&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 3b091389e9..4e0331a014 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -113,9 +113,6 @@ 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 @@ -188,12 +185,7 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - et := field.Type - if field.Type.Kind() == reflect.Ptr { - et = field.Type.Elem() - } - - textDecoder, err := bind.CompileTextDecoder(et) + textDecoder, err := bind.CompileTextDecoder(field.Type) if err != nil { return nil, err } @@ -205,7 +197,6 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag reqField: tagContent, dec: textDecoder, get: get, - et: et, } if len(parentIndex) > 0 { @@ -215,13 +206,11 @@ 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 12c70ad8ea408093447540f8e496719410257c66 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 30 Aug 2024 21:05:50 +0300 Subject: [PATCH 25/31] Revert "add support for queries like data[0][name] (not yet supporting deeper nested levels)" This reverts commit 3661d336c89d8f83452c96d288a3a89a3b0fc362. --- bind_test.go | 27 -------------- binder_compile.go | 70 ++++++------------------------------- binder_slice.go | 89 ----------------------------------------------- 3 files changed, 11 insertions(+), 175 deletions(-) diff --git a/bind_test.go b/bind_test.go index d13f2da0e8..2c68f49fe9 100644 --- a/bind_test.go +++ b/bind_test.go @@ -65,33 +65,6 @@ 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.AcquireCtx(&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 4e0331a014..38e02b56e7 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -72,12 +72,17 @@ type parentStruct struct { index []int } -func lookupTagScope(field reflect.StructField, opt bindCompileOption) (tagScope string) { +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 + } + 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 @@ -85,15 +90,6 @@ func lookupTagScope(field reflect.StructField, opt bindCompileOption) (tagScope } } - 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 } @@ -206,56 +202,15 @@ 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() - 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) - } + elementUnmarshaler, err := bind.CompileTextDecoder(et) + if err != nil { + return nil, fmt.Errorf("failed to build slice binder: %w", err) } var eqBytes = bytes.Equal @@ -281,8 +236,7 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - fieldSliceDecoder := &fieldSliceDecoder{ - elems: elems, + return []decoder{&fieldSliceDecoder{ fieldIndex: index, eqBytes: eqBytes, fieldName: field.Name, @@ -291,7 +245,5 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag fieldType: field.Type, elementType: et, elementDecoder: elementUnmarshaler, - } - - return []decoder{fieldSliceDecoder}, nil + }}, nil } diff --git a/binder_slice.go b/binder_slice.go index 409c19079c..e3eb828c0e 100644 --- a/binder_slice.go +++ b/binder_slice.go @@ -1,9 +1,7 @@ package fiber import ( - "bytes" "reflect" - "strconv" "github.com/gofiber/fiber/v3/internal/bind" "github.com/gofiber/utils/v2" @@ -13,7 +11,6 @@ var _ decoder = (*fieldSliceDecoder)(nil) type fieldSliceDecoder struct { fieldIndex int - elems []subElem fieldName string fieldType reflect.Type reqKey []byte @@ -25,10 +22,6 @@ 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) { @@ -66,88 +59,6 @@ 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 52a8b8aa96f222f88be7a128be229a24b9e9116c Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 30 Aug 2024 21:06:09 +0300 Subject: [PATCH 26/31] Revert "add basic nested binding support (not yet for slices)" This reverts commit 6cb876a51bc6d2c4e237fa54b074f9b95bbf032c. --- bind.go | 26 +++++------------- bind_test.go | 26 ------------------ binder_compile.go | 68 ++++++++--------------------------------------- 3 files changed, 18 insertions(+), 102 deletions(-) diff --git a/bind.go b/bind.go index 6d4d18ad9f..cce399203b 100644 --- a/bind.go +++ b/bind.go @@ -36,13 +36,12 @@ func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { } type fieldTextDecoder struct { - 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 + 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 { @@ -51,18 +50,7 @@ func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } - 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)) - } - + 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 2c68f49fe9..3dd961064a 100644 --- a/bind_test.go +++ b/bind_test.go @@ -39,32 +39,6 @@ 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.AcquireCtx(&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 38e02b56e7..2085f50606 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, parentStruct{}) + dec, err := compileFieldDecoder(el.Field(i), i, opt) if err != nil { return nil, err } if dec != nil { - decoders = append(decoders, dec...) + decoders = append(decoders, dec) } } @@ -67,14 +67,9 @@ func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { }, nil } -type parentStruct struct { - tag string - index []int -} - -func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent parentStruct) ([]decoder, error) { +func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption) (decoder, error) { if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { - return []decoder{&fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}}, nil + return &fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}, nil } var tags = []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} @@ -96,10 +91,6 @@ 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) } @@ -108,38 +99,7 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp return compileSliceFieldTextBasedDecoder(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) + return compileTextBasedDecoder(field, index, tagScope, tagContent) } func formGetter(ctx Ctx, key string, defaultValue ...string) string { @@ -160,7 +120,7 @@ func multipartGetter(ctx Ctx, key string, defaultValue ...string) string { return v[0] } -func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string, parentIndex ...[]int) ([]decoder, error) { +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: @@ -186,23 +146,17 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return nil, err } - fieldDecoder := &fieldTextDecoder{ + return &fieldTextDecoder{ index: index, fieldName: field.Name, tag: tagScope, reqField: tagContent, dec: textDecoder, get: get, - } - - if len(parentIndex) > 0 { - fieldDecoder.parentIndex = parentIndex[0] - } - - return []decoder{fieldDecoder}, nil + }, 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()) } @@ -236,7 +190,7 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) } - return []decoder{&fieldSliceDecoder{ + return &fieldSliceDecoder{ fieldIndex: index, eqBytes: eqBytes, fieldName: field.Name, @@ -245,5 +199,5 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag fieldType: field.Type, elementType: et, elementDecoder: elementUnmarshaler, - }}, nil + }, nil } From 5f1945090f7614f780e6eaff4ebfef6440e98cd6 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Fri, 30 Aug 2024 23:37:31 +0300 Subject: [PATCH 27/31] binder_compile: support embedding simple structs --- bind.go | 13 +-- binder_compile.go | 71 +++++++++++---- binder_compile_test.go | 187 +++++++++++++++++++++++++++++++++++++++ internal/bind/compile.go | 2 +- 4 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 binder_compile_test.go diff --git a/bind.go b/bind.go index cce399203b..28c871b106 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 + fieldName string + tag string // query,param,header,respHeader ... + reqField string + dec bind.TextDecoder + get func(c Ctx, key string, defaultValue ...string) string + subFieldDecoders []decoder } func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { diff --git a/binder_compile.go b/binder_compile.go index 2085f50606..c2fe710c0e 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -45,7 +45,7 @@ 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, nil) if err != nil { return nil, err } @@ -67,11 +67,18 @@ func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { }, nil } -func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption) (decoder, error) { - if reflect.PtrTo(field.Type).Implements(bindUnmarshalerType) { +type parentDecoder struct { + tagScope string + tagContent string +} + +func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent *parentDecoder) (decoder, error) { + // Custom unmarshaler + if reflect.PointerTo(field.Type).Implements(bindUnmarshalerType) { return &fieldCtxDecoder{index: index, fieldName: field.Name, fieldType: field.Type}, nil } + // Validate tag scope var tags = []string{bindTagRespHeader, bindTagQuery, bindTagParam, bindTagHeader, bindTagCookie} if opt.bodyDecoder { tags = []string{bindTagForm, bindTagMultipart} @@ -89,17 +96,24 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp return nil, nil } - tagContent := field.Tag.Get(tagScope) + // If parent tag scope is present, just override it and append the parent tag content + var tagContent string + if parent != nil { + tagScope = parent.tagScope + tagContent = parent.tagContent + "." + field.Tag.Get(tagScope) + } else { + tagContent = field.Tag.Get(tagScope) + } - if reflect.PtrTo(field.Type).Implements(textUnmarshalerType) { - return compileTextBasedDecoder(field, index, tagScope, tagContent) + if reflect.PointerTo(field.Type).Implements(textUnmarshalerType) { + return compileTextBasedDecoder(field, index, tagScope, tagContent, opt) } if field.Type.Kind() == reflect.Slice { return compileSliceFieldTextBasedDecoder(field, index, tagScope, tagContent) } - return compileTextBasedDecoder(field, index, tagScope, tagContent) + return compileTextBasedDecoder(field, index, tagScope, tagContent, opt) } func formGetter(ctx Ctx, key string, defaultValue ...string) string { @@ -120,7 +134,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, opt bindCompileOption) (decoder, error) { var get func(ctx Ctx, key string, defaultValue ...string) string switch tagScope { case bindTagQuery: @@ -141,19 +155,44 @@ 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) - if err != nil { - return nil, err - } - - return &fieldTextDecoder{ + fieldDecoder := &fieldTextDecoder{ index: index, fieldName: field.Name, tag: tagScope, reqField: tagContent, - dec: textDecoder, get: get, - }, nil + } + + // Support simple embeded structs + if field.Type.Kind() == reflect.Struct { + var decoders []decoder + for i := 0; i < field.Type.NumField(); i++ { + if !field.Type.Field(i).IsExported() { + // ignore unexported field + continue + } + + dec, err := compileFieldDecoder(field.Type.Field(i), i, opt, &parentDecoder{tagScope: tagScope, tagContent: tagContent}) + if err != nil { + return nil, err + } + + decoders = append(decoders, dec) + } + + fieldDecoder.subFieldDecoders = decoders + + return fieldDecoder, nil + } + + textDecoder, err := bind.CompileTextDecoder(field.Type) + if err != nil { + return nil, err + } + + fieldDecoder.dec = textDecoder + + return fieldDecoder, nil } func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tagScope string, tagContent string) (decoder, error) { diff --git a/binder_compile_test.go b/binder_compile_test.go new file mode 100644 index 0000000000..88d8752632 --- /dev/null +++ b/binder_compile_test.go @@ -0,0 +1,187 @@ +package fiber + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +type postStruct struct { + Title string `form:"title"` + Body string `form:"body"` +} + +type testStruct struct { + Name string `form:"name"` + Age int `form:"age"` + Post postStruct `form:"post"` +} + +// type testStruct2 struct { +// Name string `form:"name"` +// Age int `form:"age"` +// Post postStruct `form:"post"` +// Posts []postStruct `form:"posts"` +// } + +func checkSubFieldDecoder(t *testing.T, textDecoder *fieldTextDecoder, reqField string) { + t.Helper() + + for _, subFieldDecoder := range textDecoder.subFieldDecoders { + subFieldTextDecoder, ok := subFieldDecoder.(*fieldTextDecoder) + require.True(t, ok) + + fmt.Println(subFieldTextDecoder.reqField) + require.Contains(t, subFieldTextDecoder.reqField, reqField+".") + + if subFieldTextDecoder.dec == nil { + checkSubFieldDecoder(t, subFieldTextDecoder, subFieldTextDecoder.reqField) + } else { + require.NotNil(t, subFieldTextDecoder.dec) + } + require.NotNil(t, subFieldTextDecoder.get) + } +} + +func Test_compileTextBasedDecoder(t *testing.T) { + t.Parallel() + + _ = bindCompileOption{ + bodyDecoder: true, + } + + type simpleStruct struct { + Integer int `form:"integer"` + Float float64 `form:"float"` + Boolean bool `form:"boolean"` + String string `form:"string"` + EmbedStruct testStruct `form:"embedStruct"` + } + + testVar := reflect.TypeOf(&simpleStruct{}) + el := testVar.Elem() + + t.Run("int", func(t *testing.T) { + field, ok := el.FieldByName("Integer") + require.True(t, ok) + + decoder, err := compileTextBasedDecoder(field, 0, "form", "integer", bindCompileOption{ + bodyDecoder: true, + }) + require.NoError(t, err) + + fieldTextDecoder, ok := decoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "integer", fieldTextDecoder.reqField) + require.Equal(t, "form", fieldTextDecoder.tag) + require.NotNil(t, fieldTextDecoder.dec) + require.NotNil(t, fieldTextDecoder.get) + }) + + t.Run("float", func(t *testing.T) { + field, ok := el.FieldByName("Float") + require.True(t, ok) + + decoder, err := compileTextBasedDecoder(field, 0, "form", "float", bindCompileOption{ + bodyDecoder: true, + }) + require.NoError(t, err) + + fieldTextDecoder, ok := decoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "float", fieldTextDecoder.reqField) + require.Equal(t, "form", fieldTextDecoder.tag) + require.NotNil(t, fieldTextDecoder.dec) + require.NotNil(t, fieldTextDecoder.get) + }) + + t.Run("bool", func(t *testing.T) { + field, ok := el.FieldByName("Boolean") + require.True(t, ok) + + decoder, err := compileTextBasedDecoder(field, 0, "form", "boolean", bindCompileOption{ + bodyDecoder: true, + }) + require.NoError(t, err) + + fieldTextDecoder, ok := decoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "boolean", fieldTextDecoder.reqField) + require.Equal(t, "form", fieldTextDecoder.tag) + require.NotNil(t, fieldTextDecoder.dec) + require.NotNil(t, fieldTextDecoder.get) + }) + + t.Run("string", func(t *testing.T) { + field, ok := el.FieldByName("String") + require.True(t, ok) + + decoder, err := compileTextBasedDecoder(field, 0, "form", "string", bindCompileOption{ + bodyDecoder: true, + }) + require.NoError(t, err) + + fieldTextDecoder, ok := decoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "string", fieldTextDecoder.reqField) + require.Equal(t, "form", fieldTextDecoder.tag) + require.NotNil(t, fieldTextDecoder.dec) + require.NotNil(t, fieldTextDecoder.get) + }) + + t.Run("embedStruct", func(t *testing.T) { + field, ok := el.FieldByName("EmbedStruct") + require.True(t, ok) + + decoder, err := compileTextBasedDecoder(field, 0, "form", "embedStruct", bindCompileOption{ + bodyDecoder: true, + }) + require.NoError(t, err) + + textDecoder, ok := decoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "embedStruct", textDecoder.reqField) + require.Equal(t, "form", textDecoder.tag) + require.Nil(t, textDecoder.dec) + require.NotNil(t, textDecoder.get) + require.Len(t, textDecoder.subFieldDecoders, 3) + + checkSubFieldDecoder(t, textDecoder, textDecoder.reqField) + }) +} + +func Test_compileFieldDecoder(t *testing.T) { + t.Parallel() + + opt := bindCompileOption{ + bodyDecoder: true, + } + + testVar := reflect.TypeOf(&testStruct{}) + el := testVar.Elem() + + var decoders []decoder + + for i := 0; i < el.NumField(); i++ { + if !el.Field(i).IsExported() { + // ignore unexported field + continue + } + + dec, err := compileFieldDecoder(el.Field(i), i, opt, nil) + require.NoError(t, err) + + if dec != nil { + decoders = append(decoders, dec) + } + } + + require.Len(t, decoders, 3) +} diff --git a/internal/bind/compile.go b/internal/bind/compile.go index 252afc6e43..c802ae6778 100644 --- a/internal/bind/compile.go +++ b/internal/bind/compile.go @@ -14,7 +14,7 @@ var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem( func CompileTextDecoder(rt reflect.Type) (TextDecoder, error) { // encoding.TextUnmarshaler - if reflect.PtrTo(rt).Implements(textUnmarshalerType) { + if reflect.PointerTo(rt).Implements(textUnmarshalerType) { return &textUnmarshalEncoder{fieldType: rt}, nil } From f19d7472da87debe3e02cf77c2732e1ef7e714c7 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sat, 31 Aug 2024 21:38:23 +0300 Subject: [PATCH 28/31] binder_compile: support embedding slice structs and fix text unmarshaler --- bind.go | 37 +++++- binder_compile.go | 89 +++++++++---- binder_compile_test.go | 287 ++++++++++++++++++++++++++++++++++++----- binder_slice.go | 13 +- 4 files changed, 355 insertions(+), 71 deletions(-) diff --git a/bind.go b/bind.go index 28c871b106..11b7ad29a5 100644 --- a/bind.go +++ b/bind.go @@ -1,6 +1,7 @@ package fiber import ( + "encoding" "fmt" "reflect" @@ -15,6 +16,7 @@ type Binder interface { // it's created with field index type decoder interface { Decode(ctx Ctx, reqValue reflect.Value) error + Kind() string } type fieldCtxDecoder struct { @@ -35,26 +37,51 @@ func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } +func (d *fieldCtxDecoder) Kind() string { + return "ctx" +} + type fieldTextDecoder struct { - index int + fieldIndex int fieldName string tag string // query,param,header,respHeader ... - reqField string + reqKey string dec bind.TextDecoder get func(c Ctx, key string, defaultValue ...string) string subFieldDecoders []decoder + isTextMarshaler bool } func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { - text := d.get(ctx, d.reqField) + text := d.get(ctx, d.reqKey) if text == "" { return nil } - err := d.dec.UnmarshalString(text, reqValue.Field(d.index)) + field := reqValue.Field(d.fieldIndex) + + if d.isTextMarshaler { + unmarshaler, ok := field.Addr().Interface().(encoding.TextUnmarshaler) + if !ok { + return fmt.Errorf("field %s does not implement encoding.TextUnmarshaler", d.fieldName) + } + + err := unmarshaler.UnmarshalText([]byte(text)) + if err != nil { + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err) + } + + return nil + } + + err := d.dec.UnmarshalString(text, field) if err != nil { - return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqField, err) + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err) } return nil } + +func (d *fieldTextDecoder) Kind() string { + return "text" +} diff --git a/binder_compile.go b/binder_compile.go index c2fe710c0e..d6a72ce61b 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -70,6 +70,7 @@ func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { type parentDecoder struct { tagScope string tagContent string + isSlice bool } func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOption, parent *parentDecoder) (decoder, error) { @@ -100,20 +101,22 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp var tagContent string if parent != nil { tagScope = parent.tagScope - tagContent = parent.tagContent + "." + field.Tag.Get(tagScope) + if parent.isSlice { + tagContent = parent.tagContent + ".NUM." + field.Tag.Get(tagScope) + } else { + tagContent = parent.tagContent + "." + field.Tag.Get(tagScope) + } } else { tagContent = field.Tag.Get(tagScope) } - if reflect.PointerTo(field.Type).Implements(textUnmarshalerType) { - return compileTextBasedDecoder(field, index, tagScope, tagContent, opt) - } - if field.Type.Kind() == reflect.Slice { - return compileSliceFieldTextBasedDecoder(field, index, tagScope, tagContent) + return compileSliceFieldTextBasedDecoder(field, index, tagScope, tagContent, opt) } - return compileTextBasedDecoder(field, index, tagScope, tagContent, opt) + isTextMarshaler := reflect.PointerTo(field.Type).Implements(textUnmarshalerType) + + return compileTextBasedDecoder(field, index, tagScope, tagContent, opt, isTextMarshaler) } func formGetter(ctx Ctx, key string, defaultValue ...string) string { @@ -134,7 +137,7 @@ func multipartGetter(ctx Ctx, key string, defaultValue ...string) string { return v[0] } -func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string, opt bindCompileOption) (decoder, error) { +func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tagContent string, opt bindCompileOption, isTextMarshaler ...bool) (decoder, error) { var get func(ctx Ctx, key string, defaultValue ...string) string switch tagScope { case bindTagQuery: @@ -156,11 +159,16 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag } fieldDecoder := &fieldTextDecoder{ - index: index, - fieldName: field.Name, - tag: tagScope, - reqField: tagContent, - get: get, + fieldIndex: index, + fieldName: field.Name, + tag: tagScope, + reqKey: tagContent, + get: get, + } + + // Check if the field implements encoding.TextUnmarshaler + if len(isTextMarshaler) > 0 && isTextMarshaler[0] { + fieldDecoder.isTextMarshaler = true } // Support simple embeded structs @@ -195,16 +203,12 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return 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, opt bindCompileOption) (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)) @@ -229,14 +233,43 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag 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 + sliceDecoder := &fieldSliceDecoder{ + fieldIndex: index, + eqBytes: eqBytes, + fieldName: field.Name, + visitAll: visitAll, + reqKey: []byte(tagContent), + fieldType: field.Type, + elementType: et, + } + + // support struct slices + if et.Kind() == reflect.Struct { + var decoders []decoder + for i := 0; i < et.NumField(); i++ { + if !et.Field(i).IsExported() { + // ignore unexported field + continue + } + + dec, err := compileFieldDecoder(et.Field(i), i, opt, &parentDecoder{tagScope: tagScope, tagContent: tagContent, isSlice: true}) + if err != nil { + return nil, err + } + + decoders = append(decoders, dec) + } + sliceDecoder.subFieldDecoders = decoders + + return sliceDecoder, nil + } + + elementUnmarshaler, err := bind.CompileTextDecoder(et) + if err != nil { + return nil, fmt.Errorf("failed to build slice binder: %w", err) + } + + sliceDecoder.elementDecoder = elementUnmarshaler + + return sliceDecoder, nil } diff --git a/binder_compile_test.go b/binder_compile_test.go index 88d8752632..7c545ae18f 100644 --- a/binder_compile_test.go +++ b/binder_compile_test.go @@ -9,8 +9,21 @@ import ( ) type postStruct struct { - Title string `form:"title"` - Body string `form:"body"` + Title string `form:"title"` + Body string `form:"body"` + Test postProperties `form:"test"` +} + +type postStruct2 struct { + Title string `form:"title"` + Body string `form:"body"` + Test postProperties `form:"test"` + Tests []postProperties `form:"tests"` +} + +type postProperties struct { + Desc string `form:"desc"` + Likes int `form:"likes"` } type testStruct struct { @@ -19,30 +32,11 @@ type testStruct struct { Post postStruct `form:"post"` } -// type testStruct2 struct { -// Name string `form:"name"` -// Age int `form:"age"` -// Post postStruct `form:"post"` -// Posts []postStruct `form:"posts"` -// } - -func checkSubFieldDecoder(t *testing.T, textDecoder *fieldTextDecoder, reqField string) { - t.Helper() - - for _, subFieldDecoder := range textDecoder.subFieldDecoders { - subFieldTextDecoder, ok := subFieldDecoder.(*fieldTextDecoder) - require.True(t, ok) - - fmt.Println(subFieldTextDecoder.reqField) - require.Contains(t, subFieldTextDecoder.reqField, reqField+".") - - if subFieldTextDecoder.dec == nil { - checkSubFieldDecoder(t, subFieldTextDecoder, subFieldTextDecoder.reqField) - } else { - require.NotNil(t, subFieldTextDecoder.dec) - } - require.NotNil(t, subFieldTextDecoder.get) - } +type testStruct2 struct { + Name string `form:"name"` + Age int `form:"age"` + Post postStruct2 `form:"post"` + Posts []postStruct2 `form:"posts"` } func Test_compileTextBasedDecoder(t *testing.T) { @@ -64,6 +58,8 @@ func Test_compileTextBasedDecoder(t *testing.T) { el := testVar.Elem() t.Run("int", func(t *testing.T) { + t.Parallel() + field, ok := el.FieldByName("Integer") require.True(t, ok) @@ -75,13 +71,15 @@ func Test_compileTextBasedDecoder(t *testing.T) { fieldTextDecoder, ok := decoder.(*fieldTextDecoder) require.True(t, ok) - require.Equal(t, "integer", fieldTextDecoder.reqField) + require.Equal(t, "integer", fieldTextDecoder.reqKey) require.Equal(t, "form", fieldTextDecoder.tag) require.NotNil(t, fieldTextDecoder.dec) require.NotNil(t, fieldTextDecoder.get) }) t.Run("float", func(t *testing.T) { + t.Parallel() + field, ok := el.FieldByName("Float") require.True(t, ok) @@ -93,13 +91,15 @@ func Test_compileTextBasedDecoder(t *testing.T) { fieldTextDecoder, ok := decoder.(*fieldTextDecoder) require.True(t, ok) - require.Equal(t, "float", fieldTextDecoder.reqField) + require.Equal(t, "float", fieldTextDecoder.reqKey) require.Equal(t, "form", fieldTextDecoder.tag) require.NotNil(t, fieldTextDecoder.dec) require.NotNil(t, fieldTextDecoder.get) }) t.Run("bool", func(t *testing.T) { + t.Parallel() + field, ok := el.FieldByName("Boolean") require.True(t, ok) @@ -111,13 +111,15 @@ func Test_compileTextBasedDecoder(t *testing.T) { fieldTextDecoder, ok := decoder.(*fieldTextDecoder) require.True(t, ok) - require.Equal(t, "boolean", fieldTextDecoder.reqField) + require.Equal(t, "boolean", fieldTextDecoder.reqKey) require.Equal(t, "form", fieldTextDecoder.tag) require.NotNil(t, fieldTextDecoder.dec) require.NotNil(t, fieldTextDecoder.get) }) t.Run("string", func(t *testing.T) { + t.Parallel() + field, ok := el.FieldByName("String") require.True(t, ok) @@ -129,13 +131,15 @@ func Test_compileTextBasedDecoder(t *testing.T) { fieldTextDecoder, ok := decoder.(*fieldTextDecoder) require.True(t, ok) - require.Equal(t, "string", fieldTextDecoder.reqField) + require.Equal(t, "string", fieldTextDecoder.reqKey) require.Equal(t, "form", fieldTextDecoder.tag) require.NotNil(t, fieldTextDecoder.dec) require.NotNil(t, fieldTextDecoder.get) }) t.Run("embedStruct", func(t *testing.T) { + t.Parallel() + field, ok := el.FieldByName("EmbedStruct") require.True(t, ok) @@ -147,13 +151,41 @@ func Test_compileTextBasedDecoder(t *testing.T) { textDecoder, ok := decoder.(*fieldTextDecoder) require.True(t, ok) - require.Equal(t, "embedStruct", textDecoder.reqField) + require.Equal(t, "embedStruct", textDecoder.reqKey) require.Equal(t, "form", textDecoder.tag) require.Nil(t, textDecoder.dec) require.NotNil(t, textDecoder.get) require.Len(t, textDecoder.subFieldDecoders, 3) - checkSubFieldDecoder(t, textDecoder, textDecoder.reqField) + checkSubFieldDecoder(t, textDecoder, textDecoder.reqKey) + }) +} + +func Test_compileSliceFieldTextBasedDecoder(t *testing.T) { + t.Parallel() + + opt := bindCompileOption{ + bodyDecoder: true, + } + + testVar := reflect.TypeOf(&testStruct2{}) + el := testVar.Elem() + + t.Run("posts", func(t *testing.T) { + field, ok := el.FieldByName("Posts") + require.True(t, ok) + + decoder, err := compileSliceFieldTextBasedDecoder(field, 0, "form", "posts", opt) + require.NoError(t, err) + + fieldSliceDecoder, ok := decoder.(*fieldSliceDecoder) + require.True(t, ok) + + require.Equal(t, "Posts", fieldSliceDecoder.fieldName) + require.Equal(t, "posts", string(fieldSliceDecoder.reqKey)) + require.NotNil(t, fieldSliceDecoder.visitAll) + require.Len(t, fieldSliceDecoder.subFieldDecoders, 4) + checkSubFieldDecoderSlice(t, fieldSliceDecoder, string(fieldSliceDecoder.reqKey)) }) } @@ -164,7 +196,7 @@ func Test_compileFieldDecoder(t *testing.T) { bodyDecoder: true, } - testVar := reflect.TypeOf(&testStruct{}) + testVar := reflect.TypeOf(&testStruct2{}) el := testVar.Elem() var decoders []decoder @@ -183,5 +215,192 @@ func Test_compileFieldDecoder(t *testing.T) { } } - require.Len(t, decoders, 3) + require.Len(t, decoders, 4) + + decoder0, ok := decoders[0].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "name", decoder0.reqKey) + require.Equal(t, "form", decoder0.tag) + require.NotNil(t, decoder0.dec) + require.NotNil(t, decoder0.get) + require.Len(t, decoder0.subFieldDecoders, 0) + + decoder1, ok := decoders[1].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "age", decoder1.reqKey) + require.Equal(t, "form", decoder1.tag) + require.NotNil(t, decoder1.dec) + require.NotNil(t, decoder1.get) + require.Len(t, decoder1.subFieldDecoders, 0) + + decoder2, ok := decoders[2].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "post", decoder2.reqKey) + require.Equal(t, "form", decoder2.tag) + require.Nil(t, decoder2.dec) + require.NotNil(t, decoder2.get) + require.Len(t, decoder2.subFieldDecoders, 4) + + decoder20 := decoder2.subFieldDecoders[0].(*fieldTextDecoder) + require.Equal(t, "post.title", decoder20.reqKey) + require.Equal(t, "form", decoder20.tag) + require.NotNil(t, decoder20.dec) + require.NotNil(t, decoder20.get) + require.Len(t, decoder20.subFieldDecoders, 0) + + decoder21 := decoder2.subFieldDecoders[1].(*fieldTextDecoder) + require.Equal(t, "post.body", decoder21.reqKey) + require.Equal(t, "form", decoder21.tag) + require.NotNil(t, decoder21.dec) + require.NotNil(t, decoder21.get) + require.Len(t, decoder21.subFieldDecoders, 0) + + decoder22 := decoder2.subFieldDecoders[2].(*fieldTextDecoder) + require.Equal(t, "post.test", decoder22.reqKey) + require.Equal(t, "form", decoder22.tag) + require.Nil(t, decoder22.dec) + require.NotNil(t, decoder22.get) + require.Len(t, decoder22.subFieldDecoders, 2) + + decoder220 := decoder22.subFieldDecoders[0].(*fieldTextDecoder) + require.Equal(t, "post.test.desc", decoder220.reqKey) + require.Equal(t, "form", decoder220.tag) + require.NotNil(t, decoder220.dec) + require.NotNil(t, decoder220.get) + require.Len(t, decoder220.subFieldDecoders, 0) + + decoder221 := decoder22.subFieldDecoders[1].(*fieldTextDecoder) + require.Equal(t, "post.test.likes", decoder221.reqKey) + require.Equal(t, "form", decoder221.tag) + require.NotNil(t, decoder221.dec) + require.NotNil(t, decoder221.get) + require.Len(t, decoder221.subFieldDecoders, 0) + + decoder3, ok := decoders[3].(*fieldSliceDecoder) + require.True(t, ok) + + require.Equal(t, "Posts", decoder3.fieldName) + require.Equal(t, "posts", string(decoder3.reqKey)) + require.NotNil(t, decoder3.visitAll) + require.Len(t, decoder3.subFieldDecoders, 4) + + checkSubFieldDecoderSlice(t, decoder3, string(decoder3.reqKey)) + + decoder30, ok := decoder3.subFieldDecoders[0].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "posts.NUM.title", decoder30.reqKey) + require.Equal(t, "form", decoder30.tag) + require.NotNil(t, decoder30.dec) + require.NotNil(t, decoder30.get) + require.Len(t, decoder30.subFieldDecoders, 0) + + decoder31, ok := decoder3.subFieldDecoders[1].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "posts.NUM.body", decoder31.reqKey) + require.Equal(t, "form", decoder31.tag) + require.NotNil(t, decoder31.dec) + require.NotNil(t, decoder31.get) + require.Len(t, decoder31.subFieldDecoders, 0) + + decoder32, ok := decoder3.subFieldDecoders[2].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "posts.NUM.test", decoder32.reqKey) + require.Equal(t, "form", decoder32.tag) + require.Nil(t, decoder32.dec) + require.NotNil(t, decoder32.get) + require.Len(t, decoder32.subFieldDecoders, 2) + + decoder320 := decoder32.subFieldDecoders[0].(*fieldTextDecoder) + require.Equal(t, "posts.NUM.test.desc", decoder320.reqKey) + require.Equal(t, "form", decoder320.tag) + require.NotNil(t, decoder320.dec) + require.NotNil(t, decoder320.get) + require.Len(t, decoder320.subFieldDecoders, 0) + + decoder321 := decoder32.subFieldDecoders[1].(*fieldTextDecoder) + require.Equal(t, "posts.NUM.test.likes", decoder321.reqKey) + require.Equal(t, "form", decoder321.tag) + require.NotNil(t, decoder321.dec) + require.NotNil(t, decoder321.get) + require.Len(t, decoder321.subFieldDecoders, 0) + + decoder33, ok := decoder3.subFieldDecoders[3].(*fieldSliceDecoder) + require.True(t, ok) + + require.Equal(t, "Tests", decoder33.fieldName) + require.Equal(t, "posts.NUM.tests", string(decoder33.reqKey)) + require.NotNil(t, decoder33.visitAll) + require.Len(t, decoder33.subFieldDecoders, 2) + + decoder330, ok := decoder33.subFieldDecoders[0].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "posts.NUM.tests.NUM.desc", decoder330.reqKey) + require.Equal(t, "form", decoder330.tag) + require.NotNil(t, decoder330.dec) + require.NotNil(t, decoder330.get) + require.Len(t, decoder330.subFieldDecoders, 0) + + decoder331, ok := decoder33.subFieldDecoders[1].(*fieldTextDecoder) + require.True(t, ok) + + require.Equal(t, "posts.NUM.tests.NUM.likes", decoder331.reqKey) + require.Equal(t, "form", decoder331.tag) + require.NotNil(t, decoder331.dec) + require.NotNil(t, decoder331.get) +} + +func checkSubFieldDecoder(t *testing.T, textDecoder *fieldTextDecoder, reqKey string) { + t.Helper() + + for _, subFieldDecoder := range textDecoder.subFieldDecoders { + fmt.Print(subFieldDecoder.Kind()) + subFieldTextDecoder, ok := subFieldDecoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Contains(t, subFieldTextDecoder.reqKey, reqKey+".") + + if subFieldTextDecoder.dec == nil { + checkSubFieldDecoder(t, subFieldTextDecoder, subFieldTextDecoder.reqKey) + } else { + require.NotNil(t, subFieldTextDecoder.dec) + } + require.NotNil(t, subFieldTextDecoder.get) + } +} + +func checkSubFieldDecoderSlice(t *testing.T, sliceDecoder *fieldSliceDecoder, reqKey string) { + t.Helper() + + for _, subFieldDecoder := range sliceDecoder.subFieldDecoders { + if subFieldDecoder.Kind() == "text" { + subFieldTextDecoder, ok := subFieldDecoder.(*fieldTextDecoder) + require.True(t, ok) + + require.Contains(t, subFieldTextDecoder.reqKey, reqKey+".") + + if subFieldTextDecoder.dec == nil { + checkSubFieldDecoder(t, subFieldTextDecoder, subFieldTextDecoder.reqKey) + } else { + require.NotNil(t, subFieldTextDecoder.dec) + } + require.NotNil(t, subFieldTextDecoder.get) + } else { + subFieldSliceDecoder, ok := subFieldDecoder.(*fieldSliceDecoder) + require.True(t, ok) + + require.Contains(t, string(subFieldSliceDecoder.reqKey), reqKey+".") + + if subFieldSliceDecoder.elementDecoder == nil { + checkSubFieldDecoderSlice(t, subFieldSliceDecoder, string(subFieldSliceDecoder.reqKey)) + } + require.NotNil(t, subFieldSliceDecoder.visitAll) + } + } } diff --git a/binder_slice.go b/binder_slice.go index e3eb828c0e..b7d03e8aba 100644 --- a/binder_slice.go +++ b/binder_slice.go @@ -15,10 +15,11 @@ type fieldSliceDecoder struct { 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)) + eqBytes func([]byte, []byte) bool + elementType reflect.Type + elementDecoder bind.TextDecoder + visitAll func(Ctx, func(key []byte, value []byte)) + subFieldDecoders []decoder } func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { @@ -59,6 +60,10 @@ func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } +func (d *fieldSliceDecoder) Kind() string { + return "slice" +} + func visitQuery(ctx Ctx, f func(key []byte, value []byte)) { ctx.Context().QueryArgs().VisitAll(f) } From 1f8459d11f0b6da4b08ad3b02c26cb6edfa71dfb Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sun, 1 Sep 2024 16:24:10 +0300 Subject: [PATCH 29/31] bind: add support for simple nested structs --- bind.go | 15 +++++++++++++-- bind_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ binder_compile.go | 9 +++++++-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/bind.go b/bind.go index 11b7ad29a5..94e62fbe55 100644 --- a/bind.go +++ b/bind.go @@ -53,13 +53,24 @@ type fieldTextDecoder struct { } func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + field := reqValue.Field(d.fieldIndex) + + // Support for sub fields + if len(d.subFieldDecoders) > 0 { + for _, subFieldDecoder := range d.subFieldDecoders { + err := subFieldDecoder.Decode(ctx, field) + if err != nil { + return err + } + } + return nil + } + text := d.get(ctx, d.reqKey) if text == "" { return nil } - field := reqValue.Field(d.fieldIndex) - if d.isTextMarshaler { unmarshaler, ok := field.Addr().Interface().(encoding.TextUnmarshaler) if !ok { diff --git a/bind_test.go b/bind_test.go index 3dd961064a..eea83efc39 100644 --- a/bind_test.go +++ b/bind_test.go @@ -121,7 +121,48 @@ func Test_Bind_BasicType(t *testing.T) { U: []uint{99}, S: []string{"john"}, }, q2) +} + +func Test_Bind_NestedStruct(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type AddressPayload struct { + Country string `query:"country"` + Country2 string `respHeader:"country"` + } + + type Address struct { + City string `query:"city"` + Zip int `query:"zip"` + Payload AddressPayload `query:"payload"` + } + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + Address Address `query:"address"` + } + + c.Request().URI().SetQueryString("name=john&age=30&address.city=NY&address.zip=10001&address.payload.country=US&address.payload.country2=US") + + var u User + require.NoError(t, c.Bind().Req(&u).Err()) + + require.Equal(t, User{ + Name: "john", + Age: 30, + Address: Address{ + City: "NY", + Zip: 10001, + Payload: AddressPayload{ + Country: "US", + Country2: "", + }, + }, + }, u) } // go test -run Test_Bind_Query -v diff --git a/binder_compile.go b/binder_compile.go index d6a72ce61b..43fea4ff8b 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -100,7 +100,10 @@ func compileFieldDecoder(field reflect.StructField, index int, opt bindCompileOp // If parent tag scope is present, just override it and append the parent tag content var tagContent string if parent != nil { - tagScope = parent.tagScope + if tagScope != parent.tagScope { + return nil, nil + } + if parent.isSlice { tagContent = parent.tagContent + ".NUM." + field.Tag.Get(tagScope) } else { @@ -185,7 +188,9 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag return nil, err } - decoders = append(decoders, dec) + if dec != nil { + decoders = append(decoders, dec) + } } fieldDecoder.subFieldDecoders = decoders From 9fbf8308996a649a4708456b83cf998b289903d0 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Sun, 1 Sep 2024 16:45:19 +0300 Subject: [PATCH 30/31] bind: add benchmark case for simple nested structs --- bind_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/bind_test.go b/bind_test.go index eea83efc39..26a9b0bfec 100644 --- a/bind_test.go +++ b/bind_test.go @@ -447,6 +447,44 @@ func Benchmark_Bind_by_hand(b *testing.B) { } } +func Benchmark_Bind_NestedStruct(b *testing.B) { + type tokenStruct struct { + Token string `header:"x-auth"` + } + + type reqStruct struct { + ID string `params:"id"` + + I int `query:"I"` + J int `query:"j"` + K int `query:"k"` + + Token tokenStruct `header:"token"` + } + + app := New() + + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} + + var u = fasthttp.URI{} + u.SetQueryString("j=1&I=123&k=-1") + ctx.Request().SetURI(&u) + + ctx.Request().Header.Set("token.x-auth", "bearer tt") + + for i := 0; i < b.N; i++ { + var req reqStruct + + err := ctx.Bind().Req(&req).Err() + if err != nil { + b.Error(err) + b.FailNow() + } + } +} + func Benchmark_Bind(b *testing.B) { ctx := getBenchCtx() for i := 0; i < b.N; i++ { From b851f71a0c7b5ca9c83a6c46e55e71b9ef4e8939 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Tue, 3 Sep 2024 17:55:42 +0300 Subject: [PATCH 31/31] WIP: binder: add slice nested binding support1 --- bind.go | 1 + bind_test.go | 140 ++++++++++++++++++++++++++++++++++++++++++++++ binder_compile.go | 40 +++++++++++++ binder_slice.go | 128 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 309 insertions(+) diff --git a/bind.go b/bind.go index 94e62fbe55..8a77cf416b 100644 --- a/bind.go +++ b/bind.go @@ -50,6 +50,7 @@ type fieldTextDecoder struct { get func(c Ctx, key string, defaultValue ...string) string subFieldDecoders []decoder isTextMarshaler bool + fragments []requestKeyFragment } func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { diff --git a/bind_test.go b/bind_test.go index 26a9b0bfec..1e582f27b7 100644 --- a/bind_test.go +++ b/bind_test.go @@ -165,6 +165,146 @@ func Test_Bind_NestedStruct(t *testing.T) { }, u) } +func Test_Bind_Slice_NestedStruct(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + 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") + + var cq CollectionQuery + + require.NoError(t, c.Bind().Req(&cq).Err()) + + require.Equal(t, CollectionQuery{ + Data: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, cq) +} + +func Benchmark_Bind_Slice_NestedStruct(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + 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") + + var cq CollectionQuery + + for i := 0; i < b.N; i++ { + _ = c.Bind().Req(&cq) + } + + require.NoError(b, c.Bind().Req(&cq).Err()) + + require.Equal(b, CollectionQuery{ + Data: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, cq) +} + +func Test_Bind_Slice_NestedStruct2(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type Family struct { + Name string `query:"name"` + Members []Person `query:"members"` + } + + type CollectionQuery struct { + Data []Family `query:"data"` + } + + c.Request().URI().SetQueryString("data.0.name=doe&data.0.members.0.name=john&data.0.members.0.age=10&data.0.members.1.name=doe&data.0.members.1.age=12&data.0.members.2.name=doe&data.0.members.2.age=12") + + var cq CollectionQuery + + require.NoError(t, c.Bind().Req(&cq).Err()) + + require.Equal(t, CollectionQuery{ + Data: []Family{ + { + Name: "doe", + Members: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, + }, + }, cq) +} + +func Test_Bind_Slice_NestedStruct3(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + type Test2 struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + Test Test2 `query:"test"` + } + + type CollectionQuery struct { + Data []Person `query:"data"` + } + + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.0.test.name=doe&data.0.test.age=12") + + var cq CollectionQuery + + require.NoError(t, c.Bind().Req(&cq).Err()) + + require.Equal(t, CollectionQuery{ + Data: []Person{ + { + Name: "john", + Age: 10, + Test: Test2{ + Name: "doe", + Age: 12, + }, + }, + }, + }, cq) +} + // go test -run Test_Bind_Query -v func Test_Bind_Query(t *testing.T) { t.Parallel() diff --git a/binder_compile.go b/binder_compile.go index 43fea4ff8b..0067db14c9 100644 --- a/binder_compile.go +++ b/binder_compile.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "github.com/gofiber/fiber/v3/internal/bind" "github.com/gofiber/utils/v2" @@ -31,6 +32,13 @@ type bindCompileOption struct { reqDecoder bool // to parse header/cookie/param/query/header/respHeader } +type requestKeyFragment struct { + key string + num int + index int + isNum bool +} + func compileReqParser(rt reflect.Type, opt bindCompileOption) (Decoder, error) { var decoders []decoder @@ -169,6 +177,22 @@ func compileTextBasedDecoder(field reflect.StructField, index int, tagScope, tag get: get, } + // append fragments + if strings.Contains(tagContent, ".") { + pieces := strings.Split(tagContent, ".") + frags := make([]requestKeyFragment, 0, len(pieces)) + + for _, piece := range pieces { + if piece == "NUM" { + frags = append(frags, requestKeyFragment{num: -1, isNum: true}) + continue + } + + frags = append(frags, requestKeyFragment{key: piece}) + } + fieldDecoder.fragments = frags + } + // Check if the field implements encoding.TextUnmarshaler if len(isTextMarshaler) > 0 && isTextMarshaler[0] { fieldDecoder.isTextMarshaler = true @@ -248,6 +272,22 @@ func compileSliceFieldTextBasedDecoder(field reflect.StructField, index int, tag elementType: et, } + // append fragments + if strings.Contains(tagContent, ".") { + pieces := strings.Split(tagContent, ".") + frags := make([]requestKeyFragment, 0, len(pieces)) + + for _, piece := range pieces { + if piece == "NUM" { + frags = append(frags, requestKeyFragment{num: -1, isNum: true}) + continue + } + + frags = append(frags, requestKeyFragment{key: piece}) + } + sliceDecoder.fragments = frags + } + // support struct slices if et.Kind() == reflect.Struct { var decoders []decoder diff --git a/binder_slice.go b/binder_slice.go index b7d03e8aba..6c1f258dd2 100644 --- a/binder_slice.go +++ b/binder_slice.go @@ -1,7 +1,10 @@ package fiber import ( + "bytes" "reflect" + "strconv" + "strings" "github.com/gofiber/fiber/v3/internal/bind" "github.com/gofiber/utils/v2" @@ -20,9 +23,20 @@ type fieldSliceDecoder struct { elementDecoder bind.TextDecoder visitAll func(Ctx, func(key []byte, value []byte)) subFieldDecoders []decoder + fragments []requestKeyFragment } func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + if len(d.subFieldDecoders) > 0 { + rv, err := d.decodeSubFields(ctx, reqValue) + if err != nil { + return err + } + + reqValue.Field(d.fieldIndex).Set(rv) + return nil + } + count := 0 d.visitAll(ctx, func(key, value []byte) { if d.eqBytes(key, d.reqKey) { @@ -60,6 +74,120 @@ func (d *fieldSliceDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { return nil } +func (d *fieldSliceDecoder) decodeSubFields(ctx Ctx, reqValue reflect.Value) (reflect.Value, error) { + rv := reflect.New(d.fieldType).Elem() + + // reqValue => ana struct + for _, subFieldDecoder := range d.subFieldDecoders { + if subFieldDecoder.Kind() == "text" { + textDec, ok := subFieldDecoder.(*fieldTextDecoder) + if !ok { + continue + } + + test := make(map[string]int) + + count := 0 + maxIndex := 0 + d.visitAll(ctx, func(key, value []byte) { + var num int + if !bytes.Contains(key, []byte(".")) { + return + } + + frag := prepareFragments(utils.UnsafeString(key)) + + if textDec.subFieldDecoders == nil && len(frag) != len(textDec.fragments) { + return + } + + if textDec.subFieldDecoders != nil && len(frag) > len(textDec.fragments) { + + } + + for i, f := range frag { + if textDec.fragments[i].isNum && f.isNum { + if f.num > maxIndex { + maxIndex = f.num + } + num = f.num + } else if textDec.fragments[i].key != f.key { + return + } + } + count++ + test[utils.UnsafeString(key)] = num + }) + + if count == 0 { + reqValue.Field(d.fieldIndex).Set(reflect.MakeSlice(d.fieldType, 0, 0)) + continue + } + + if rv.Len() < maxIndex+1 { + rv = reflect.MakeSlice(d.fieldType, maxIndex+1, maxIndex+1) + } + + d.visitAll(ctx, func(key, value []byte) { + if index, ok := test[utils.UnsafeString(key)]; ok { + textDec.dec.UnmarshalString(utils.UnsafeString(value), rv.Index(index).Field(textDec.fieldIndex)) + } + }) + } else { + sliceDec, ok := subFieldDecoder.(*fieldSliceDecoder) + if !ok { + continue + } + + var count int + var maxIndex int + + d.visitAll(ctx, func(key, value []byte) { + if !bytes.Contains(key, []byte(".")) { + return + } + + frag := prepareFragments(utils.UnsafeString(key)) + + if len(frag) < len(sliceDec.fragments)+1 { + return + } + for i := 0; i < len(sliceDec.fragments)+1; i++ { + if i == len(sliceDec.fragments) && frag[i].isNum { + count++ + if frag[i].num > maxIndex { + maxIndex = frag[i].num + } + continue + } + + if frag[i].key != sliceDec.fragments[i].key && !frag[i].isNum { + return + } + } + }) + //sliceDec.decodeSubFields(ctx, rv) + } + } + + return rv, nil +} + +func prepareFragments(key string) []requestKeyFragment { + split := strings.Split(key, ".") + fragments := make([]requestKeyFragment, 0, len(split)) + for _, fragment := range split { + num, err := strconv.Atoi(fragment) + fragments = append(fragments, requestKeyFragment{ + key: fragment, + num: num, + isNum: err == nil, + }) + } + + return fragments +} + func (d *fieldSliceDecoder) Kind() string { return "slice" }