diff --git a/app.go b/app.go index e0240d3c16..f0cdabc744 100644 --- a/app.go +++ b/app.go @@ -102,14 +102,18 @@ type App struct { newCtxFunc func(app *App) CustomCtx // TLS handler tlsHandler *TLSHandler + // bind decoder cache + bindDecoderCache sync.Map + // form decoder cache + formDecoderCache sync.Map + // multipart decoder cache + multipartDecoderCache sync.Map // Mount fields mountFields *mountFields // Route stack divided by HTTP methods stack [][]*Route // Route stack divided by HTTP methods and route prefixes treeStack []map[string][]*Route - // custom binders - customBinders []CustomBinder // customConstraints is a list of external constraints customConstraints []CustomConstraint // sendfiles stores configurations for handling ctx.SendFile operations @@ -325,6 +329,23 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa // Default: xml.Marshal XMLEncoder utils.XMLMarshal `json:"-"` + // XMLDecoder set by an external client of Fiber it will use the provided implementation of a + // XMLUnmarshal + // + // Allowing for flexibility in using another XML library for 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. + // + // Default: NetworkTCP4 + Network string + // If you find yourself behind some sort of proxy, like a load balancer, // then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header. // For example, the Host HTTP header is usually used to return the requested host. @@ -366,12 +387,6 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa // 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 @@ -439,12 +454,11 @@ func New(config ...Config) *App { // Create a new app app := &App{ // Create config - config: Config{}, - getBytes: utils.UnsafeBytes, - getString: utils.UnsafeString, - latestRoute: &Route{}, - customBinders: []CustomBinder{}, - sendfiles: []*sendFileStore{}, + config: Config{}, + getBytes: utils.UnsafeBytes, + getString: utils.UnsafeString, + latestRoute: &Route{}, + sendfiles: []*sendFileStore{}, } // Create Ctx pool @@ -503,9 +517,15 @@ 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 len(app.config.RequestMethods) == 0 { app.config.RequestMethods = DefaultMethods } @@ -554,12 +574,6 @@ func (app *App) RegisterCustomConstraint(constraint CustomConstraint) { app.customConstraints = append(app.customConstraints, constraint) } -// RegisterCustomBinder Allows to 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) -} - // SetTLSHandler Can be used to set ClientHelloInfo when using TLS with Listener. func (app *App) SetTLSHandler(tlsHandler *TLSHandler) { // Attach the tlsHandler to the config diff --git a/bind.go b/bind.go index f7e449f6e3..8a77cf416b 100644 --- a/bind.go +++ b/bind.go @@ -1,193 +1,99 @@ package fiber import ( - "github.com/gofiber/fiber/v3/binder" - "github.com/gofiber/utils/v2" -) - -// CustomBinder An interface to register custom binders. -type CustomBinder interface { - Name() string - MIMETypes() []string - Parse(c Ctx, out any) error -} + "encoding" + "fmt" + "reflect" -// StructValidator is an interface to register custom struct validator for binding. -type StructValidator interface { - Validate(out any) error -} + "github.com/gofiber/fiber/v3/internal/bind" +) -// Bind struct -type Bind struct { - ctx Ctx - should bool +type Binder interface { + UnmarshalFiberCtx(ctx Ctx) error } -// Should 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 +// decoder should set a field on reqValue +// it's created with field index +type decoder interface { + Decode(ctx Ctx, reqValue reflect.Value) error + Kind() string } -// Must 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 +type fieldCtxDecoder struct { + index int + fieldName string + fieldType reflect.Type } -// 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 -} +func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + v := reflect.New(d.fieldType) + unmarshaler := v.Interface().(Binder) -// Struct validation. -func (b *Bind) validateStruct(out any) error { - validator := b.ctx.App().config.StructValidator - if validator != nil { - return validator.Validate(out) + if err := unmarshaler.UnmarshalFiberCtx(ctx); err != nil { + return err } + reqValue.Field(d.index).Set(v.Elem()) return nil } -// Custom 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 _, customBinder := range binders { - if customBinder.Name() == name { - return b.returnErr(customBinder.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) +func (d *fieldCtxDecoder) Kind() string { + return "ctx" } -// 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) +type fieldTextDecoder struct { + fieldIndex int + fieldName string + tag string // query,param,header,respHeader ... + reqKey string + dec bind.TextDecoder + get func(c Ctx, key string, defaultValue ...string) string + subFieldDecoders []decoder + isTextMarshaler bool + fragments []requestKeyFragment } -// 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 - } +func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error { + field := reqValue.Field(d.fieldIndex) - return b.validateStruct(out) -} - -// Query binds the query string into the struct, map[string]string and map[string][]string. -func (b *Bind) Query(out any) error { - if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.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 + // 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 } - 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 + text := d.get(ctx, d.reqKey) + if text == "" { + return nil } - 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 - } + if d.isTextMarshaler { + unmarshaler, ok := field.Addr().Interface().(encoding.TextUnmarshaler) + if !ok { + return fmt.Errorf("field %s does not implement encoding.TextUnmarshaler", d.fieldName) + } - return b.validateStruct(out) -} + err := unmarshaler.UnmarshalText([]byte(text)) + if err != nil { + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err) + } -// 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 nil } - 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 + err := d.dec.UnmarshalString(text, field) + if err != nil { + return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err) } - return b.validateStruct(out) + return nil } -// 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)) - - // Check custom binders - binders := b.ctx.App().customBinders - for _, customBinder := range binders { - for _, mime := range customBinder.MIMETypes() { - if mime == ctype { - return b.returnErr(customBinder.Parse(b.ctx, out)) - } - } - } - - // 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) - } - - // No suitable content type found - return ErrUnprocessableEntity +func (d *fieldTextDecoder) Kind() string { + return "text" } diff --git a/bind_readme.md b/bind_readme.md new file mode 100644 index 0000000000..341626e7af --- /dev/null +++ b/bind_readme.md @@ -0,0 +1,226 @@ +# 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 + +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: + +all tags you can use are: + +- respHeader +- header +- 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 + +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"` // by time.Time.UnmarshalText, will ben explained later + 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 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. + +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 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 + +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. + +```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("/", 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) + }) +} +``` + +### 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(). // will validate &req + JSON(&body).Validate(). // will validate &body + Err(); err != nil { + return err + } + + return nil + }) +} +``` + +### 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(...)` diff --git a/bind_test.go b/bind_test.go index 48f53f62a1..1e582f27b7 100644 --- a/bind_test.go +++ b/bind_test.go @@ -3,259 +3,173 @@ package fiber import ( "bytes" - "compress/gzip" - "encoding/json" - "errors" "fmt" - "net/http/httptest" - "reflect" + "mime/multipart" + "net/url" + "regexp" + "strconv" "testing" "time" - "github.com/gofiber/fiber/v3/binder" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" ) -const helloWorld = "hello world" - -// go test -run Test_Bind_Query -v -func Test_Bind_Query(t *testing.T) { +func Test_Binder(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - - type Query struct { - Name string - Hobby []string - ID int - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - q := new(Query) - require.NoError(t, c.Bind().Query(q)) - require.Len(t, q.Hobby, 2) - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = new(Query) - require.NoError(t, c.Bind().Query(q)) - require.Len(t, q.Hobby, 2) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = new(Query) - require.NoError(t, c.Bind().Query(q)) - require.Len(t, q.Hobby, 3) + ctx := app.AcquireCtx(&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") - empty := new(Query) - c.Request().URI().SetQueryString("") - require.NoError(t, c.Bind().Query(empty)) - require.Empty(t, empty.Hobby) - - type Query2 struct { - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - ID int - Bool bool + var req struct { + ID string `param:"id"` } - 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 = helloWorld - require.NoError(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"` + var body struct { + Name string `json:"name"` } - 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.NoError(t, c.Bind().Query(aq)) - require.Len(t, aq.Data, 2) + 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_Query_Map -v -func Test_Bind_Query_Map(t *testing.T) { +// go test -run Test_Bind_BasicType -v +func Test_Bind_BasicType(t *testing.T) { t.Parallel() - app := New() c := app.AcquireCtx(&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.NoError(t, c.Bind().Query(&q)) - require.Len(t, q["hobby"], 2) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") - q = make(map[string][]string) - require.NoError(t, c.Bind().Query(&q)) - require.Len(t, q["hobby"], 2) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") - q = make(map[string][]string) - require.NoError(t, c.Bind().Query(&q)) - require.Len(t, q["hobby"], 3) - - c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer") - qq := make(map[string]string) - require.NoError(t, c.Bind().Query(&qq)) - require.Equal(t, "1", qq["id"]) + 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) - empty := make(map[string][]string) - c.Request().URI().SetQueryString("") - require.NoError(t, c.Bind().Query(&empty)) - require.Empty(t, empty["hobby"]) + type Query2 struct { + Flag []bool `query:"enable"` - em := make(map[string][]int) - c.Request().URI().SetQueryString("") - require.ErrorIs(t, c.Bind().Query(&em), binder.ErrMapNotConvertable) -} + I8 []int8 `query:"i8"` + I16 []int16 `query:"i16"` + I32 []int32 `query:"i32"` + I64 []int64 `query:"i64"` + I []int `query:"i"` -// go test -run Test_Bind_Query_WithSetParserDecoder -v -func Test_Bind_Query_WithSetParserDecoder(t *testing.T) { - type NonRFCTime time.Time + U8 []uint8 `query:"u8"` + U16 []uint16 `query:"u16"` + U32 []uint32 `query:"u32"` + U64 []uint64 `query:"u64"` + U []uint `query:"u"` - nonRFCConverter := func(value string) reflect.Value { - if v, err := time.Parse("2006-01-02", value); err == nil { - return reflect.ValueOf(v) - } - return reflect.Value{} + S []string `query:"s"` } - nonRFCTime := binder.ParserType{ - Customtype: NonRFCTime{}, - Converter: nonRFCConverter, - } + var q2 Query2 - binder.SetParserDecoder(binder.ParserConfig{ - IgnoreUnknownKeys: true, - ParserType: []binder.ParserType{nonRFCTime}, - ZeroEmpty: true, - SetAliasTag: "query", - }) + c.Request().URI().SetQueryString(qs) + require.NoError(t, c.Bind().Req(&q2).Err()) - app := New() - c := app.AcquireCtx(&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.NoError(t, c.Bind().Query(q)) - 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.NoError(t, c.Bind().Query(q)) - require.Equal(t, "", q.Title) + 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_Schema -v -func Test_Bind_Query_Schema(t *testing.T) { +func Test_Bind_NestedStruct(t *testing.T) { t.Parallel() + app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type Query1 struct { - Name string `query:"name,required"` - Nested struct { - Age int `query:"age"` - } `query:"nested,required"` + type AddressPayload struct { + Country string `query:"country"` + Country2 string `respHeader:"country"` } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q := new(Query1) - require.NoError(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.NoError(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 Address struct { + City string `query:"city"` + Zip int `query:"zip"` + Payload AddressPayload `query:"payload"` + } - type Query2 struct { - Name string `query:"name"` - Nested struct { - Age int `query:"age,required"` - } `query:"nested"` + type User struct { + Name string `query:"name"` + Age int `query:"age"` + Address Address `query:"address"` } - c.Request().URI().SetQueryString("name=tom&nested.age=10") - q2 := new(Query2) - require.NoError(t, c.Bind().Query(q2)) - c.Request().URI().SetQueryString("nested.age=10") - q2 = new(Query2) - require.NoError(t, c.Bind().Query(q2)) + c.Request().URI().SetQueryString("name=john&age=30&address.city=NY&address.zip=10001&address.payload.country=US&address.payload.country2=US") - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) + var u User + require.NoError(t, c.Bind().Req(&u).Err()) - c.Request().URI().SetQueryString("nested.agex=10") - q2 = new(Query2) - require.Equal(t, "nested.age is empty", c.Bind().Query(q2).Error()) + require.Equal(t, User{ + Name: "john", + Age: 30, + Address: Address{ + City: "NY", + Zip: 10001, + Payload: AddressPayload{ + Country: "US", + Country2: "", + }, + }, + }, u) +} - type Node struct { - Next *Node `query:"next,required"` - Value int `query:"val,required"` - } - c.Request().URI().SetQueryString("val=1&next.val=3") - n := new(Node) - require.NoError(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.NoError(t, c.Bind().Query(n)) - require.Equal(t, 3, n.Value) - require.Equal(t, 0, n.Next.Value) +func Test_Bind_Slice_NestedStruct(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Person struct { Name string `query:"name"` @@ -266,1416 +180,478 @@ func Test_Bind_Query_Schema(t *testing.T) { 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.NoError(t, c.Bind().Query(cq)) - require.Len(t, cq.Data, 2) - 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.NoError(t, c.Bind().Query(cq)) - require.Len(t, cq.Data, 2) - 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.AcquireCtx(&fasthttp.RequestCtx{}) + var cq CollectionQuery - type Header struct { - Name string - Hobby []string - ID int - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") + require.NoError(t, c.Bind().Req(&cq).Err()) - c.Request().Header.Add("id", "1") - c.Request().Header.Add("Name", "John Doe") - c.Request().Header.Add("Hobby", "golang,fiber") - q := new(Header) - require.NoError(t, c.Bind().Header(q)) - require.Len(t, q.Hobby, 2) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.NoError(t, c.Bind().Header(q)) - require.Len(t, q.Hobby, 3) - - empty := new(Header) - c.Request().Header.Del("hobby") - require.NoError(t, c.Bind().Query(empty)) - require.Empty(t, empty.Hobby) - - type Header2 struct { - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - ID int - Bool bool - } - - 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 = helloWorld - require.NoError(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.AcquireCtx(&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.NoError(t, c.Bind().Header(&q)) - require.Len(t, q["Hobby"], 2) - - c.Request().Header.Del("hobby") - c.Request().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.NoError(t, c.Bind().Header(&q)) - require.Len(t, q["Hobby"], 3) - - empty := make(map[string][]string, 0) - c.Request().Header.Del("hobby") - require.NoError(t, c.Bind().Query(&empty)) - require.Empty(t, empty["Hobby"]) + require.Equal(t, CollectionQuery{ + Data: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, cq) } -// 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", - }) - +func Benchmark_Bind_Slice_NestedStruct(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type NonRFCTimeInput struct { - Date NonRFCTime `req:"date"` - Title string `req:"title"` - Body string `req:"body"` + type Person struct { + Name string `query:"name"` + Age int `query:"age"` } - 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.NoError(t, c.Bind().Header(r)) - 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", + type CollectionQuery struct { + Data []Person `query:"data"` } - require.NoError(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.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") - 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("") + var cq CollectionQuery - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - q := new(Header1) - require.NoError(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.NoError(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"` + for i := 0; i < b.N; i++ { + _ = c.Bind().Req(&cq) } - c.Request().Header.Add("Name", "tom") - c.Request().Header.Add("Nested.Age", "10") - - h2 := new(Header2) - require.NoError(t, c.Bind().Header(h2)) + require.NoError(b, c.Bind().Req(&cq).Err()) - c.Request().Header.Del("Name") - h2 = new(Header2) - require.NoError(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 { - Next *Node `header:"Next,required"` - Value int `header:"Val,required"` - } - c.Request().Header.Add("Val", "1") - c.Request().Header.Add("Next.Val", "3") - n := new(Node) - require.NoError(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.NoError(t, c.Bind().Header(n)) - require.Equal(t, 3, n.Value) - require.Equal(t, 0, n.Next.Value) + require.Equal(b, CollectionQuery{ + Data: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, cq) } -// go test -run Test_Bind_Resp_Header -v -func Test_Bind_RespHeader(t *testing.T) { +func Test_Bind_Slice_NestedStruct2(t *testing.T) { t.Parallel() + app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type Header struct { - Name string - Hobby []string - ID int + type Person struct { + Name string `query:"name"` + Age int `query:"age"` } - 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.NoError(t, c.Bind().RespHeader(q)) - require.Len(t, q.Hobby, 2) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = new(Header) - require.NoError(t, c.Bind().RespHeader(q)) - require.Len(t, q.Hobby, 3) - - empty := new(Header) - c.Response().Header.Del("hobby") - require.NoError(t, c.Bind().Query(empty)) - require.Empty(t, empty.Hobby) - - type Header2 struct { - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - ID int - Bool bool + type Family struct { + Name string `query:"name"` + Members []Person `query:"members"` } - 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 = helloWorld - require.NoError(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"` + type CollectionQuery struct { + Data []Family `query:"data"` } - 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.AcquireCtx(&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.NoError(t, c.Bind().RespHeader(&q)) - require.Len(t, q["Hobby"], 2) - - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") - q = make(map[string][]string, 0) - require.NoError(t, c.Bind().RespHeader(&q)) - require.Len(t, q["Hobby"], 3) - - empty := make(map[string][]string, 0) - c.Response().Header.Del("hobby") - require.NoError(t, c.Bind().Query(&empty)) - require.Empty(t, empty["Hobby"]) -} + 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") -// go test -v -run=^$ -bench=Benchmark_Bind_Query -benchmem -count=4 -func Benchmark_Bind_Query(b *testing.B) { - var err error + var cq CollectionQuery - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) + require.NoError(t, c.Bind().Req(&cq).Err()) - type Query struct { - Name string - Hobby []string - ID int - } - 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++ { - err = c.Bind().Query(q) - } - require.NoError(b, err) + require.Equal(t, CollectionQuery{ + Data: []Family{ + { + Name: "doe", + Members: []Person{ + {Name: "john", Age: 10}, + {Name: "doe", Age: 12}, + }, + }, + }, + }, cq) } -// go test -v -run=^$ -bench=Benchmark_Bind_Query_Map -benchmem -count=4 -func Benchmark_Bind_Query_Map(b *testing.B) { - var err error +func Test_Bind_Slice_NestedStruct3(t *testing.T) { + t.Parallel() app := New() c := app.AcquireCtx(&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++ { - err = c.Bind().Query(&q) + type Test2 struct { + Name string `query:"name"` + Age int `query:"age"` } - require.NoError(b, err) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Query_WithParseParam -benchmem -count=4 -func Benchmark_Bind_Query_WithParseParam(b *testing.B) { - var err error - - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) type Person struct { Name string `query:"name"` Age int `query:"age"` + Test Test2 `query:"test"` } 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++ { - err = c.Bind().Query(cq) - } - - require.NoError(b, err) -} + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.0.test.name=doe&data.0.test.age=12") -// go test -v -run=^$ -bench=Benchmark_Bind_Query_Comma -benchmem -count=4 -func Benchmark_Bind_Query_Comma(b *testing.B) { - var err error + var cq CollectionQuery - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) + require.NoError(t, c.Bind().Req(&cq).Err()) - type Query struct { - Name string - Hobby []string - ID int - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") - 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++ { - err = c.Bind().Query(q) - } - require.NoError(b, err) + require.Equal(t, CollectionQuery{ + Data: []Person{ + { + Name: "john", + Age: 10, + Test: Test2{ + Name: "doe", + Age: 12, + }, + }, + }, + }, cq) } -// go test -v -run=^$ -bench=Benchmark_Bind_Header -benchmem -count=4 -func Benchmark_Bind_Header(b *testing.B) { - var err error - +// go test -run Test_Bind_Query -v +func Test_Bind_Query(t *testing.T) { + t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type ReqHeader struct { - Name string - Hobby []string - ID int - } - 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++ { - err = c.Bind().Header(q) + type Query struct { + ID int `query:"id"` + Name string `query:"name"` + Hobby []string `query:"hobby"` } - require.NoError(b, err) -} -// go test -v -run=^$ -bench=Benchmark_Bind_Header_Map -benchmem -count=4 -func Benchmark_Bind_Header_Map(b *testing.B) { - var err error - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) + var q Query - c.Request().SetBody([]byte(``)) + c.Request().SetBody([]byte{}) c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") - 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++ { - err = c.Bind().Header(&q) - } - require.NoError(b, err) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader -benchmem -count=4 -func Benchmark_Bind_RespHeader(b *testing.B) { - var err error + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 2, len(q.Hobby)) - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) + 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)) - type ReqHeader struct { - Name string - Hobby []string - ID int - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") + 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.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + c.Request().URI().SetQueryString("") + require.NoError(t, c.Bind().Req(&q).Err()) + require.Equal(t, 0, len(q.Hobby)) - q := new(ReqHeader) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - err = c.Bind().RespHeader(q) + 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"` } - require.NoError(b, err) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_RespHeader_Map -benchmem -count=4 -func Benchmark_Bind_RespHeader_Map(b *testing.B) { - var err error - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") + var q2 Query2 - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + 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) - q := make(map[string][]string) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - err = c.Bind().RespHeader(&q) + type ArrayQuery struct { + Data []string `query:"data[]"` } - require.NoError(b, err) + 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_Body_Compression -func Test_Bind_Body(t *testing.T) { +// go test -run Test_Bind_Resp_Header -v +func Test_Bind_Resp_Header(t *testing.T) { t.Parallel() app := New() - reqBody := []byte(`{"name":"john"}`) - - type Demo struct { - Name string `json:"name" xml:"name" form:"name" query:"name"` - } - - // Helper function to test compressed bodies - testCompressedBody := func(t *testing.T, compressedBody []byte, encoding string) { - t.Helper() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.Set(fasthttp.HeaderContentEncoding, encoding) - c.Request().SetBody(compressedBody) - c.Request().Header.SetContentLength(len(compressedBody)) - d := new(Demo) - require.NoError(t, c.Bind().Body(d)) - require.Equal(t, "john", d.Name) - c.Request().Header.Del(fasthttp.HeaderContentEncoding) - } + c := app.AcquireCtx(&fasthttp.RequestCtx{}) - t.Run("Gzip", func(t *testing.T) { - t.Parallel() - compressedBody := fasthttp.AppendGzipBytes(nil, reqBody) - require.NotEqual(t, reqBody, compressedBody) - testCompressedBody(t, compressedBody, "gzip") - }) - - t.Run("Deflate", func(t *testing.T) { - t.Parallel() - compressedBody := fasthttp.AppendDeflateBytes(nil, reqBody) - require.NotEqual(t, reqBody, compressedBody) - testCompressedBody(t, compressedBody, "deflate") - }) - - t.Run("Brotli", func(t *testing.T) { - t.Parallel() - compressedBody := fasthttp.AppendBrotliBytes(nil, reqBody) - require.NotEqual(t, reqBody, compressedBody) - testCompressedBody(t, compressedBody, "br") - }) - - t.Run("Zstd", func(t *testing.T) { - t.Parallel() - compressedBody := fasthttp.AppendZstdBytes(nil, reqBody) - require.NotEqual(t, reqBody, compressedBody) - testCompressedBody(t, compressedBody, "zstd") - }) - - testDecodeParser := func(t *testing.T, contentType, body string) { - t.Helper() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - require.NoError(t, c.Bind().Body(d)) - require.Equal(t, "john", d.Name) - } + type resHeader struct { + Key string `respHeader:"k"` - t.Run("JSON", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationJSON, `{"name":"john"}`) - }) - - t.Run("XML", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationXML, `john`) - }) - - t.Run("Form", func(t *testing.T) { - testDecodeParser(t, MIMEApplicationForm, "name=john") - }) - - t.Run("MultipartForm", func(t *testing.T) { - testDecodeParser(t, MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") - }) - - testDecodeParserError := func(t *testing.T, contentType, body string) { - t.Helper() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().Header.SetContentType(contentType) - c.Request().SetBody([]byte(body)) - c.Request().Header.SetContentLength(len(body)) - require.Error(t, c.Bind().Body(nil)) + Keys []string `respHeader:"keys"` } - t.Run("ErrorInvalidContentType", func(t *testing.T) { - testDecodeParserError(t, "invalid-content-type", "") - }) - - t.Run("ErrorMalformedMultipart", func(t *testing.T) { - testDecodeParserError(t, MIMEMultipartForm+`;boundary="b"`, "--b") - }) + c.Set("k", "vv") + c.Response().Header.Add("keys", "v1") + c.Response().Header.Add("keys", "v2") - type CollectionQuery struct { - Data []Demo `query:"data"` - } - - t.Run("CollectionQuerySquareBrackets", func(t *testing.T) { - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - 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.NoError(t, c.Bind().Body(cq)) - require.Len(t, cq.Data, 2) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, "doe", cq.Data[1].Name) - }) - - t.Run("CollectionQueryDotNotation", func(t *testing.T) { - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - 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.NoError(t, c.Bind().Body(cq)) - require.Len(t, cq.Data, 2) - require.Equal(t, "john", cq.Data[0].Name) - require.Equal(t, "doe", cq.Data[1].Name) - }) + 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) } -// 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.AcquireCtx(&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.NoError(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) - } +var _ Binder = (*userCtxUnmarshaler)(nil) - 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--") +type userCtxUnmarshaler struct { + V int } -// go test -v -run=^$ -bench=Benchmark_Bind_Body_JSON -benchmem -count=4 -func Benchmark_Bind_Body_JSON(b *testing.B) { - var err error - - app := New() - c := app.AcquireCtx(&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++ { - err = c.Bind().Body(d) - } - require.NoError(b, err) - require.Equal(b, "john", d.Name) +func (u *userCtxUnmarshaler) UnmarshalFiberCtx(ctx Ctx) error { + u.V++ + return nil } -// go test -v -run=^$ -bench=Benchmark_Bind_Body_XML -benchmem -count=4 -func Benchmark_Bind_Body_XML(b *testing.B) { - var err error - +// go test -run Test_Bind_CustomizedUnmarshaler -v +func Test_Bind_CustomizedUnmarshaler(t *testing.T) { + t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type Demo struct { - Name string `xml:"name"` + type Req struct { + Key userCtxUnmarshaler } - 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() + var r Req + require.NoError(t, c.Bind().Req(&r).Err()) + require.Equal(t, 1, r.Key.V) - for n := 0; n < b.N; n++ { - err = c.Bind().Body(d) - } - require.NoError(b, err) - require.Equal(b, "john", d.Name) + require.NoError(t, c.Bind().Req(&r).Err()) + require.Equal(t, 1, r.Key.V) } -// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form -benchmem -count=4 -func Benchmark_Bind_Body_Form(b *testing.B) { - var err error - +// go test -run Test_Bind_TextUnmarshaler -v +func Test_Bind_TextUnmarshaler(t *testing.T) { + t.Parallel() app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - type Demo struct { - Name string `form:"name"` + type Req struct { + Time time.Time `query:"time"` } - 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() + now := time.Now() - for n := 0; n < b.N; n++ { - err = c.Bind().Body(d) - } - require.NoError(b, err) - require.Equal(b, "john", d.Name) -} + c.Request().URI().SetQueryString(url.Values{ + "time": []string{now.Format(time.RFC3339Nano)}, + }.Encode()) -// go test -v -run=^$ -bench=Benchmark_Bind_Body_MultipartForm -benchmem -count=4 -func Benchmark_Bind_Body_MultipartForm(b *testing.B) { - var err error + 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.AcquireCtx(&fasthttp.RequestCtx{}) - type Demo struct { - Name string `form:"name"` + type Req struct { + Time time.Time `query:"time"` } - 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) + c.Request().URI().SetQueryString("time=john") - b.ReportAllocs() - b.ResetTimer() + err := c.Bind().Req(&Req{}).Err() - for n := 0; n < b.N; n++ { - err = c.Bind().Body(d) - } - require.NoError(b, err) - require.Equal(b, "john", d.Name) + require.Error(t, err) + require.Regexp(t, regexp.MustCompile(`unable to decode 'john' as time`), err.Error()) } -// go test -v -run=^$ -bench=Benchmark_Bind_Body_Form_Map -benchmem -count=4 -func Benchmark_Bind_Body_Form_Map(b *testing.B) { - var err error - +func Test_Bind_Form(t *testing.T) { + t.Parallel() app := New() - c := app.AcquireCtx(&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) + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - b.ReportAllocs() - b.ResetTimer() + c.Context().Request.Header.Set(HeaderContentType, MIMEApplicationForm) + c.Context().Request.SetBody([]byte(url.Values{ + "username": {"u"}, + "password": {"p"}, + "likes": {"apple", "banana"}, + }.Encode())) - for n := 0; n < b.N; n++ { - err = c.Bind().Body(&d) + type Req struct { + Username string `form:"username"` + Password string `form:"password"` + Likes []string `form:"likes"` } - require.NoError(b, err) - require.Equal(b, "john", d["name"]) -} -// go test -run Test_Bind_URI -func Test_Bind_URI(t *testing.T) { - t.Parallel() + var r Req + err := c.Bind().Form(&r).Err() - app := New() - app.Get("/test1/userId/role/:roleId", func(c Ctx) error { - type Demo struct { - UserID uint `uri:"userId"` - RoleID uint `uri:"roleId"` - } - 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 - }) - _, err := app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) - require.NoError(t, err) - _, err = app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) require.NoError(t, err) + require.Equal(t, "u", r.Username) + require.Equal(t, "p", r.Password) + require.Equal(t, []string{"apple", "banana"}, r.Likes) } -// go test -run Test_Bind_URI_Map -func Test_Bind_URI_Map(t *testing.T) { +func Test_Bind_Multipart(t *testing.T) { t.Parallel() - app := New() - app.Get("/test1/userId/role/:roleId", func(c Ctx) error { - d := make(map[string]string) + c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) - 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 - }) - _, err := app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) - require.NoError(t, err) - _, err = app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) - require.NoError(t, err) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_URI -benchmem -count=4 -func Benchmark_Bind_URI(b *testing.B) { - var err error - - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed - - c.route = &Route{ - Params: []string{ - "param1", "param2", "param3", "param4", + 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"}, }, - } - 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++ { - err = c.Bind().URI(&res) - } + }, boundary) - require.NoError(b, err) - 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) { - var err error + require.NoError(t, err) - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) //nolint:errcheck, forcetypeassert // not needed + c.Context().Request.Header.Set(HeaderContentType, fmt.Sprintf("%s; boundary=%s", MIMEMultipartForm, boundary)) + c.Context().Request.SetBody(buf.Bytes()) - c.route = &Route{ - Params: []string{ - "param1", "param2", "param3", "param4", - }, - } - c.values = [maxParams]string{ - "john", "doe", "is", "awesome", + type Req struct { + Username string `multipart:"username"` + Password string `multipart:"password"` + Likes []string `multipart:"likes"` } - res := make(map[string]string) - - b.ReportAllocs() - b.ResetTimer() - - for n := 0; n < b.N; n++ { - err = c.Bind().URI(&res) - } + var r Req + err = c.Bind().Multipart(&r).Err() + require.NoError(t, err) - require.NoError(b, err) - require.Equal(b, "john", res["param1"]) - require.Equal(b, "doe", res["param2"]) - require.Equal(b, "is", res["param3"]) - require.Equal(b, "awesome", res["param4"]) + require.Equal(t, "u", r.Username) + require.Equal(t, "p", r.Password) + require.Equal(t, []string{"apple", "banana"}, r.Likes) } -// go test -run Test_Bind_Cookie -v -func Test_Bind_Cookie(t *testing.T) { - t.Parallel() - - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - - type Cookie struct { - Name string - Hobby []string - ID int - } - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") +type Req struct { + ID string `params:"id"` - c.Request().Header.SetCookie("id", "1") - c.Request().Header.SetCookie("Name", "John Doe") - c.Request().Header.SetCookie("Hobby", "golang,fiber") - q := new(Cookie) - require.NoError(t, c.Bind().Cookie(q)) - require.Len(t, q.Hobby, 2) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = new(Cookie) - require.NoError(t, c.Bind().Cookie(q)) - require.Len(t, q.Hobby, 3) - - empty := new(Cookie) - c.Request().Header.DelCookie("hobby") - require.NoError(t, c.Bind().Query(empty)) - require.Empty(t, empty.Hobby) - - type Cookie2 struct { - Name string - Hobby string - FavouriteDrinks []string - Empty []string - Alloc []string - No []int64 - ID int - Bool bool - } + I int `query:"I"` + J int `query:"j"` + K int `query:"k"` - 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 = helloWorld - require.NoError(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()) + Token string `header:"x-auth"` } -// go test -run Test_Bind_Cookie_Map -v -func Test_Bind_Cookie_Map(t *testing.T) { - t.Parallel() - +func getBenchCtx() Ctx { app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetBody([]byte(``)) - c.Request().Header.SetContentType("") + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} - 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.NoError(t, c.Bind().Cookie(&q)) - require.Len(t, q["Hobby"], 2) - - c.Request().Header.DelCookie("hobby") - c.Request().Header.SetCookie("Hobby", "golang,fiber,go") - q = make(map[string][]string) - require.NoError(t, c.Bind().Cookie(&q)) - require.Len(t, q["Hobby"], 3) - - empty := make(map[string][]string) - c.Request().Header.DelCookie("hobby") - require.NoError(t, c.Bind().Query(&empty)) - require.Empty(t, 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.AcquireCtx(&fasthttp.RequestCtx{}) + var u = fasthttp.URI{} + u.SetQueryString("j=1&j=123&k=-1") + ctx.Request().SetURI(&u) - type NonRFCTimeInput struct { - Date NonRFCTime `cerez:"date"` - Title string `cerez:"title"` - Body string `cerez:"body"` - } + ctx.Request().Header.Set("a-auth", "bearer tt") - 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.NoError(t, c.Bind().Cookie(r)) - 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.NoError(t, c.Bind().Cookie(r)) - require.Equal(t, "", r.Title) + return ctx } -// go test -run Test_Bind_Cookie_Schema -v -func Test_Bind_Cookie_Schema(t *testing.T) { - t.Parallel() - - app := New() - c := app.AcquireCtx(&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.NoError(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.NoError(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"` - } +func Benchmark_Bind_by_hand(b *testing.B) { + ctx := getBenchCtx() + for i := 0; i < b.N; i++ { + var req Req + var err error - c.Request().Header.SetCookie("Name", "tom") - c.Request().Header.SetCookie("Nested.Age", "10") + if raw := ctx.Params("id"); raw != "" { + req.ID = raw + } - h2 := new(Cookie2) - require.NoError(t, c.Bind().Cookie(h2)) + if raw := ctx.Query("i"); raw != "" { + req.I, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } - c.Request().Header.DelCookie("Name") - h2 = new(Cookie2) - require.NoError(t, c.Bind().Cookie(h2)) + if raw := ctx.Query("j"); raw != "" { + req.J, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } - 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()) + if raw := ctx.Query("k"); raw != "" { + req.K, err = strconv.Atoi(raw) + if err != nil { + b.Error(err) + b.FailNow() + } + } - type Node struct { - Next *Node `cookie:"Next,required"` - Value int `cookie:"Val,required"` + req.Token = ctx.Get("x-auth") } - c.Request().Header.SetCookie("Val", "1") - c.Request().Header.SetCookie("Next.Val", "3") - n := new(Node) - require.NoError(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.NoError(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) { - var err error - - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - - type Cookie struct { - Name string - Hobby []string - ID int +func Benchmark_Bind_NestedStruct(b *testing.B) { + type tokenStruct struct { + Token string `header:"x-auth"` } - 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") + type reqStruct struct { + ID string `params:"id"` - q := new(Cookie) - b.ReportAllocs() - b.ResetTimer() + I int `query:"I"` + J int `query:"j"` + K int `query:"k"` - for n := 0; n < b.N; n++ { - err = c.Bind().Cookie(q) + Token tokenStruct `header:"token"` } - require.NoError(b, err) -} - -// go test -v -run=^$ -bench=Benchmark_Bind_Cookie_Map -benchmem -count=4 -func Benchmark_Bind_Cookie_Map(b *testing.B) { - var err error app := New() - c := app.AcquireCtx(&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++ { - err = c.Bind().Cookie(&q) - } - require.NoError(b, err) -} -// custom binder for testing -type customBinder struct{} + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"id string"} + ctx.route = &Route{Params: []string{"id"}} -func (*customBinder) Name() string { - return "custom" -} + var u = fasthttp.URI{} + u.SetQueryString("j=1&I=123&k=-1") + ctx.Request().SetURI(&u) -func (*customBinder) MIMETypes() []string { - return []string{"test", "test2"} -} + ctx.Request().Header.Set("token.x-auth", "bearer tt") -func (*customBinder) Parse(c Ctx, out any) error { - return json.Unmarshal(c.Body(), out) -} + for i := 0; i < b.N; i++ { + var req reqStruct -// go test -run Test_Bind_CustomBinder -func Test_Bind_CustomBinder(t *testing.T) { - app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - - // Register binder - customBinder := &customBinder{} - app.RegisterCustomBinder(customBinder) - - type Demo struct { - Name string `json:"name"` + err := ctx.Bind().Req(&req).Err() + if err != nil { + b.Error(err) + b.FailNow() + } } - body := []byte(`{"name":"john"}`) - c.Request().SetBody(body) - c.Request().Header.SetContentType("test") - c.Request().Header.SetContentLength(len(body)) - d := new(Demo) - - require.NoError(t, c.Bind().Body(d)) - require.NoError(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.AcquireCtx(&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 (*structValidator) Validate(out any) error { - out = reflect.ValueOf(out).Elem().Interface() - sq, ok := out.(simpleQuery) - if !ok { - return errors.New("failed to type-assert to simpleQuery") - } - - if sq.Name != "john" { - return errors.New("you should have entered right name") +func Benchmark_Bind(b *testing.B) { + ctx := getBenchCtx() + for i := 0; i < b.N; i++ { + var v = Req{} + err := ctx.Bind().Req(&v).Err() + if err != nil { + b.Error(err) + b.FailNow() + } } - - 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.AcquireCtx(&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.NoError(t, c.Bind().Query(rq)) } -// go test -run Test_Bind_RepeatParserWithSameStruct -v -func Test_Bind_RepeatParserWithSameStruct(t *testing.T) { +func Test_Binder_Float(t *testing.T) { t.Parallel() app := New() - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - - type Request struct { - QueryParam string `query:"query_param"` - HeaderParam string `header:"header_param"` - BodyParam string `json:"body_param" xml:"body_param" form:"body_param"` - } - r := new(Request) + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}).(*DefaultCtx) + ctx.values = [maxParams]string{"3.14"} + ctx.route = &Route{Params: []string{"id"}} - c.Request().URI().SetQueryString("query_param=query_param") - require.NoError(t, c.Bind().Query(r)) - require.Equal(t, "query_param", r.QueryParam) - - c.Request().Header.Add("header_param", "header_param") - require.NoError(t, c.Bind().Header(r)) - require.Equal(t, "header_param", r.HeaderParam) - - var gzipJSON bytes.Buffer - w := gzip.NewWriter(&gzipJSON) - _, err := w.Write([]byte(`{"body_param":"body_param"}`)) - require.NoError(t, err) - err = w.Close() - require.NoError(t, err) - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().Header.Set(HeaderContentEncoding, "gzip") - c.Request().SetBody(gzipJSON.Bytes()) - c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) - require.NoError(t, c.Bind().Body(r)) - require.Equal(t, "body_param", r.BodyParam) - 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)) - require.NoError(t, c.Bind().Body(r)) - require.Equal(t, "body_param", r.BodyParam) + var req struct { + ID1 float32 `param:"id"` + ID2 float64 `param:"id"` } - testDecodeParser(MIMEApplicationJSON, `{"body_param":"body_param"}`) - testDecodeParser(MIMEApplicationXML, `body_param`) - testDecodeParser(MIMEApplicationForm, "body_param=body_param") - testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--") + 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/binder.go b/binder.go new file mode 100644 index 0000000000..86bf2bd4cd --- /dev/null +++ b/binder.go @@ -0,0 +1,242 @@ +package fiber + +import ( + "bytes" + "net/http" + "reflect" + "sync" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/gofiber/utils/v2" +) + +var binderPool = sync.Pool{New: func() any { + return &Bind{} +}} + +type Bind struct { + err error + ctx Ctx + val any // last decoded val + strict bool +} + +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 { + err := b.Err() + + if err != nil { + if fe, ok := err.(*Error); ok { + return fe + } + + return NewError(http.StatusBadRequest, err.Error()) + } + + return 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 { + err := b.err + + b.reset() + binderPool.Put(b) + + return err +} + +// JSON unmarshal body as json +func (b *Bind) JSON(v any) *Bind { + if b.err != nil { + return b + } + + 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.App().config.JSONDecoder(b.ctx.Body(), v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + +// XML unmarshal body as xml +func (b *Bind) XML(v any) *Bind { + if b.err != nil { + return b + } + + 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.App().config.XMLDecoder(b.ctx.Body(), v); err != nil { + return b.setErr(err) + } + + b.val = v + return b +} + +// Form unmarshal body as form +func (b *Bind) Form(v any) *Bind { + if b.err != nil { + return b + } + + 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 { + 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 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 { + 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.reqDecode(v); err != nil { + return b.setErr(err) + } + + b.val = v + 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) reqDecode(v any) error { + rv, typeID := bind.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(), bindCompileOption{reqDecoder: true}) + if err != nil { + return err + } + + b.ctx.App().bindDecoderCache.Store(typeID, decoder) + return decoder(b.ctx, rv.Elem()) +} + +func (b *Bind) formDecode(v any) error { + rv, typeID := bind.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 := bind.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/README.md b/binder/README.md deleted file mode 100644 index 676e1c9e89..0000000000 --- a/binder/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Fiber Binders - -Binder is a new request/response binding feature for Fiber. Against the 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: - -```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: - -```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. If you want to use your own 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" -``` diff --git a/binder/binder.go b/binder/binder.go deleted file mode 100644 index fb7ac12dab..0000000000 --- a/binder/binder.go +++ /dev/null @@ -1,23 +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{} - RespHeaderBinder = &respHeaderBinding{} - CookieBinder = &cookieBinding{} - QueryBinder = &queryBinding{} - FormBinder = &formBinding{} - URIBinder = &uriBinding{} - XMLBinder = &xmlBinding{} - JSONBinder = &jsonBinding{} -) diff --git a/binder/cookie.go b/binder/cookie.go deleted file mode 100644 index 0f5c650c33..0000000000 --- a/binder/cookie.go +++ /dev/null @@ -1,44 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/utils/v2" - "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 f45407fe93..0000000000 --- a/binder/form.go +++ /dev/null @@ -1,57 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/utils/v2" - "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) - } - }) - - if err != nil { - return err - } - - 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 196163694d..0000000000 --- a/binder/header.go +++ /dev/null @@ -1,34 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/utils/v2" - "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 6c0d80c89b..0000000000 --- a/binder/json.go +++ /dev/null @@ -1,15 +0,0 @@ -package binder - -import ( - "github.com/gofiber/utils/v2" -) - -type jsonBinding struct{} - -func (*jsonBinding) Name() string { - return "json" -} - -func (*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 07af94a152..0000000000 --- a/binder/mapping.go +++ /dev/null @@ -1,228 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - "sync" - - "github.com/gofiber/fiber/v3/internal/schema" - "github.com/gofiber/utils/v2" - "github.com/valyala/bytebufferpool" -) - -// ParserConfig form decoder config for SetParserDecoder -type ParserConfig struct { - SetAliasTag string - ParserType []ParserType - IgnoreUnknownKeys bool - 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 -} - -var ( - // decoderPoolMap helps to improve binders - decoderPoolMap = map[string]*sync.Pool{} - // tags is used to classify parser's pool - tags = []string{HeaderBinder.Name(), RespHeaderBinder.Name(), CookieBinder.Name(), QueryBinder.Name(), FormBinder.Name(), URIBinder.Name()} -) - -// SetParserDecoder allow globally change the option of form decoder, update decoderPool -func SetParserDecoder(parserConfig ParserConfig) { - for _, tag := range tags { - decoderPoolMap[tag] = &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 -} - -func init() { - for _, tag := range tags { - decoderPoolMap[tag] = &sync.Pool{New: func() any { - return decoderBuilder(ParserConfig{ - IgnoreUnknownKeys: true, - ZeroEmpty: true, - }) - }} - } -} - -// 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 := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed - defer decoderPoolMap[aliasTag].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 //nolint:wrapcheck // unnecessary to wrap it - } - } - - if b == '[' || b == ']' { - continue - } - - if err := bb.WriteByte(b); err != nil { - return "", err //nolint:wrapcheck // unnecessary to wrap it - } - } - - 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 { - // Is the field an embedded struct? - if structFieldKind == reflect.Struct { - // Loop over embedded struct fields - for j := 0; j < structField.NumField(); j++ { - structFieldField := structField.Field(j) - - // Can this embedded field be changed? - if !structFieldField.CanSet() { - continue - } - - // Is the embedded struct field type equal to the input? - if structFieldField.Kind() == kind { - return true - } - } - } - - 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 25b69f5bc3..0000000000 --- a/binder/query.go +++ /dev/null @@ -1,48 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/utils/v2" - "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 0455185ba1..0000000000 --- a/binder/resp_header.go +++ /dev/null @@ -1,34 +0,0 @@ -package binder - -import ( - "reflect" - "strings" - - "github.com/gofiber/utils/v2" - "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 35d1de86bc..0000000000 --- a/binder/xml.go +++ /dev/null @@ -1,20 +0,0 @@ -package binder - -import ( - "encoding/xml" - "fmt" -) - -type xmlBinding struct{} - -func (*xmlBinding) Name() string { - return "xml" -} - -func (*xmlBinding) Bind(body []byte, out any) error { - if err := xml.Unmarshal(body, out); err != nil { - return fmt.Errorf("failed to unmarshal xml: %w", err) - } - - return nil -} diff --git a/binder_compile.go b/binder_compile.go new file mode 100644 index 0000000000..0067db14c9 --- /dev/null +++ b/binder_compile.go @@ -0,0 +1,320 @@ +package fiber + +import ( + "bytes" + "encoding" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/gofiber/utils/v2" +) + +type Decoder func(c Ctx, rv reflect.Value) error + +const bindTagRespHeader = "respHeader" +const bindTagHeader = "header" +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() + +type bindCompileOption struct { + bodyDecoder bool // to parse `form` or `multipart/form-data` + 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 + + el := rt.Elem() + if el.Kind() != reflect.Struct { + return nil, &UnsupportedBinderError{Type: rt} + } + + 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) + 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 +} + +type parentDecoder struct { + tagScope string + tagContent string + isSlice bool +} + +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} + } + + var tagScope = "" + for _, loopTagScope := range tags { + if _, ok := field.Tag.Lookup(loopTagScope); ok { + tagScope = loopTagScope + break + } + } + + if tagScope == "" { + return nil, nil + } + + // If parent tag scope is present, just override it and append the parent tag content + var tagContent string + if parent != nil { + if tagScope != parent.tagScope { + return nil, nil + } + + 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 field.Type.Kind() == reflect.Slice { + return compileSliceFieldTextBasedDecoder(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 { + 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, opt bindCompileOption, isTextMarshaler ...bool) (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 + case bindTagMultipart: + get = multipartGetter + case bindTagForm: + get = formGetter + default: + return nil, errors.New("unexpected tag scope " + strconv.Quote(tagScope)) + } + + fieldDecoder := &fieldTextDecoder{ + fieldIndex: index, + fieldName: field.Name, + tag: tagScope, + reqKey: tagContent, + 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 + } + + // 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 + } + + if dec != nil { + 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, opt bindCompileOption) (decoder, error) { + if field.Type.Kind() != reflect.Slice { + panic("BUG: unexpected type, expecting slice " + field.Type.String()) + } + + et := field.Type.Elem() + + 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 bindTagForm: + visitAll = visitForm + case bindTagMultipart: + visitAll = visitMultipart + 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)) + } + + sliceDecoder := &fieldSliceDecoder{ + fieldIndex: index, + eqBytes: eqBytes, + fieldName: field.Name, + visitAll: visitAll, + reqKey: []byte(tagContent), + fieldType: field.Type, + 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 + 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 new file mode 100644 index 0000000000..7c545ae18f --- /dev/null +++ b/binder_compile_test.go @@ -0,0 +1,406 @@ +package fiber + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +type postStruct struct { + 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 { + Name string `form:"name"` + Age int `form:"age"` + Post postStruct `form:"post"` +} + +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) { + 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) { + t.Parallel() + + 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.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) + + 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.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) + + 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.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) + + 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.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) + + 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.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.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)) + }) +} + +func Test_compileFieldDecoder(t *testing.T) { + t.Parallel() + + opt := bindCompileOption{ + bodyDecoder: true, + } + + testVar := reflect.TypeOf(&testStruct2{}) + 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, 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 new file mode 100644 index 0000000000..6c1f258dd2 --- /dev/null +++ b/binder_slice.go @@ -0,0 +1,226 @@ +package fiber + +import ( + "bytes" + "reflect" + "strconv" + "strings" + + "github.com/gofiber/fiber/v3/internal/bind" + "github.com/gofiber/utils/v2" +) + +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)) + 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) { + 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 (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" +} + +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) +} + +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/ctx.go b/ctx.go index 4d7417ee2a..a7fd21f4a8 100644 --- a/ctx.go +++ b/ctx.go @@ -51,7 +51,6 @@ type DefaultCtx struct { app *App // Reference to *App route *Route // Reference to *Route fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - bind *Bind // Default bind reference redirect *Redirect // Default redirect reference values [maxParams]string // Route parameter values viewBindMap sync.Map // Default view map to bind template engine @@ -269,6 +268,19 @@ func (c *DefaultCtx) BaseURL() string { return c.baseURI } +func (c *DefaultCtx) Bind() *Bind { + b := binderPool.Get().(*Bind) + b.ctx = c + return b +} + +func (c *DefaultCtx) Validate(v any) error { + if c.app.config.Validator == nil { + return NilValidatorError{} + } + return c.app.config.Validator.Validate(v) +} + // 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. @@ -1858,17 +1870,15 @@ func (c *DefaultCtx) IsFromLocal() bool { return c.isLocalHost(c.fasthttp.RemoteIP().String()) } -// 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 -func (c *DefaultCtx) Bind() *Bind { - if c.bind == nil { - c.bind = &Bind{ - ctx: c, - should: true, - } +// 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 c.bind + + return params } // Reset is a method to reset context fields by given request when to use server handlers. @@ -1895,7 +1905,6 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) { func (c *DefaultCtx) release() { c.route = nil c.fasthttp = nil - c.bind = nil c.flashMessages = c.flashMessages[:0] c.viewBindMap = sync.Map{} if c.redirect != nil { 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/ctx_test.go b/ctx_test.go index a1685898b0..79b4608b25 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -1413,110 +1413,6 @@ func Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) { } } -// go test -run Test_Ctx_Binders -v -func Test_Ctx_Binders(t *testing.T) { - t.Parallel() - // setup - app := New() - - type TestEmbeddedStruct struct { - Names []string `query:"names"` - } - - type TestStruct struct { - Name string - NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" header:"Name2"` - TestEmbeddedStruct - Class int - ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" header:"Class2"` - } - - withValues := func(t *testing.T, actionFn func(c Ctx, testStruct *TestStruct) error) { - t.Helper() - - c := app.AcquireCtx(&fasthttp.RequestCtx{}) - defer app.ReleaseCtx(c) - testStruct := new(TestStruct) - - require.NoError(t, actionFn(c, testStruct)) - require.Equal(t, "foo", testStruct.Name) - require.Equal(t, 111, testStruct.Class) - require.Equal(t, "bar", testStruct.NameWithDefault) - require.Equal(t, 222, testStruct.ClassWithDefault) - require.Equal(t, []string{"foo", "bar", "test"}, testStruct.TestEmbeddedStruct.Names) - } - - t.Run("Body:xml", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationXML) - c.Request().SetBody([]byte(`foo111bar222foobartest`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Body:form", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationForm) - c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222&names=foo,bar,test`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("BodyParser:json", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.SetContentType(MIMEApplicationJSON) - c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222,"names":["foo","bar","test"]}`)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Body:multiform", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"names\"\r\n\r\ntest\r\n--b--") - c.Request().SetBody(body) - c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) - c.Request().Header.SetContentLength(len(body)) - return c.Bind().Body(testStruct) - }) - }) - t.Run("Cookie", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222;names=foo,bar,test") - return c.Bind().Cookie(testStruct) - }) - }) - t.Run("Query", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222&names=foo,bar,test") - return c.Bind().Query(testStruct) - }) - }) - t.Run("URI", func(t *testing.T) { - t.Skip("URI is not ready for v3") - //nolint:gocritic // TODO: uncomment - //t.Parallel() - //withValues(t, func(c Ctx, testStruct *TestStruct) error { - // c.Route().Params = []string{"name", "name2", "class", "class2"} - // c.Params().value = [30]string{"foo", "bar", "111", "222"} - // return c.Bind().URI(testStruct) - //}) - }) - t.Run("ReqHeader", func(t *testing.T) { - t.Parallel() - withValues(t, func(c Ctx, testStruct *TestStruct) error { - c.Request().Header.Add("name", "foo") - c.Request().Header.Add("name2", "bar") - c.Request().Header.Add("class", "111") - c.Request().Header.Add("class2", "222") - c.Request().Header.Add("names", "foo,bar,test") - return c.Bind().Header(testStruct) - }) - }) -} - // go test -run Test_Ctx_Get func Test_Ctx_Get(t *testing.T) { t.Parallel() diff --git a/error.go b/error.go index 2e44c27769..048b5d6292 100644 --- a/error.go +++ b/error.go @@ -1,10 +1,8 @@ package fiber import ( - "encoding/json" "errors" - - "github.com/gofiber/fiber/v3/internal/schema" + "reflect" ) // Wrap and return this for unreachable code if panicking is undesirable (i.e., in a handler). @@ -31,8 +29,9 @@ var ( ErrRangeUnsatisfiable = errors.New("range: unsatisfiable range") ) -// Binder errors -var ErrCustomBinderNotFound = errors.New("binder: custom binder not found, please be sure to enter the right name") +// NilValidatorError is the validate error when context.EnableValidate is called but no validator is set in config. +type NilValidatorError struct { +} // Format errors var ( @@ -40,37 +39,31 @@ var ( ErrNoHandlers = errors.New("format: at least one handler is required, but none were set") ) -// gorilla/schema errors -type ( - // ConversionError 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 = json.InvalidUnmarshalError +func (n NilValidatorError) Error() string { + return "fiber: ctx.EnableValidate(v any) is called without validator" +} - // A MarshalerError represents an error from calling a MarshalJSON or MarshalText method. - MarshalerError = json.MarshalerError +// InvalidBinderError is the error when try to bind invalid value. +type InvalidBinderError struct { + Type reflect.Type +} - // A SyntaxError is a description of a JSON syntax error. - SyntaxError = json.SyntaxError +func (e *InvalidBinderError) Error() string { + if e.Type == nil { + return "fiber: Bind(nil)" + } - // An UnmarshalTypeError describes a JSON value that was - // not appropriate for a value of a specific Go type. - UnmarshalTypeError = json.UnmarshalTypeError + if e.Type.Kind() != reflect.Pointer { + return "fiber: Unmarshal(non-pointer " + e.Type.String() + ")" + } + return "fiber: Bind(nil " + e.Type.String() + ")" +} - // An UnsupportedTypeError is returned by Marshal when attempting - // to encode an unsupported value type. - UnsupportedTypeError = json.UnsupportedTypeError +// UnsupportedBinderError is the error when try to bind unsupported type. +type UnsupportedBinderError struct { + Type reflect.Type +} - UnsupportedValueError = json.UnsupportedValueError -) +func (e *UnsupportedBinderError) Error() string { + return "unsupported binder: ctx.Bind().Req(" + e.Type.String() + "), only binding struct is supported new" +} diff --git a/error_test.go b/error_test.go deleted file mode 100644 index e2eace101c..0000000000 --- a/error_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package fiber - -import ( - "encoding/json" - "errors" - "testing" - - "github.com/gofiber/fiber/v3/internal/schema" - "github.com/stretchr/testify/require" -) - -func Test_ConversionError(t *testing.T) { - t.Parallel() - ok := errors.As(ConversionError{}, &schema.ConversionError{}) - require.True(t, ok) -} - -func Test_UnknownKeyError(t *testing.T) { - t.Parallel() - ok := errors.As(UnknownKeyError{}, &schema.UnknownKeyError{}) - require.True(t, ok) -} - -func Test_EmptyFieldError(t *testing.T) { - t.Parallel() - ok := errors.As(EmptyFieldError{}, &schema.EmptyFieldError{}) - require.True(t, ok) -} - -func Test_MultiError(t *testing.T) { - t.Parallel() - ok := errors.As(MultiError{}, &schema.MultiError{}) - require.True(t, ok) -} - -func Test_InvalidUnmarshalError(t *testing.T) { - t.Parallel() - var e *json.InvalidUnmarshalError - ok := errors.As(&InvalidUnmarshalError{}, &e) - require.True(t, ok) -} - -func Test_MarshalerError(t *testing.T) { - t.Parallel() - var e *json.MarshalerError - ok := errors.As(&MarshalerError{}, &e) - require.True(t, ok) -} - -func Test_SyntaxError(t *testing.T) { - t.Parallel() - var e *json.SyntaxError - ok := errors.As(&SyntaxError{}, &e) - require.True(t, ok) -} - -func Test_UnmarshalTypeError(t *testing.T) { - t.Parallel() - var e *json.UnmarshalTypeError - ok := errors.As(&UnmarshalTypeError{}, &e) - require.True(t, ok) -} - -func Test_UnsupportedTypeError(t *testing.T) { - t.Parallel() - var e *json.UnsupportedTypeError - ok := errors.As(&UnsupportedTypeError{}, &e) - require.True(t, ok) -} - -func Test_UnsupportedValeError(t *testing.T) { - t.Parallel() - var e *json.UnsupportedValueError - ok := errors.As(&UnsupportedValueError{}, &e) - require.True(t, ok) -} 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..c802ae6778 --- /dev/null +++ b/internal/bind/compile.go @@ -0,0 +1,53 @@ +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.PointerTo(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 + 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 +} 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/reflect.go b/internal/bind/reflect.go new file mode 100644 index 0000000000..bd4ee7ecdc --- /dev/null +++ b/internal/bind/reflect.go @@ -0,0 +1,18 @@ +package bind + +import ( + "reflect" + "unsafe" +) + +func ValueAndTypeID(v any) (reflect.Value, uintptr) { + header := (*emptyInterface)(unsafe.Pointer(&v)) + + rv := reflect.ValueOf(v) + return rv, header.typeID +} + +type emptyInterface struct { + typeID uintptr + dataPtr unsafe.Pointer +} 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/bind/string.go b/internal/bind/string.go new file mode 100644 index 0000000000..2ca242cb54 --- /dev/null +++ b/internal/bind/string.go @@ -0,0 +1,15 @@ +package bind + +import ( + "reflect" + + "github.com/gofiber/utils/v2" +) + +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..c97dd8de5a --- /dev/null +++ b/internal/bind/text_unmarshaler.go @@ -0,0 +1,29 @@ +package bind + +import ( + "encoding" + "reflect" + + "github.com/gofiber/utils/v2" +) + +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(utils.UnsafeBytes(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/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 85e28f174a..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 { - m map[reflect.Type]*structInfo - regconv map[reflect.Type]Converter - tag string - l sync.RWMutex -} - -// registerConverter registers a converter function for a custom type. -func (c *cache) registerConverter(value any, 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 310b783e38..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 any, 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 any, 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() - multiError := 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 { - multiError[path] = err - } - } else if !d.ignoreUnknownKeys { - multiError[path] = UnknownKeyError{Key: path} - } - } - multiError.merge(d.checkRequired(t, src)) - if len(multiError) > 0 { - return multiError - } - 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 { - Type reflect.Type // expected type of elem - Err error // low-level error (when it exists) - Key string // key from the source map. - Index int // index for multi-value fields; -1 for single-value fields. -} - -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 849d7c0fec..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 any, 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 any, 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/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 923ce5ce9a..1997813bd5 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -122,11 +122,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/logger_test.go b/middleware/logger/logger_test.go index 0bc06531c9..5eb406660f 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -221,7 +221,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() diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index 8baacfdc09..3679745446 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -99,12 +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 := 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+"="+strings.Join(v, ",")) } diff --git a/redirect.go b/redirect.go index ebbcb499b8..c471fe335b 100644 --- a/redirect.go +++ b/redirect.go @@ -8,9 +8,9 @@ import ( "errors" "sync" - "github.com/gofiber/fiber/v3/binder" "github.com/gofiber/utils/v2" "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" ) // Pool for redirection @@ -142,16 +142,23 @@ func (r *Redirect) With(key, value string, level ...uint8) *Redirect { func (r *Redirect) WithInput() *Redirect { // Get content-type ctype := utils.ToLower(utils.UnsafeString(r.c.Context().Request.Header.ContentType())) - ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype)) + ctype = filterFlags(utils.ParseVendorSpecificContentType(ctype)) oldInput := make(map[string]string) + switch ctype { case MIMEApplicationForm: - _ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed + oldInput = fasthttpArgsToMap(r.c.Context().PostArgs()) case MIMEMultipartForm: - _ = r.c.Bind().MultipartForm(oldInput) //nolint:errcheck // not needed + form, err := r.c.Context().MultipartForm() + if err != nil { + return r + } + for key, values := range form.Value { + oldInput[key] = values[0] + } default: - _ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed + oldInput = fasthttpArgsToMap(r.c.Context().QueryArgs()) } // Add old input data @@ -166,6 +173,25 @@ func (r *Redirect) WithInput() *Redirect { return r } +// 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 +} + +func fasthttpArgsToMap(v *fasthttp.Args) map[string]string { + var u = make(map[string]string) + v.VisitAll(func(key, value []byte) { + u[string(key)] = string(value) + }) + + return u +} + // Messages Get flash messages. func (r *Redirect) Messages() []FlashMessage { flashMessages := make([]FlashMessage, 0) 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 +}