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
M. Efe Çetin 2022-08-08 10:16:08 +03:00 committed by GitHub
parent 41159296f3
commit eacde70294
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2513 additions and 1184 deletions

27
app.go
View File

@ -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

194
bind.go Normal file
View File

@ -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
}

1544
bind_test.go Normal file

File diff suppressed because it is too large Load Diff

194
binder/README.md Normal file
View File

@ -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"
```

19
binder/binder.go Normal file
View File

@ -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{}

45
binder/cookie.go Normal file
View File

@ -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)
}

53
binder/form.go Normal file
View File

@ -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)
}

34
binder/header.go Normal file
View File

@ -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)
}

15
binder/json.go Normal file
View File

@ -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)
}

201
binder/mapping.go Normal file
View File

@ -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
}

31
binder/mapping_test.go Normal file
View File

@ -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"))
}

49
binder/query.go Normal file
View File

@ -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)
}

34
binder/resp_header.go Normal file
View File

@ -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)
}

16
binder/uri.go Normal file
View File

@ -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)
}

15
binder/xml.go Normal file
View File

@ -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
View File

@ -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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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.)

View File

@ -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, "&")))