mirror of https://github.com/gofiber/fiber.git
✨ v3 (feature): initial support for binding (#1981)
* ✨ v3 (feature): initial support for binding * ✨ v3 (feature): initial support for binding #1981 use pointer/references instead of copies * ✨ v3 (feature): initial support for binding embed bind into the ctx * ✨ v3 (feature): initial support for binding - add URI binder. * ✨ v3 (feature): initial support for binding - add response header binder. * ✨ v3 (feature): initial support for binding - add response header binder. * ✨ v3 (feature): initial support for binding - add cookie binder. * ✨ v3 (feature): initial support for binding - custom binder support for body binding. - test case for custom binder. * ✨ v3 (feature): initial support for binding - add map[string][]string & map[string]string support for binders. * ✨ v3 (feature): initial support for binding - fix Test_Bind_Header_Map * ✨ v3 (feature): initial support for binding - Functional Should/Must * ✨ v3 (feature): initial support for binding - custom struct validator support. * ✨ v3 (feature): initial support for binding - README for binding. - Docs for binding methods. * ✨ v3 (feature): initial support for binding - Bind() -> BindVars(), Binding() -> Bind() * ✨ v3 (feature): initial support for binding - fix doc problems * ✨ v3 (feature): initial support for binding - fix doc problems Co-authored-by: wernerr <rene@gofiber.io>pull/1979/head
parent
41159296f3
commit
eacde70294
27
app.go
27
app.go
|
@ -123,6 +123,8 @@ type App struct {
|
|||
latestGroup *Group
|
||||
// newCtxFunc
|
||||
newCtxFunc func(app *App) CustomCtx
|
||||
// custom binders
|
||||
customBinders []CustomBinder
|
||||
}
|
||||
|
||||
// Config is a struct holding the server settings.
|
||||
|
@ -366,6 +368,12 @@ type Config struct {
|
|||
// If set to true, will print all routes with their method, path and handler.
|
||||
// Default: false
|
||||
EnablePrintRoutes bool `json:"enable_print_routes"`
|
||||
|
||||
// If you want to validate header/form/query... automatically when to bind, you can define struct validator.
|
||||
// Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator.
|
||||
//
|
||||
// Default: nil
|
||||
StructValidator StructValidator
|
||||
}
|
||||
|
||||
// Static defines configuration options when defining static assets.
|
||||
|
@ -453,12 +461,13 @@ func New(config ...Config) *App {
|
|||
stack: make([][]*Route, len(intMethod)),
|
||||
treeStack: make([]map[string][]*Route, len(intMethod)),
|
||||
// Create config
|
||||
config: Config{},
|
||||
getBytes: utils.UnsafeBytes,
|
||||
getString: utils.UnsafeString,
|
||||
appList: make(map[string]*App),
|
||||
latestRoute: &Route{},
|
||||
latestGroup: &Group{},
|
||||
config: Config{},
|
||||
getBytes: utils.UnsafeBytes,
|
||||
getString: utils.UnsafeString,
|
||||
appList: make(map[string]*App),
|
||||
latestRoute: &Route{},
|
||||
latestGroup: &Group{},
|
||||
customBinders: []CustomBinder{},
|
||||
}
|
||||
|
||||
// Create Ctx pool
|
||||
|
@ -546,6 +555,12 @@ func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
|
|||
app.newCtxFunc = function
|
||||
}
|
||||
|
||||
// You can register custom binders to use as Bind().Custom("name").
|
||||
// They should be compatible with CustomBinder interface.
|
||||
func (app *App) RegisterCustomBinder(binder CustomBinder) {
|
||||
app.customBinders = append(app.customBinders, binder)
|
||||
}
|
||||
|
||||
// Mount attaches another app instance as a sub-router along a routing path.
|
||||
// It's very useful to split up a large API as many independent routers and
|
||||
// compose them as a single service using Mount. The fiber's error handler and
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
package fiber
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/binder"
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
)
|
||||
|
||||
// An interface to register custom binders.
|
||||
type CustomBinder interface {
|
||||
Name() string
|
||||
MIMETypes() []string
|
||||
Parse(Ctx, any) error
|
||||
}
|
||||
|
||||
// An interface to register custom struct validator for binding.
|
||||
type StructValidator interface {
|
||||
Engine() any
|
||||
ValidateStruct(any) error
|
||||
}
|
||||
|
||||
// Bind struct
|
||||
type Bind struct {
|
||||
ctx *DefaultCtx
|
||||
should bool
|
||||
}
|
||||
|
||||
// To handle binder errors manually, you can prefer Should method.
|
||||
// It's default behavior of binder.
|
||||
func (b *Bind) Should() *Bind {
|
||||
b.should = true
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// If you want to handle binder errors automatically, you can use Must.
|
||||
// If there's an error it'll return error and 400 as HTTP status.
|
||||
func (b *Bind) Must() *Bind {
|
||||
b.should = false
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Check Should/Must errors and return it by usage.
|
||||
func (b *Bind) returnErr(err error) error {
|
||||
if !b.should {
|
||||
b.ctx.Status(StatusBadRequest)
|
||||
return NewErrors(StatusBadRequest, "Bad request: "+err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Struct validation.
|
||||
func (b *Bind) validateStruct(out any) error {
|
||||
validator := b.ctx.app.config.StructValidator
|
||||
if validator != nil {
|
||||
return validator.ValidateStruct(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// To use custom binders, you have to use this method.
|
||||
// You can register them from RegisterCustomBinder method of Fiber instance.
|
||||
// They're checked by name, if it's not found, it will return an error.
|
||||
// NOTE: Should/Must is still valid for Custom binders.
|
||||
func (b *Bind) Custom(name string, dest any) error {
|
||||
binders := b.ctx.App().customBinders
|
||||
for _, binder := range binders {
|
||||
if binder.Name() == name {
|
||||
return b.returnErr(binder.Parse(b.ctx, dest))
|
||||
}
|
||||
}
|
||||
|
||||
return ErrCustomBinderNotFound
|
||||
}
|
||||
|
||||
// Header binds the request header strings into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) Header(out any) error {
|
||||
if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) RespHeader(out any) error {
|
||||
if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// Cookie binds the requesr cookie strings into the struct, map[string]string and map[string][]string.
|
||||
// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie.
|
||||
func (b *Bind) Cookie(out any) error {
|
||||
if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.Context(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// QueryParser binds the query string into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) Query(out any) error {
|
||||
if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.Context(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// JSON binds the body string into the struct.
|
||||
func (b *Bind) JSON(out any) error {
|
||||
if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// XML binds the body string into the struct.
|
||||
func (b *Bind) XML(out any) error {
|
||||
if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// Form binds the form into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) Form(out any) error {
|
||||
if err := b.returnErr(binder.FormBinder.Bind(b.ctx.Context(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// URI binds the route parameters into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) URI(out any) error {
|
||||
if err := b.returnErr(binder.URIBinder.Bind(b.ctx.route.Params, b.ctx.Params, out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string.
|
||||
func (b *Bind) MultipartForm(out any) error {
|
||||
if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), out)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.validateStruct(out)
|
||||
}
|
||||
|
||||
// Body binds the request body into the struct, map[string]string and map[string][]string.
|
||||
// It supports decoding the following content types based on the Content-Type header:
|
||||
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
|
||||
// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder.
|
||||
// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error.
|
||||
func (b *Bind) Body(out any) error {
|
||||
// Get content-type
|
||||
ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType()))
|
||||
ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))
|
||||
|
||||
// Parse body accordingly
|
||||
switch ctype {
|
||||
case MIMEApplicationJSON:
|
||||
return b.JSON(out)
|
||||
case MIMETextXML, MIMEApplicationXML:
|
||||
return b.XML(out)
|
||||
case MIMEApplicationForm:
|
||||
return b.Form(out)
|
||||
case MIMEMultipartForm:
|
||||
return b.MultipartForm(out)
|
||||
}
|
||||
|
||||
// Check custom binders
|
||||
binders := b.ctx.App().customBinders
|
||||
for _, binder := range binders {
|
||||
for _, mime := range binder.MIMETypes() {
|
||||
if mime == ctype {
|
||||
return b.returnErr(binder.Parse(b.ctx, out))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No suitable content type found
|
||||
return ErrUnprocessableEntity
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,194 @@
|
|||
# Fiber Binders
|
||||
|
||||
Binder is new request/response binding feature for Fiber. By aganist old Fiber parsers, it supports custom binder registration, struct validation, **map[string]string**, **map[string][]string** and more. It's introduced in Fiber v3 and a replacement of:
|
||||
- BodyParser
|
||||
- ParamsParser
|
||||
- GetReqHeaders
|
||||
- GetRespHeaders
|
||||
- AllParams
|
||||
- QueryParser
|
||||
- ReqHeaderParser
|
||||
|
||||
|
||||
## Default Binders
|
||||
- [Form](form.go)
|
||||
- [Query](query.go)
|
||||
- [URI](uri.go)
|
||||
- [Header](header.go)
|
||||
- [Response Header](resp_header.go)
|
||||
- [Cookie](cookie.go)
|
||||
- [JSON](json.go)
|
||||
- [XML](xml.go)
|
||||
|
||||
## Guides
|
||||
|
||||
### Binding into the Struct
|
||||
Fiber supports binding into the struct with [gorilla/schema](https://github.com/gorilla/schema). Here's an example for it:
|
||||
```go
|
||||
// Field names should start with an uppercase letter
|
||||
type Person struct {
|
||||
Name string `json:"name" xml:"name" form:"name"`
|
||||
Pass string `json:"pass" xml:"pass" form:"pass"`
|
||||
}
|
||||
|
||||
app.Post("/", func(c fiber.Ctx) error {
|
||||
p := new(Person)
|
||||
|
||||
if err := c.Bind().Body(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println(p.Name) // john
|
||||
log.Println(p.Pass) // doe
|
||||
|
||||
// ...
|
||||
})
|
||||
|
||||
// Run tests with the following curl commands:
|
||||
|
||||
// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000
|
||||
|
||||
// curl -X POST -H "Content-Type: application/xml" --data "<login><name>john</name><pass>doe</pass></login>" localhost:3000
|
||||
|
||||
// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000
|
||||
|
||||
// curl -X POST -F name=john -F pass=doe http://localhost:3000
|
||||
|
||||
// curl -X POST "http://localhost:3000/?name=john&pass=doe"
|
||||
```
|
||||
|
||||
### Binding into the Map
|
||||
Fiber supports binding into the **map[string]string** or **map[string][]string**. Here's an example for it:
|
||||
```go
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
p := make(map[string][]string)
|
||||
|
||||
if err := c.Bind().Query(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println(p["name"]) // john
|
||||
log.Println(p["pass"]) // doe
|
||||
log.Println(p["products"]) // [shoe, hat]
|
||||
|
||||
// ...
|
||||
})
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat"
|
||||
```
|
||||
### Behaviors of Should/Must
|
||||
Normally, Fiber returns binder error directly. However; if you want to handle it automatically, you can prefer `Must()`.
|
||||
|
||||
If there's an error it'll return error and 400 as HTTP status. Here's an example for it:
|
||||
```go
|
||||
// Field names should start with an uppercase letter
|
||||
type Person struct {
|
||||
Name string `json:"name,required"`
|
||||
Pass string `json:"pass"`
|
||||
}
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
p := new(Person)
|
||||
|
||||
if err := c.Bind().Must().JSON(p); err != nil {
|
||||
return err
|
||||
// Status code: 400
|
||||
// Response: Bad request: name is empty
|
||||
}
|
||||
|
||||
// ...
|
||||
})
|
||||
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl -X GET -H "Content-Type: application/json" --data "{\"pass\":\"doe\"}" localhost:3000
|
||||
```
|
||||
### Defining Custom Binder
|
||||
We didn't add much binder to make Fiber codebase minimal. But if you want to use your binders, it's easy to register and use them. Here's an example for TOML binder.
|
||||
```go
|
||||
type Person struct {
|
||||
Name string `toml:"name"`
|
||||
Pass string `toml:"pass"`
|
||||
}
|
||||
|
||||
type tomlBinding struct{}
|
||||
|
||||
func (b *tomlBinding) Name() string {
|
||||
return "toml"
|
||||
}
|
||||
|
||||
func (b *tomlBinding) MIMETypes() []string {
|
||||
return []string{"application/toml"}
|
||||
}
|
||||
|
||||
func (b *tomlBinding) Parse(c fiber.Ctx, out any) error {
|
||||
return toml.Unmarshal(c.Body(), out)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := fiber.New()
|
||||
app.RegisterCustomBinder(&tomlBinding{})
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
out := new(Person)
|
||||
if err := c.Bind().Body(out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// or you can use like:
|
||||
// if err := c.Bind().Custom("toml", out); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return c.SendString(out.Pass) // test
|
||||
})
|
||||
|
||||
app.Listen(":3000")
|
||||
}
|
||||
|
||||
// curl -X GET -H "Content-Type: application/toml" --data "name = 'bar'
|
||||
// pass = 'test'" localhost:3000
|
||||
```
|
||||
### Defining Custom Validator
|
||||
All Fiber binders supporting struct validation if you defined validator inside of the config. You can create own validator, or use [go-playground/validator](https://github.com/go-playground/validator), [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)... Here's an example of simple custom validator:
|
||||
```go
|
||||
type Query struct {
|
||||
Name string `query:"name"`
|
||||
}
|
||||
|
||||
type structValidator struct{}
|
||||
|
||||
func (v *structValidator) Engine() any {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *structValidator) ValidateStruct(out any) error {
|
||||
out = reflect.ValueOf(out).Elem().Interface()
|
||||
sq := out.(Query)
|
||||
|
||||
if sq.Name != "john" {
|
||||
return errors.New("you should have entered right name!")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := fiber.New(fiber.Config{StructValidator: &structValidator{}})
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
out := new(Query)
|
||||
if err := c.Bind().Query(out); err != nil {
|
||||
return err // you should have entered right name!
|
||||
}
|
||||
return c.SendString(out.Name)
|
||||
})
|
||||
|
||||
app.Listen(":3000")
|
||||
}
|
||||
|
||||
// Run tests with the following curl command:
|
||||
|
||||
// curl "http://localhost:3000/?name=efe"
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
package binder
|
||||
|
||||
import "errors"
|
||||
|
||||
// Binder errors
|
||||
var (
|
||||
ErrSuitableContentNotFound = errors.New("binder: suitable content not found to parse body")
|
||||
ErrMapNotConvertable = errors.New("binder: map is not convertable to map[string]string or map[string][]string")
|
||||
)
|
||||
|
||||
// Init default binders for Fiber
|
||||
var HeaderBinder = &headerBinding{}
|
||||
var RespHeaderBinder = &respHeaderBinding{}
|
||||
var CookieBinder = &cookieBinding{}
|
||||
var QueryBinder = &queryBinding{}
|
||||
var FormBinder = &formBinding{}
|
||||
var URIBinder = &uriBinding{}
|
||||
var XMLBinder = &xmlBinding{}
|
||||
var JSONBinder = &jsonBinding{}
|
|
@ -0,0 +1,45 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type cookieBinding struct{}
|
||||
|
||||
func (*cookieBinding) Name() string {
|
||||
return "cookie"
|
||||
}
|
||||
|
||||
func (b *cookieBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.Request.Header.VisitAllCookie(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type formBinding struct{}
|
||||
|
||||
func (*formBinding) Name() string {
|
||||
return "form"
|
||||
}
|
||||
|
||||
func (b *formBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.PostArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(k, "[") {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
})
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
||||
|
||||
func (b *formBinding) BindMultipart(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
data, err := reqCtx.MultipartForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parse(b.Name(), out, data.Value)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type headerBinding struct{}
|
||||
|
||||
func (*headerBinding) Name() string {
|
||||
return "header"
|
||||
}
|
||||
|
||||
func (b *headerBinding) Bind(req *fasthttp.Request, out any) error {
|
||||
data := make(map[string][]string)
|
||||
req.Header.VisitAll(func(key, val []byte) {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
})
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
)
|
||||
|
||||
type jsonBinding struct{}
|
||||
|
||||
func (*jsonBinding) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
func (b *jsonBinding) Bind(body []byte, jsonDecoder utils.JSONUnmarshal, out any) error {
|
||||
return jsonDecoder(body, out)
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gofiber/fiber/v3/internal/schema"
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// ParserConfig form decoder config for SetParserDecoder
|
||||
type ParserConfig struct {
|
||||
IgnoreUnknownKeys bool
|
||||
SetAliasTag string
|
||||
ParserType []ParserType
|
||||
ZeroEmpty bool
|
||||
}
|
||||
|
||||
// ParserType require two element, type and converter for register.
|
||||
// Use ParserType with BodyParser for parsing custom type in form data.
|
||||
type ParserType struct {
|
||||
Customtype any
|
||||
Converter func(string) reflect.Value
|
||||
}
|
||||
|
||||
// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance
|
||||
var decoderPool = &sync.Pool{New: func() any {
|
||||
return decoderBuilder(ParserConfig{
|
||||
IgnoreUnknownKeys: true,
|
||||
ZeroEmpty: true,
|
||||
})
|
||||
}}
|
||||
|
||||
// SetParserDecoder allow globally change the option of form decoder, update decoderPool
|
||||
func SetParserDecoder(parserConfig ParserConfig) {
|
||||
decoderPool = &sync.Pool{New: func() any {
|
||||
return decoderBuilder(parserConfig)
|
||||
}}
|
||||
}
|
||||
|
||||
func decoderBuilder(parserConfig ParserConfig) any {
|
||||
decoder := schema.NewDecoder()
|
||||
decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys)
|
||||
if parserConfig.SetAliasTag != "" {
|
||||
decoder.SetAliasTag(parserConfig.SetAliasTag)
|
||||
}
|
||||
for _, v := range parserConfig.ParserType {
|
||||
decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter)
|
||||
}
|
||||
decoder.ZeroEmpty(parserConfig.ZeroEmpty)
|
||||
return decoder
|
||||
}
|
||||
|
||||
// parse data into the map or struct
|
||||
func parse(aliasTag string, out any, data map[string][]string) error {
|
||||
ptrVal := reflect.ValueOf(out)
|
||||
|
||||
// Get pointer value
|
||||
var ptr any
|
||||
if ptrVal.Kind() == reflect.Ptr {
|
||||
ptrVal = ptrVal.Elem()
|
||||
ptr = ptrVal.Interface()
|
||||
}
|
||||
|
||||
// Parse into the map
|
||||
if ptrVal.Kind() == reflect.Map && ptrVal.Type().Key().Kind() == reflect.String {
|
||||
return parseToMap(ptr, data)
|
||||
}
|
||||
|
||||
// Parse into the struct
|
||||
return parseToStruct(aliasTag, out, data)
|
||||
}
|
||||
|
||||
// Parse data into the struct with gorilla/schema
|
||||
func parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
||||
// Get decoder from pool
|
||||
schemaDecoder := decoderPool.Get().(*schema.Decoder)
|
||||
defer decoderPool.Put(schemaDecoder)
|
||||
|
||||
// Set alias tag
|
||||
schemaDecoder.SetAliasTag(aliasTag)
|
||||
|
||||
return schemaDecoder.Decode(out, data)
|
||||
}
|
||||
|
||||
// Parse data into the map
|
||||
// thanks to https://github.com/gin-gonic/gin/blob/master/binding/binding.go
|
||||
func parseToMap(ptr any, data map[string][]string) error {
|
||||
elem := reflect.TypeOf(ptr).Elem()
|
||||
|
||||
// map[string][]string
|
||||
if elem.Kind() == reflect.Slice {
|
||||
newMap, ok := ptr.(map[string][]string)
|
||||
if !ok {
|
||||
return ErrMapNotConvertable
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
newMap[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// map[string]string
|
||||
newMap, ok := ptr.(map[string]string)
|
||||
if !ok {
|
||||
return ErrMapNotConvertable
|
||||
}
|
||||
|
||||
for k, v := range data {
|
||||
newMap[k] = v[len(v)-1]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseParamSquareBrackets(k string) (string, error) {
|
||||
bb := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(bb)
|
||||
|
||||
kbytes := []byte(k)
|
||||
|
||||
for i, b := range kbytes {
|
||||
|
||||
if b == '[' && kbytes[i+1] != ']' {
|
||||
if err := bb.WriteByte('.'); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if b == '[' || b == ']' {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := bb.WriteByte(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return bb.String(), nil
|
||||
}
|
||||
|
||||
func equalFieldType(out any, kind reflect.Kind, key string) bool {
|
||||
// Get type of interface
|
||||
outTyp := reflect.TypeOf(out).Elem()
|
||||
key = utils.ToLower(key)
|
||||
|
||||
// Support maps
|
||||
if outTyp.Kind() == reflect.Map && outTyp.Key().Kind() == reflect.String {
|
||||
return true
|
||||
}
|
||||
|
||||
// Must be a struct to match a field
|
||||
if outTyp.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
// Copy interface to an value to be used
|
||||
outVal := reflect.ValueOf(out).Elem()
|
||||
// Loop over each field
|
||||
for i := 0; i < outTyp.NumField(); i++ {
|
||||
// Get field value data
|
||||
structField := outVal.Field(i)
|
||||
// Can this field be changed?
|
||||
if !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Get field key data
|
||||
typeField := outTyp.Field(i)
|
||||
// Get type of field key
|
||||
structFieldKind := structField.Kind()
|
||||
// Does the field type equals input?
|
||||
if structFieldKind != kind {
|
||||
continue
|
||||
}
|
||||
// Get tag from field if exist
|
||||
inputFieldName := typeField.Tag.Get(QueryBinder.Name())
|
||||
if inputFieldName == "" {
|
||||
inputFieldName = typeField.Name
|
||||
} else {
|
||||
inputFieldName = strings.Split(inputFieldName, ",")[0]
|
||||
}
|
||||
// Compare field/tag with provided key
|
||||
if utils.ToLower(inputFieldName) == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Get content type from content type header
|
||||
func FilterFlags(content string) string {
|
||||
for i, char := range content {
|
||||
if char == ' ' || char == ';' {
|
||||
return content[:i]
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
)
|
||||
|
||||
func Test_EqualFieldType(t *testing.T) {
|
||||
var out int
|
||||
utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key"))
|
||||
|
||||
var dummy struct{ f string }
|
||||
utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key"))
|
||||
|
||||
var dummy2 struct{ f string }
|
||||
utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f"))
|
||||
|
||||
var user struct {
|
||||
Name string
|
||||
Address string `query:"address"`
|
||||
Age int `query:"AGE"`
|
||||
}
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age"))
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type queryBinding struct{}
|
||||
|
||||
func (*queryBinding) Name() string {
|
||||
return "query"
|
||||
}
|
||||
|
||||
func (b *queryBinding) Bind(reqCtx *fasthttp.RequestCtx, out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
reqCtx.QueryArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(k, "[") {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type respHeaderBinding struct{}
|
||||
|
||||
func (*respHeaderBinding) Name() string {
|
||||
return "respHeader"
|
||||
}
|
||||
|
||||
func (b *respHeaderBinding) Bind(resp *fasthttp.Response, out any) error {
|
||||
data := make(map[string][]string)
|
||||
resp.Header.VisitAll(func(key, val []byte) {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
})
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package binder
|
||||
|
||||
type uriBinding struct{}
|
||||
|
||||
func (*uriBinding) Name() string {
|
||||
return "uri"
|
||||
}
|
||||
|
||||
func (b *uriBinding) Bind(params []string, paramsFunc func(key string, defaultValue ...string) string, out any) error {
|
||||
data := make(map[string][]string, len(params))
|
||||
for _, param := range params {
|
||||
data[param] = append(data[param], paramsFunc(param))
|
||||
}
|
||||
|
||||
return parse(b.Name(), out, data)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package binder
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
type xmlBinding struct{}
|
||||
|
||||
func (*xmlBinding) Name() string {
|
||||
return "xml"
|
||||
}
|
||||
|
||||
func (b *xmlBinding) Bind(body []byte, out any) error {
|
||||
return xml.Unmarshal(body, out)
|
||||
}
|
274
ctx.go
274
ctx.go
|
@ -9,21 +9,18 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3/internal/schema"
|
||||
"github.com/gofiber/fiber/v3/utils"
|
||||
"github.com/savsgio/dictpool"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
|
@ -33,13 +30,6 @@ import (
|
|||
// maxParams defines the maximum number of parameters per route.
|
||||
const maxParams = 30
|
||||
|
||||
// Some constants for BodyParser, QueryParser and ReqHeaderParser.
|
||||
const (
|
||||
queryTag = "query"
|
||||
reqHeaderTag = "reqHeader"
|
||||
bodyTag = "form"
|
||||
)
|
||||
|
||||
// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
|
||||
const userContextKey = "__local_user_context__"
|
||||
|
||||
|
@ -61,6 +51,7 @@ type DefaultCtx struct {
|
|||
fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx
|
||||
matched bool // Non use route matched
|
||||
viewBindMap *dictpool.Dict // Default view map to bind template engine
|
||||
bind *Bind // Default bind reference
|
||||
}
|
||||
|
||||
// Range data for c.Range
|
||||
|
@ -92,21 +83,6 @@ type Views interface {
|
|||
Render(io.Writer, string, any, ...string) error
|
||||
}
|
||||
|
||||
// ParserType require two element, type and converter for register.
|
||||
// Use ParserType with BodyParser for parsing custom type in form data.
|
||||
type ParserType struct {
|
||||
Customtype any
|
||||
Converter func(string) reflect.Value
|
||||
}
|
||||
|
||||
// ParserConfig form decoder config for SetParserDecoder
|
||||
type ParserConfig struct {
|
||||
IgnoreUnknownKeys bool
|
||||
SetAliasTag string
|
||||
ParserType []ParserType
|
||||
ZeroEmpty bool
|
||||
}
|
||||
|
||||
// Accepts checks if the specified extensions or content types are acceptable.
|
||||
func (c *DefaultCtx) Accepts(offers ...string) string {
|
||||
if len(offers) == 0 {
|
||||
|
@ -259,91 +235,6 @@ func (c *DefaultCtx) Body() []byte {
|
|||
return body
|
||||
}
|
||||
|
||||
// decoderPool helps to improve BodyParser's, QueryParser's and ReqHeaderParser's performance
|
||||
var decoderPool = &sync.Pool{New: func() any {
|
||||
return decoderBuilder(ParserConfig{
|
||||
IgnoreUnknownKeys: true,
|
||||
ZeroEmpty: true,
|
||||
})
|
||||
}}
|
||||
|
||||
// SetParserDecoder allow globally change the option of form decoder, update decoderPool
|
||||
func SetParserDecoder(parserConfig ParserConfig) {
|
||||
decoderPool = &sync.Pool{New: func() any {
|
||||
return decoderBuilder(parserConfig)
|
||||
}}
|
||||
}
|
||||
|
||||
func decoderBuilder(parserConfig ParserConfig) any {
|
||||
decoder := schema.NewDecoder()
|
||||
decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys)
|
||||
if parserConfig.SetAliasTag != "" {
|
||||
decoder.SetAliasTag(parserConfig.SetAliasTag)
|
||||
}
|
||||
for _, v := range parserConfig.ParserType {
|
||||
decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter)
|
||||
}
|
||||
decoder.ZeroEmpty(parserConfig.ZeroEmpty)
|
||||
return decoder
|
||||
}
|
||||
|
||||
// BodyParser binds the request body to a struct.
|
||||
// It supports decoding the following content types based on the Content-Type header:
|
||||
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
|
||||
// If none of the content types above are matched, it will return a ErrUnprocessableEntity error
|
||||
func (c *DefaultCtx) BodyParser(out any) error {
|
||||
// Get content-type
|
||||
ctype := utils.ToLower(utils.UnsafeString(c.fasthttp.Request.Header.ContentType()))
|
||||
|
||||
ctype = utils.ParseVendorSpecificContentType(ctype)
|
||||
|
||||
// Parse body accordingly
|
||||
if strings.HasPrefix(ctype, MIMEApplicationJSON) {
|
||||
return c.app.config.JSONDecoder(c.Body(), out)
|
||||
}
|
||||
if strings.HasPrefix(ctype, MIMEApplicationForm) {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
c.fasthttp.PostArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(k, "[") {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return c.parseToStruct(bodyTag, out, data)
|
||||
}
|
||||
if strings.HasPrefix(ctype, MIMEMultipartForm) {
|
||||
data, err := c.fasthttp.MultipartForm()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.parseToStruct(bodyTag, out, data.Value)
|
||||
}
|
||||
if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) {
|
||||
return xml.Unmarshal(c.Body(), out)
|
||||
}
|
||||
// No suitable content type found
|
||||
return ErrUnprocessableEntity
|
||||
}
|
||||
|
||||
// ClearCookie expires a specific cookie by key on the client side.
|
||||
// If no key is provided it expires all cookies that came with the request.
|
||||
func (c *DefaultCtx) ClearCookie(key ...string) {
|
||||
|
@ -571,30 +462,6 @@ func (c *DefaultCtx) GetRespHeader(key string, defaultValue ...string) string {
|
|||
return defaultString(c.app.getString(c.fasthttp.Response.Header.Peek(key)), defaultValue)
|
||||
}
|
||||
|
||||
// GetReqHeaders returns the HTTP request headers.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
func (c *DefaultCtx) GetReqHeaders() map[string]string {
|
||||
headers := make(map[string]string)
|
||||
c.Request().Header.VisitAll(func(k, v []byte) {
|
||||
headers[string(k)] = c.app.getString(v)
|
||||
})
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// GetRespHeaders returns the HTTP response headers.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
func (c *DefaultCtx) GetRespHeaders() map[string]string {
|
||||
headers := make(map[string]string)
|
||||
c.Response().Header.VisitAll(func(k, v []byte) {
|
||||
headers[string(k)] = c.app.getString(v)
|
||||
})
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
|
@ -805,17 +672,6 @@ func (c *DefaultCtx) Params(key string, defaultValue ...string) string {
|
|||
return defaultString("", defaultValue)
|
||||
}
|
||||
|
||||
// Params is used to get all route parameters.
|
||||
// Using Params method to get params.
|
||||
func (c *DefaultCtx) AllParams() map[string]string {
|
||||
params := make(map[string]string, len(c.route.Params))
|
||||
for _, param := range c.route.Params {
|
||||
params[param] = c.Params(param)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
// ParamsInt is used to get an integer from the route parameters
|
||||
// it defaults to zero if the parameter is not found or if the
|
||||
// parameter cannot be converted to an integer
|
||||
|
@ -892,41 +748,6 @@ func (c *DefaultCtx) Query(key string, defaultValue ...string) string {
|
|||
return defaultString(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), defaultValue)
|
||||
}
|
||||
|
||||
// QueryParser binds the query string to a struct.
|
||||
func (c *DefaultCtx) QueryParser(out any) error {
|
||||
data := make(map[string][]string)
|
||||
var err error
|
||||
|
||||
c.fasthttp.QueryArgs().VisitAll(func(key, val []byte) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(k, "[") {
|
||||
k, err = parseParamSquareBrackets(k)
|
||||
}
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.parseToStruct(queryTag, out, data)
|
||||
}
|
||||
|
||||
func parseParamSquareBrackets(k string) (string, error) {
|
||||
bb := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(bb)
|
||||
|
@ -953,84 +774,6 @@ func parseParamSquareBrackets(k string) (string, error) {
|
|||
return bb.String(), nil
|
||||
}
|
||||
|
||||
// ReqHeaderParser binds the request header strings to a struct.
|
||||
func (c *DefaultCtx) ReqHeaderParser(out any) error {
|
||||
data := make(map[string][]string)
|
||||
c.fasthttp.Request.Header.VisitAll(func(key, val []byte) {
|
||||
k := utils.UnsafeString(key)
|
||||
v := utils.UnsafeString(val)
|
||||
|
||||
if strings.Contains(v, ",") && equalFieldType(out, reflect.Slice, k) {
|
||||
values := strings.Split(v, ",")
|
||||
for i := 0; i < len(values); i++ {
|
||||
data[k] = append(data[k], values[i])
|
||||
}
|
||||
} else {
|
||||
data[k] = append(data[k], v)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return c.parseToStruct(reqHeaderTag, out, data)
|
||||
}
|
||||
|
||||
func (c *DefaultCtx) parseToStruct(aliasTag string, out any, data map[string][]string) error {
|
||||
// Get decoder from pool
|
||||
schemaDecoder := decoderPool.Get().(*schema.Decoder)
|
||||
defer decoderPool.Put(schemaDecoder)
|
||||
|
||||
// Set alias tag
|
||||
schemaDecoder.SetAliasTag(aliasTag)
|
||||
|
||||
return schemaDecoder.Decode(out, data)
|
||||
}
|
||||
|
||||
func equalFieldType(out any, kind reflect.Kind, key string) bool {
|
||||
// Get type of interface
|
||||
outTyp := reflect.TypeOf(out).Elem()
|
||||
key = utils.ToLower(key)
|
||||
// Must be a struct to match a field
|
||||
if outTyp.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
// Copy interface to an value to be used
|
||||
outVal := reflect.ValueOf(out).Elem()
|
||||
// Loop over each field
|
||||
for i := 0; i < outTyp.NumField(); i++ {
|
||||
// Get field value data
|
||||
structField := outVal.Field(i)
|
||||
// Can this field be changed?
|
||||
if !structField.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Get field key data
|
||||
typeField := outTyp.Field(i)
|
||||
// Get type of field key
|
||||
structFieldKind := structField.Kind()
|
||||
// Does the field type equals input?
|
||||
if structFieldKind != kind {
|
||||
continue
|
||||
}
|
||||
// Get tag from field if exist
|
||||
inputFieldName := typeField.Tag.Get(queryTag)
|
||||
if inputFieldName == "" {
|
||||
inputFieldName = typeField.Name
|
||||
} else {
|
||||
inputFieldName = strings.Split(inputFieldName, ",")[0]
|
||||
}
|
||||
// Compare field/tag with provided key
|
||||
if utils.ToLower(inputFieldName) == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRangeMalformed = errors.New("range: malformed range header string")
|
||||
ErrRangeUnsatisfiable = errors.New("range: unsatisfiable range")
|
||||
)
|
||||
|
||||
// Range returns a struct containing the type and a slice of ranges.
|
||||
func (c *DefaultCtx) Range(size int) (rangeData Range, err error) {
|
||||
rangeStr := c.Get(HeaderRange)
|
||||
|
@ -1095,7 +838,7 @@ func (c *DefaultCtx) Redirect(location string, status ...int) error {
|
|||
|
||||
// Add vars to default view var map binding to template engine.
|
||||
// Variables are read by the Render method and may be overwritten.
|
||||
func (c *DefaultCtx) Bind(vars Map) error {
|
||||
func (c *DefaultCtx) BindVars(vars Map) error {
|
||||
// init viewBindMap - lazy map
|
||||
if c.viewBindMap == nil {
|
||||
c.viewBindMap = dictpool.AcquireDict()
|
||||
|
@ -1576,3 +1319,16 @@ func (c *DefaultCtx) IsFromLocal() bool {
|
|||
}
|
||||
return c.isLocalHost(ips[0])
|
||||
}
|
||||
|
||||
// You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method.
|
||||
// It gives custom binding support, detailed binding options and more.
|
||||
// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser
|
||||
func (c *DefaultCtx) Bind() *Bind {
|
||||
if c.bind == nil {
|
||||
c.bind = &Bind{
|
||||
ctx: c,
|
||||
should: true,
|
||||
}
|
||||
}
|
||||
return c.bind
|
||||
}
|
||||
|
|
|
@ -46,12 +46,6 @@ type Ctx interface {
|
|||
// Make copies or use the Immutable setting instead.
|
||||
Body() []byte
|
||||
|
||||
// BodyParser binds the request body to a struct.
|
||||
// It supports decoding the following content types based on the Content-Type header:
|
||||
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
|
||||
// If none of the content types above are matched, it will return a ErrUnprocessableEntity error
|
||||
BodyParser(out any) error
|
||||
|
||||
// ClearCookie expires a specific cookie by key on the client side.
|
||||
// If no key is provided it expires all cookies that came with the request.
|
||||
ClearCookie(key ...string)
|
||||
|
@ -128,16 +122,6 @@ type Ctx interface {
|
|||
// Make copies or use the Immutable setting instead.
|
||||
GetRespHeader(key string, defaultValue ...string) string
|
||||
|
||||
// GetReqHeaders returns the HTTP request headers.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
GetReqHeaders() map[string]string
|
||||
|
||||
// GetRespHeaders returns the HTTP response headers.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
GetRespHeaders() map[string]string
|
||||
|
||||
// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header.
|
||||
// Returned value is only valid within the handler. Do not store any references.
|
||||
// Make copies or use the Immutable setting instead.
|
||||
|
@ -206,10 +190,6 @@ type Ctx interface {
|
|||
// Make copies or use the Immutable setting to use the value outside the Handler.
|
||||
Params(key string, defaultValue ...string) string
|
||||
|
||||
// Params is used to get all route parameters.
|
||||
// Using Params method to get params.
|
||||
AllParams() map[string]string
|
||||
|
||||
// ParamsInt is used to get an integer from the route parameters
|
||||
// it defaults to zero if the parameter is not found or if the
|
||||
// parameter cannot be converted to an integer
|
||||
|
@ -235,12 +215,6 @@ type Ctx interface {
|
|||
// Make copies or use the Immutable setting to use the value outside the Handler.
|
||||
Query(key string, defaultValue ...string) string
|
||||
|
||||
// QueryParser binds the query string to a struct.
|
||||
QueryParser(out any) error
|
||||
|
||||
// ReqHeaderParser binds the request header strings to a struct.
|
||||
ReqHeaderParser(out any) error
|
||||
|
||||
// Range returns a struct containing the type and a slice of ranges.
|
||||
Range(size int) (rangeData Range, err error)
|
||||
|
||||
|
@ -250,7 +224,7 @@ type Ctx interface {
|
|||
|
||||
// Add vars to default view var map binding to template engine.
|
||||
// Variables are read by the Render method and may be overwritten.
|
||||
Bind(vars Map) error
|
||||
BindVars(vars Map) error
|
||||
|
||||
// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831"
|
||||
GetRouteURL(routeName string, params Map) (string, error)
|
||||
|
@ -350,6 +324,11 @@ type Ctx interface {
|
|||
// Reset is a method to reset context fields by given request when to use server handlers.
|
||||
Reset(fctx *fasthttp.RequestCtx)
|
||||
|
||||
// You can bind body, cookie, headers etc. into the map, map slice, struct easily by using Binding method.
|
||||
// It gives custom binding support, detailed binding options and more.
|
||||
// Replacement of: BodyParser, ParamsParser, GetReqHeaders, GetRespHeaders, AllParams, QueryParser, ReqHeaderParser
|
||||
Bind() *Bind
|
||||
|
||||
// SetReq resets fields of context that is relating to request.
|
||||
setReq(fctx *fasthttp.RequestCtx)
|
||||
|
||||
|
@ -451,6 +430,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) {
|
|||
func (c *DefaultCtx) release() {
|
||||
c.route = nil
|
||||
c.fasthttp = nil
|
||||
c.bind = nil
|
||||
if c.viewBindMap != nil {
|
||||
dictpool.ReleaseDict(c.viewBindMap)
|
||||
}
|
||||
|
|
899
ctx_test.go
899
ctx_test.go
|
@ -20,7 +20,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -378,229 +377,6 @@ func Benchmark_Ctx_Body_With_Compression(b *testing.B) {
|
|||
utils.AssertEqual(b, []byte("john=doe"), c.Body())
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_BodyParser
|
||||
func Test_Ctx_BodyParser(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Name string `json:"name" xml:"name" form:"name" query:"name"`
|
||||
}
|
||||
|
||||
{
|
||||
var gzipJSON bytes.Buffer
|
||||
w := gzip.NewWriter(&gzipJSON)
|
||||
_, _ = w.Write([]byte(`{"name":"john"}`))
|
||||
_ = w.Close()
|
||||
|
||||
c.Request().Header.SetContentType(MIMEApplicationJSON)
|
||||
c.Request().Header.Set(HeaderContentEncoding, "gzip")
|
||||
c.Request().SetBody(gzipJSON.Bytes())
|
||||
c.Request().Header.SetContentLength(len(gzipJSON.Bytes()))
|
||||
d := new(Demo)
|
||||
utils.AssertEqual(t, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(t, "john", d.Name)
|
||||
c.Request().Header.Del(HeaderContentEncoding)
|
||||
}
|
||||
|
||||
testDecodeParser := func(contentType, body string) {
|
||||
c.Request().Header.SetContentType(contentType)
|
||||
c.Request().SetBody([]byte(body))
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := new(Demo)
|
||||
utils.AssertEqual(t, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(t, "john", d.Name)
|
||||
}
|
||||
|
||||
testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`)
|
||||
testDecodeParser(MIMEApplicationXML, `<Demo><name>john</name></Demo>`)
|
||||
testDecodeParser(MIMEApplicationForm, "name=john")
|
||||
testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
|
||||
|
||||
testDecodeParserError := func(contentType, body string) {
|
||||
c.Request().Header.SetContentType(contentType)
|
||||
c.Request().SetBody([]byte(body))
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
utils.AssertEqual(t, false, c.BodyParser(nil) == nil)
|
||||
}
|
||||
|
||||
testDecodeParserError("invalid-content-type", "")
|
||||
testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b")
|
||||
|
||||
type CollectionQuery struct {
|
||||
Data []Demo `query:"data"`
|
||||
}
|
||||
|
||||
c.Request().Reset()
|
||||
c.Request().Header.SetContentType(MIMEApplicationForm)
|
||||
c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe"))
|
||||
c.Request().Header.SetContentLength(len(c.Body()))
|
||||
cq := new(CollectionQuery)
|
||||
utils.AssertEqual(t, nil, c.BodyParser(cq))
|
||||
utils.AssertEqual(t, 2, len(cq.Data))
|
||||
utils.AssertEqual(t, "john", cq.Data[0].Name)
|
||||
utils.AssertEqual(t, "doe", cq.Data[1].Name)
|
||||
|
||||
c.Request().Reset()
|
||||
c.Request().Header.SetContentType(MIMEApplicationForm)
|
||||
c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe"))
|
||||
c.Request().Header.SetContentLength(len(c.Body()))
|
||||
cq = new(CollectionQuery)
|
||||
utils.AssertEqual(t, nil, c.BodyParser(cq))
|
||||
utils.AssertEqual(t, 2, len(cq.Data))
|
||||
utils.AssertEqual(t, "john", cq.Data[0].Name)
|
||||
utils.AssertEqual(t, "doe", cq.Data[1].Name)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_BodyParser_WithSetParserDecoder
|
||||
func Test_Ctx_BodyParser_WithSetParserDecoder(t *testing.T) {
|
||||
type CustomTime time.Time
|
||||
|
||||
timeConverter := func(value string) reflect.Value {
|
||||
if v, err := time.Parse("2006-01-02", value); err == nil {
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
customTime := ParserType{
|
||||
Customtype: CustomTime{},
|
||||
Converter: timeConverter,
|
||||
}
|
||||
|
||||
SetParserDecoder(ParserConfig{
|
||||
IgnoreUnknownKeys: true,
|
||||
ParserType: []ParserType{customTime},
|
||||
ZeroEmpty: true,
|
||||
SetAliasTag: "form",
|
||||
})
|
||||
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Date CustomTime `form:"date"`
|
||||
Title string `form:"title"`
|
||||
Body string `form:"body"`
|
||||
}
|
||||
|
||||
testDecodeParser := func(contentType, body string) {
|
||||
c.Request().Header.SetContentType(contentType)
|
||||
c.Request().SetBody([]byte(body))
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := Demo{
|
||||
Title: "Existing title",
|
||||
Body: "Existing Body",
|
||||
}
|
||||
utils.AssertEqual(t, nil, c.BodyParser(&d))
|
||||
date := fmt.Sprintf("%v", d.Date)
|
||||
utils.AssertEqual(t, "{0 63743587200 <nil>}", date)
|
||||
utils.AssertEqual(t, "", d.Title)
|
||||
utils.AssertEqual(t, "New Body", d.Body)
|
||||
}
|
||||
|
||||
testDecodeParser(MIMEApplicationForm, "date=2020-12-15&title=&body=New Body")
|
||||
testDecodeParser(MIMEMultipartForm+`; boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"date\"\r\n\r\n2020-12-15\r\n--b\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n\r\n--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nNew Body\r\n--b--")
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON -benchmem -count=4
|
||||
func Benchmark_Ctx_BodyParser_JSON(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
body := []byte(`{"name":"john"}`)
|
||||
c.Request().SetBody(body)
|
||||
c.Request().Header.SetContentType(MIMEApplicationJSON)
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := new(Demo)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = c.BodyParser(d)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(b, "john", d.Name)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_XML -benchmem -count=4
|
||||
func Benchmark_Ctx_BodyParser_XML(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Name string `xml:"name"`
|
||||
}
|
||||
body := []byte("<Demo><name>john</name></Demo>")
|
||||
c.Request().SetBody(body)
|
||||
c.Request().Header.SetContentType(MIMEApplicationXML)
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := new(Demo)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = c.BodyParser(d)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(b, "john", d.Name)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_Form -benchmem -count=4
|
||||
func Benchmark_Ctx_BodyParser_Form(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Name string `form:"name"`
|
||||
}
|
||||
body := []byte("name=john")
|
||||
c.Request().SetBody(body)
|
||||
c.Request().Header.SetContentType(MIMEApplicationForm)
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := new(Demo)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = c.BodyParser(d)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(b, "john", d.Name)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_MultipartForm -benchmem -count=4
|
||||
func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Demo struct {
|
||||
Name string `form:"name"`
|
||||
}
|
||||
|
||||
body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--")
|
||||
c.Request().SetBody(body)
|
||||
c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`)
|
||||
c.Request().Header.SetContentLength(len(body))
|
||||
d := new(Demo)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_ = c.BodyParser(d)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.BodyParser(d))
|
||||
utils.AssertEqual(b, "john", d.Name)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_Context
|
||||
func Test_Ctx_Context(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -1365,44 +1141,6 @@ func Test_Ctx_Params(t *testing.T) {
|
|||
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
|
||||
}
|
||||
|
||||
// go test -race -run Test_Ctx_AllParams
|
||||
func Test_Ctx_AllParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
app.Get("/test/:user", func(c Ctx) error {
|
||||
utils.AssertEqual(t, map[string]string{"user": "john"}, c.AllParams())
|
||||
return nil
|
||||
})
|
||||
app.Get("/test2/*", func(c Ctx) error {
|
||||
utils.AssertEqual(t, map[string]string{"*1": "im/a/cookie"}, c.AllParams())
|
||||
return nil
|
||||
})
|
||||
app.Get("/test3/*/blafasel/*", func(c Ctx) error {
|
||||
utils.AssertEqual(t, map[string]string{"*1": "1111", "*2": "2222"}, c.AllParams())
|
||||
return nil
|
||||
})
|
||||
app.Get("/test4/:optional?", func(c Ctx) error {
|
||||
utils.AssertEqual(t, map[string]string{"optional": ""}, c.AllParams())
|
||||
return nil
|
||||
})
|
||||
|
||||
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil))
|
||||
utils.AssertEqual(t, nil, err, "app.Test(req)")
|
||||
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/im/a/cookie", nil))
|
||||
utils.AssertEqual(t, nil, err, "app.Test(req)")
|
||||
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/1111/blafasel/2222", nil))
|
||||
utils.AssertEqual(t, nil, err, "app.Test(req)")
|
||||
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
|
||||
|
||||
resp, err = app.Test(httptest.NewRequest(MethodGet, "/test4", nil))
|
||||
utils.AssertEqual(t, nil, err, "app.Test(req)")
|
||||
utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code")
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4
|
||||
func Benchmark_Ctx_Params(b *testing.B) {
|
||||
app := New()
|
||||
|
@ -1428,32 +1166,6 @@ func Benchmark_Ctx_Params(b *testing.B) {
|
|||
utils.AssertEqual(b, "awesome", res)
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_AllParams -benchmem -count=4
|
||||
func Benchmark_Ctx_AllParams(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{}).(*DefaultCtx)
|
||||
|
||||
c.route = &Route{
|
||||
Params: []string{
|
||||
"param1", "param2", "param3", "param4",
|
||||
},
|
||||
}
|
||||
c.values = [maxParams]string{
|
||||
"john", "doe", "is", "awesome",
|
||||
}
|
||||
var res map[string]string
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
res = c.AllParams()
|
||||
}
|
||||
utils.AssertEqual(b, map[string]string{"param1": "john",
|
||||
"param2": "doe",
|
||||
"param3": "is",
|
||||
"param4": "awesome"},
|
||||
res)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_Path
|
||||
func Test_Ctx_Path(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -2444,13 +2156,13 @@ func Test_Ctx_RenderWithLocals(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func Test_Ctx_RenderWithBind(t *testing.T) {
|
||||
func Test_Ctx_RenderWithBindVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Bind(Map{
|
||||
c.BindVars(Map{
|
||||
"Title": "Hello, World!",
|
||||
})
|
||||
|
||||
|
@ -2465,7 +2177,7 @@ func Test_Ctx_RenderWithBind(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func Test_Ctx_RenderWithBindLocals(t *testing.T) {
|
||||
func Test_Ctx_RenderWithBindVarsLocals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := New(Config{
|
||||
|
@ -2474,7 +2186,7 @@ func Test_Ctx_RenderWithBindLocals(t *testing.T) {
|
|||
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Bind(Map{
|
||||
c.BindVars(Map{
|
||||
"Title": "Hello, World!",
|
||||
})
|
||||
|
||||
|
@ -2507,7 +2219,7 @@ func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) {
|
|||
utils.AssertEqual(t, "<h1>Hello, World!</h1>", string(c.Response().Body()))
|
||||
}
|
||||
|
||||
func Benchmark_Ctx_RenderWithLocalsAndBinding(b *testing.B) {
|
||||
func Benchmark_Ctx_RenderWithLocalsAndBindVars(b *testing.B) {
|
||||
engine := &testTemplateEngine{}
|
||||
err := engine.Load()
|
||||
utils.AssertEqual(b, nil, err)
|
||||
|
@ -2517,7 +2229,7 @@ func Benchmark_Ctx_RenderWithLocalsAndBinding(b *testing.B) {
|
|||
})
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Bind(Map{
|
||||
c.BindVars(Map{
|
||||
"Title": "Hello, World!",
|
||||
})
|
||||
c.Locals("Summary", "Test")
|
||||
|
@ -2603,7 +2315,7 @@ func Benchmark_Ctx_RenderLocals(b *testing.B) {
|
|||
utils.AssertEqual(b, "<h1>Hello, World!</h1>", string(c.Response().Body()))
|
||||
}
|
||||
|
||||
func Benchmark_Ctx_RenderBind(b *testing.B) {
|
||||
func Benchmark_Ctx_RenderBindVars(b *testing.B) {
|
||||
engine := &testTemplateEngine{}
|
||||
err := engine.Load()
|
||||
utils.AssertEqual(b, nil, err)
|
||||
|
@ -2611,7 +2323,7 @@ func Benchmark_Ctx_RenderBind(b *testing.B) {
|
|||
app.config.Views = engine
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Bind(Map{
|
||||
c.BindVars(Map{
|
||||
"Title": "Hello, World!",
|
||||
})
|
||||
|
||||
|
@ -3150,569 +2862,6 @@ func Benchmark_Ctx_SendString_B(b *testing.B) {
|
|||
utils.AssertEqual(b, []byte("Hello, world!"), c.Response().Body())
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_QueryParser -v
|
||||
func Test_Ctx_QueryParser(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query struct {
|
||||
ID int
|
||||
Name string
|
||||
Hobby []string
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football")
|
||||
q := new(Query)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
utils.AssertEqual(t, 2, len(q.Hobby))
|
||||
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football")
|
||||
q = new(Query)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
utils.AssertEqual(t, 2, len(q.Hobby))
|
||||
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football")
|
||||
q = new(Query)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
utils.AssertEqual(t, 3, len(q.Hobby))
|
||||
|
||||
empty := new(Query)
|
||||
c.Request().URI().SetQueryString("")
|
||||
utils.AssertEqual(t, nil, c.QueryParser(empty))
|
||||
utils.AssertEqual(t, 0, len(empty.Hobby))
|
||||
|
||||
type Query2 struct {
|
||||
Bool bool
|
||||
ID int
|
||||
Name string
|
||||
Hobby string
|
||||
FavouriteDrinks []string
|
||||
Empty []string
|
||||
Alloc []string
|
||||
No []int64
|
||||
}
|
||||
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1")
|
||||
q2 := new(Query2)
|
||||
q2.Bool = true
|
||||
q2.Name = "hello world"
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q2))
|
||||
utils.AssertEqual(t, "basketball,football", q2.Hobby)
|
||||
utils.AssertEqual(t, true, q2.Bool)
|
||||
utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten
|
||||
utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks)
|
||||
var nilSlice []string
|
||||
utils.AssertEqual(t, nilSlice, q2.Empty)
|
||||
utils.AssertEqual(t, []string{""}, q2.Alloc)
|
||||
utils.AssertEqual(t, []int64{1}, q2.No)
|
||||
|
||||
type RequiredQuery struct {
|
||||
Name string `query:"name,required"`
|
||||
}
|
||||
rq := new(RequiredQuery)
|
||||
c.Request().URI().SetQueryString("")
|
||||
utils.AssertEqual(t, "name is empty", c.QueryParser(rq).Error())
|
||||
|
||||
type ArrayQuery struct {
|
||||
Data []string
|
||||
}
|
||||
aq := new(ArrayQuery)
|
||||
c.Request().URI().SetQueryString("data[]=john&data[]=doe")
|
||||
utils.AssertEqual(t, nil, c.QueryParser(aq))
|
||||
utils.AssertEqual(t, 2, len(aq.Data))
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_QueryParser_WithSetParserDecoder -v
|
||||
func Test_Ctx_QueryParser_WithSetParserDecoder(t *testing.T) {
|
||||
type NonRFCTime time.Time
|
||||
|
||||
NonRFCConverter := func(value string) reflect.Value {
|
||||
if v, err := time.Parse("2006-01-02", value); err == nil {
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
nonRFCTime := ParserType{
|
||||
Customtype: NonRFCTime{},
|
||||
Converter: NonRFCConverter,
|
||||
}
|
||||
|
||||
SetParserDecoder(ParserConfig{
|
||||
IgnoreUnknownKeys: true,
|
||||
ParserType: []ParserType{nonRFCTime},
|
||||
ZeroEmpty: true,
|
||||
SetAliasTag: "query",
|
||||
})
|
||||
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type NonRFCTimeInput struct {
|
||||
Date NonRFCTime `query:"date"`
|
||||
Title string `query:"title"`
|
||||
Body string `query:"body"`
|
||||
}
|
||||
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
q := new(NonRFCTimeInput)
|
||||
|
||||
c.Request().URI().SetQueryString("date=2021-04-10&title=CustomDateTest&Body=October")
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
fmt.Println(q.Date, "q.Date")
|
||||
utils.AssertEqual(t, "CustomDateTest", q.Title)
|
||||
date := fmt.Sprintf("%v", q.Date)
|
||||
utils.AssertEqual(t, "{0 63753609600 <nil>}", date)
|
||||
utils.AssertEqual(t, "October", q.Body)
|
||||
|
||||
c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October")
|
||||
q = &NonRFCTimeInput{
|
||||
Title: "Existing title",
|
||||
Body: "Existing Body",
|
||||
}
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
utils.AssertEqual(t, "", q.Title)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_QueryParser_Schema -v
|
||||
func Test_Ctx_QueryParser_Schema(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query1 struct {
|
||||
Name string `query:"name,required"`
|
||||
Nested struct {
|
||||
Age int `query:"age"`
|
||||
} `query:"nested,required"`
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
c.Request().URI().SetQueryString("name=tom&nested.age=10")
|
||||
q := new(Query1)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
|
||||
c.Request().URI().SetQueryString("namex=tom&nested.age=10")
|
||||
q = new(Query1)
|
||||
utils.AssertEqual(t, "name is empty", c.QueryParser(q).Error())
|
||||
|
||||
c.Request().URI().SetQueryString("name=tom&nested.agex=10")
|
||||
q = new(Query1)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q))
|
||||
|
||||
c.Request().URI().SetQueryString("name=tom&test.age=10")
|
||||
q = new(Query1)
|
||||
utils.AssertEqual(t, "nested is empty", c.QueryParser(q).Error())
|
||||
|
||||
type Query2 struct {
|
||||
Name string `query:"name"`
|
||||
Nested struct {
|
||||
Age int `query:"age,required"`
|
||||
} `query:"nested"`
|
||||
}
|
||||
c.Request().URI().SetQueryString("name=tom&nested.age=10")
|
||||
q2 := new(Query2)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q2))
|
||||
|
||||
c.Request().URI().SetQueryString("nested.age=10")
|
||||
q2 = new(Query2)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(q2))
|
||||
|
||||
c.Request().URI().SetQueryString("nested.agex=10")
|
||||
q2 = new(Query2)
|
||||
utils.AssertEqual(t, "nested.age is empty", c.QueryParser(q2).Error())
|
||||
|
||||
c.Request().URI().SetQueryString("nested.agex=10")
|
||||
q2 = new(Query2)
|
||||
utils.AssertEqual(t, "nested.age is empty", c.QueryParser(q2).Error())
|
||||
|
||||
type Node struct {
|
||||
Value int `query:"val,required"`
|
||||
Next *Node `query:"next,required"`
|
||||
}
|
||||
c.Request().URI().SetQueryString("val=1&next.val=3")
|
||||
n := new(Node)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(n))
|
||||
utils.AssertEqual(t, 1, n.Value)
|
||||
utils.AssertEqual(t, 3, n.Next.Value)
|
||||
|
||||
c.Request().URI().SetQueryString("next.val=2")
|
||||
n = new(Node)
|
||||
utils.AssertEqual(t, "val is empty", c.QueryParser(n).Error())
|
||||
|
||||
c.Request().URI().SetQueryString("val=3&next.value=2")
|
||||
n = new(Node)
|
||||
n.Next = new(Node)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(n))
|
||||
utils.AssertEqual(t, 3, n.Value)
|
||||
utils.AssertEqual(t, 0, n.Next.Value)
|
||||
|
||||
type Person struct {
|
||||
Name string `query:"name"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
|
||||
type CollectionQuery struct {
|
||||
Data []Person `query:"data"`
|
||||
}
|
||||
|
||||
c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12")
|
||||
cq := new(CollectionQuery)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(cq))
|
||||
utils.AssertEqual(t, 2, len(cq.Data))
|
||||
utils.AssertEqual(t, "john", cq.Data[0].Name)
|
||||
utils.AssertEqual(t, 10, cq.Data[0].Age)
|
||||
utils.AssertEqual(t, "doe", cq.Data[1].Name)
|
||||
utils.AssertEqual(t, 12, cq.Data[1].Age)
|
||||
|
||||
c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12")
|
||||
cq = new(CollectionQuery)
|
||||
utils.AssertEqual(t, nil, c.QueryParser(cq))
|
||||
utils.AssertEqual(t, 2, len(cq.Data))
|
||||
utils.AssertEqual(t, "john", cq.Data[0].Name)
|
||||
utils.AssertEqual(t, 10, cq.Data[0].Age)
|
||||
utils.AssertEqual(t, "doe", cq.Data[1].Name)
|
||||
utils.AssertEqual(t, 12, cq.Data[1].Age)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_ReqHeaderParser -v
|
||||
func Test_Ctx_ReqHeaderParser(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Header struct {
|
||||
ID int
|
||||
Name string
|
||||
Hobby []string
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
|
||||
c.Request().Header.Add("id", "1")
|
||||
c.Request().Header.Add("Name", "John Doe")
|
||||
c.Request().Header.Add("Hobby", "golang,fiber")
|
||||
q := new(Header)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
|
||||
utils.AssertEqual(t, 2, len(q.Hobby))
|
||||
|
||||
c.Request().Header.Del("hobby")
|
||||
c.Request().Header.Add("Hobby", "golang,fiber,go")
|
||||
q = new(Header)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
|
||||
utils.AssertEqual(t, 3, len(q.Hobby))
|
||||
|
||||
empty := new(Header)
|
||||
c.Request().Header.Del("hobby")
|
||||
utils.AssertEqual(t, nil, c.QueryParser(empty))
|
||||
utils.AssertEqual(t, 0, len(empty.Hobby))
|
||||
|
||||
type Header2 struct {
|
||||
Bool bool
|
||||
ID int
|
||||
Name string
|
||||
Hobby string
|
||||
FavouriteDrinks []string
|
||||
Empty []string
|
||||
Alloc []string
|
||||
No []int64
|
||||
}
|
||||
|
||||
c.Request().Header.Add("id", "2")
|
||||
c.Request().Header.Add("Name", "Jane Doe")
|
||||
c.Request().Header.Del("hobby")
|
||||
c.Request().Header.Add("Hobby", "go,fiber")
|
||||
c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi")
|
||||
c.Request().Header.Add("alloc", "")
|
||||
c.Request().Header.Add("no", "1")
|
||||
|
||||
h2 := new(Header2)
|
||||
h2.Bool = true
|
||||
h2.Name = "hello world"
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))
|
||||
utils.AssertEqual(t, "go,fiber", h2.Hobby)
|
||||
utils.AssertEqual(t, true, h2.Bool)
|
||||
utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten
|
||||
utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks)
|
||||
var nilSlice []string
|
||||
utils.AssertEqual(t, nilSlice, h2.Empty)
|
||||
utils.AssertEqual(t, []string{""}, h2.Alloc)
|
||||
utils.AssertEqual(t, []int64{1}, h2.No)
|
||||
|
||||
type RequiredHeader struct {
|
||||
Name string `reqHeader:"name,required"`
|
||||
}
|
||||
rh := new(RequiredHeader)
|
||||
c.Request().Header.Del("name")
|
||||
utils.AssertEqual(t, "name is empty", c.ReqHeaderParser(rh).Error())
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_ReqHeaderParser_WithSetParserDecoder -v
|
||||
func Test_Ctx_ReqHeaderParser_WithSetParserDecoder(t *testing.T) {
|
||||
type NonRFCTime time.Time
|
||||
|
||||
NonRFCConverter := func(value string) reflect.Value {
|
||||
if v, err := time.Parse("2006-01-02", value); err == nil {
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
return reflect.Value{}
|
||||
}
|
||||
|
||||
nonRFCTime := ParserType{
|
||||
Customtype: NonRFCTime{},
|
||||
Converter: NonRFCConverter,
|
||||
}
|
||||
|
||||
SetParserDecoder(ParserConfig{
|
||||
IgnoreUnknownKeys: true,
|
||||
ParserType: []ParserType{nonRFCTime},
|
||||
ZeroEmpty: true,
|
||||
SetAliasTag: "req",
|
||||
})
|
||||
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type NonRFCTimeInput struct {
|
||||
Date NonRFCTime `req:"date"`
|
||||
Title string `req:"title"`
|
||||
Body string `req:"body"`
|
||||
}
|
||||
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
r := new(NonRFCTimeInput)
|
||||
|
||||
c.Request().Header.Add("Date", "2021-04-10")
|
||||
c.Request().Header.Add("Title", "CustomDateTest")
|
||||
c.Request().Header.Add("Body", "October")
|
||||
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(r))
|
||||
fmt.Println(r.Date, "q.Date")
|
||||
utils.AssertEqual(t, "CustomDateTest", r.Title)
|
||||
date := fmt.Sprintf("%v", r.Date)
|
||||
utils.AssertEqual(t, "{0 63753609600 <nil>}", date)
|
||||
utils.AssertEqual(t, "October", r.Body)
|
||||
|
||||
c.Request().Header.Add("Title", "")
|
||||
r = &NonRFCTimeInput{
|
||||
Title: "Existing title",
|
||||
Body: "Existing Body",
|
||||
}
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(r))
|
||||
utils.AssertEqual(t, "", r.Title)
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_ReqHeaderParser_Schema -v
|
||||
func Test_Ctx_ReqHeaderParser_Schema(t *testing.T) {
|
||||
t.Parallel()
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Header1 struct {
|
||||
Name string `reqHeader:"Name,required"`
|
||||
Nested struct {
|
||||
Age int `reqHeader:"Age"`
|
||||
} `reqHeader:"Nested,required"`
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
|
||||
c.Request().Header.Add("Name", "tom")
|
||||
c.Request().Header.Add("Nested.Age", "10")
|
||||
q := new(Header1)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
|
||||
|
||||
c.Request().Header.Del("Name")
|
||||
q = new(Header1)
|
||||
utils.AssertEqual(t, "Name is empty", c.ReqHeaderParser(q).Error())
|
||||
|
||||
c.Request().Header.Add("Name", "tom")
|
||||
c.Request().Header.Del("Nested.Age")
|
||||
c.Request().Header.Add("Nested.Agex", "10")
|
||||
q = new(Header1)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(q))
|
||||
|
||||
c.Request().Header.Del("Nested.Agex")
|
||||
q = new(Header1)
|
||||
utils.AssertEqual(t, "Nested is empty", c.ReqHeaderParser(q).Error())
|
||||
|
||||
c.Request().Header.Del("Nested.Agex")
|
||||
c.Request().Header.Del("Name")
|
||||
|
||||
type Header2 struct {
|
||||
Name string `reqHeader:"Name"`
|
||||
Nested struct {
|
||||
Age int `reqHeader:"age,required"`
|
||||
} `reqHeader:"Nested"`
|
||||
}
|
||||
|
||||
c.Request().Header.Add("Name", "tom")
|
||||
c.Request().Header.Add("Nested.Age", "10")
|
||||
|
||||
h2 := new(Header2)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))
|
||||
|
||||
c.Request().Header.Del("Name")
|
||||
h2 = new(Header2)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(h2))
|
||||
|
||||
c.Request().Header.Del("Name")
|
||||
c.Request().Header.Del("Nested.Age")
|
||||
c.Request().Header.Add("Nested.Agex", "10")
|
||||
h2 = new(Header2)
|
||||
utils.AssertEqual(t, "Nested.age is empty", c.ReqHeaderParser(h2).Error())
|
||||
|
||||
type Node struct {
|
||||
Value int `reqHeader:"Val,required"`
|
||||
Next *Node `reqHeader:"Next,required"`
|
||||
}
|
||||
c.Request().Header.Add("Val", "1")
|
||||
c.Request().Header.Add("Next.Val", "3")
|
||||
n := new(Node)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(n))
|
||||
utils.AssertEqual(t, 1, n.Value)
|
||||
utils.AssertEqual(t, 3, n.Next.Value)
|
||||
|
||||
c.Request().Header.Del("Val")
|
||||
n = new(Node)
|
||||
utils.AssertEqual(t, "Val is empty", c.ReqHeaderParser(n).Error())
|
||||
|
||||
c.Request().Header.Add("Val", "3")
|
||||
c.Request().Header.Del("Next.Val")
|
||||
c.Request().Header.Add("Next.Value", "2")
|
||||
n = new(Node)
|
||||
n.Next = new(Node)
|
||||
utils.AssertEqual(t, nil, c.ReqHeaderParser(n))
|
||||
utils.AssertEqual(t, 3, n.Value)
|
||||
utils.AssertEqual(t, 0, n.Next.Value)
|
||||
}
|
||||
|
||||
func Test_Ctx_EqualFieldType(t *testing.T) {
|
||||
var out int
|
||||
utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key"))
|
||||
|
||||
var dummy struct{ f string }
|
||||
utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key"))
|
||||
|
||||
var dummy2 struct{ f string }
|
||||
utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f"))
|
||||
|
||||
var user struct {
|
||||
Name string
|
||||
Address string `query:"address"`
|
||||
Age int `query:"AGE"`
|
||||
}
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE"))
|
||||
utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age"))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4
|
||||
func Benchmark_Ctx_QueryParser(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query struct {
|
||||
ID int
|
||||
Name string
|
||||
Hobby []string
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football")
|
||||
q := new(Query)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
c.QueryParser(q)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.QueryParser(q))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_parseQuery -benchmem -count=4
|
||||
func Benchmark_Ctx_parseQuery(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Person struct {
|
||||
Name string `query:"name"`
|
||||
Age int `query:"age"`
|
||||
}
|
||||
|
||||
type CollectionQuery struct {
|
||||
Data []Person `query:"data"`
|
||||
}
|
||||
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10")
|
||||
cq := new(CollectionQuery)
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
c.QueryParser(cq)
|
||||
}
|
||||
|
||||
utils.AssertEqual(b, nil, c.QueryParser(cq))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser_Comma -benchmem -count=4
|
||||
func Benchmark_Ctx_QueryParser_Comma(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type Query struct {
|
||||
ID int
|
||||
Name string
|
||||
Hobby []string
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
// c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football")
|
||||
c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football")
|
||||
q := new(Query)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
c.QueryParser(q)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.QueryParser(q))
|
||||
}
|
||||
|
||||
// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParser -benchmem -count=4
|
||||
func Benchmark_Ctx_ReqHeaderParser(b *testing.B) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
type ReqHeader struct {
|
||||
ID int
|
||||
Name string
|
||||
Hobby []string
|
||||
}
|
||||
c.Request().SetBody([]byte(``))
|
||||
c.Request().Header.SetContentType("")
|
||||
|
||||
c.Request().Header.Add("id", "1")
|
||||
c.Request().Header.Add("Name", "John Doe")
|
||||
c.Request().Header.Add("Hobby", "golang,fiber")
|
||||
|
||||
q := new(ReqHeader)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
c.ReqHeaderParser(q)
|
||||
}
|
||||
utils.AssertEqual(b, nil, c.ReqHeaderParser(q))
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_BodyStreamWriter
|
||||
func Test_Ctx_BodyStreamWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -3876,38 +3025,6 @@ func Test_Ctx_GetRespHeader(t *testing.T) {
|
|||
utils.AssertEqual(t, c.GetRespHeader(HeaderContentType), "application/json")
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_GetRespHeaders
|
||||
func Test_Ctx_GetRespHeaders(t *testing.T) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Set("test", "Hello, World 👋!")
|
||||
c.Set("foo", "bar")
|
||||
c.Response().Header.Set(HeaderContentType, "application/json")
|
||||
|
||||
utils.AssertEqual(t, c.GetRespHeaders(), map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Foo": "bar",
|
||||
"Test": "Hello, World 👋!",
|
||||
})
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_GetReqHeaders
|
||||
func Test_Ctx_GetReqHeaders(t *testing.T) {
|
||||
app := New()
|
||||
c := app.NewCtx(&fasthttp.RequestCtx{})
|
||||
|
||||
c.Request().Header.Set("test", "Hello, World 👋!")
|
||||
c.Request().Header.Set("foo", "bar")
|
||||
c.Request().Header.Set(HeaderContentType, "application/json")
|
||||
|
||||
utils.AssertEqual(t, c.GetReqHeaders(), map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"Foo": "bar",
|
||||
"Test": "Hello, World 👋!",
|
||||
})
|
||||
}
|
||||
|
||||
// go test -run Test_Ctx_IsFromLocal
|
||||
func Test_Ctx_IsFromLocal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
|
12
error.go
12
error.go
|
@ -2,10 +2,21 @@ package fiber
|
|||
|
||||
import (
|
||||
errors "encoding/json"
|
||||
goErrors "errors"
|
||||
|
||||
"github.com/gofiber/fiber/v3/internal/schema"
|
||||
)
|
||||
|
||||
// Range errors
|
||||
var (
|
||||
ErrRangeMalformed = goErrors.New("range: malformed range header string")
|
||||
ErrRangeUnsatisfiable = goErrors.New("range: unsatisfiable range")
|
||||
)
|
||||
|
||||
// Binder errors
|
||||
var ErrCustomBinderNotFound = goErrors.New("binder: custom binder not found, please be sure to enter the right name!")
|
||||
|
||||
// gorilla/schema errors
|
||||
type (
|
||||
// Conversion error exposes the internal schema.ConversionError for public use.
|
||||
ConversionError = schema.ConversionError
|
||||
|
@ -17,6 +28,7 @@ type (
|
|||
MultiError = schema.MultiError
|
||||
)
|
||||
|
||||
// encoding/json errors
|
||||
type (
|
||||
// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal.
|
||||
// (The argument to Unmarshal must be a non-nil pointer.)
|
||||
|
|
|
@ -247,8 +247,13 @@ func New(config ...Config) fiber.Handler {
|
|||
case TagResBody:
|
||||
return buf.Write(c.Response().Body())
|
||||
case TagReqHeaders:
|
||||
out := make(map[string]string, 0)
|
||||
if err := c.Bind().Header(&out); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
reqHeaders := make([]string, 0)
|
||||
for k, v := range c.GetReqHeaders() {
|
||||
for k, v := range out {
|
||||
reqHeaders = append(reqHeaders, k+"="+v)
|
||||
}
|
||||
return buf.Write([]byte(strings.Join(reqHeaders, "&")))
|
||||
|
|
Loading…
Reference in New Issue