Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ v3 (feature): request binding #2006

Draft
wants to merge 38 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2cb58a2
remove old binding
trim21 Aug 23, 2022
3251afc
add new bind
trim21 Aug 23, 2022
a6696e5
replace panic with returning error
trim21 Aug 31, 2022
b5eeaa4
get typeID like stdlilb reflect
trim21 Aug 31, 2022
ffc1c41
support form and multipart
trim21 Aug 31, 2022
9887ac5
move internal/reflectunsafe into internal/bind
trim21 Aug 31, 2022
c8bc2e4
make content-type checking optional
trim21 Aug 31, 2022
257e791
add doc about chaining API
trim21 Aug 31, 2022
0be435a
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Sep 11, 2022
5569568
no alloc req headers logger
trim21 Sep 11, 2022
d8d0e52
handle error
trim21 Sep 11, 2022
df19a9e
lint
trim21 Sep 11, 2022
2369002
bench params
trim21 Sep 22, 2022
0883994
remove dead code
trim21 Sep 22, 2022
4ffac50
add more doc
trim21 Sep 22, 2022
f64b9d4
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Sep 23, 2022
befee12
fix test
trim21 Sep 23, 2022
d52652e
Merge remote-tracking branch 'origin/v3-beta' into bind
efectn Nov 15, 2022
6cb876a
add basic nested binding support (not yet for slices)
efectn Nov 17, 2022
3661d33
add support for queries like data[0][name] (not yet supporting deeper…
efectn Nov 20, 2022
4183069
support pointer fields
efectn Nov 27, 2022
7345517
add old methods
efectn Dec 14, 2022
081809e
feat: support float
FGYFFFF Jan 5, 2023
21c6e40
Merge remote-tracking branch 'upstream/v3-beta' into bind
trim21 Jan 11, 2023
967e52a
Merge remote-tracking branch 'origin/v3-beta' into bind
efectn Aug 6, 2023
8e6b3bb
fix mws
efectn Aug 6, 2023
3c20e85
Merge remote-tracking branch 'origin/main' into bind
efectn Mar 16, 2024
8a77269
update somem methods
efectn Mar 16, 2024
3bb9df5
Merge remote-tracking branch 'origin/main' into bind
efectn Aug 30, 2024
e7545b2
update
efectn Aug 30, 2024
857fdc0
Revert "support pointer fields"
efectn Aug 30, 2024
12c70ad
Revert "add support for queries like data[0][name] (not yet supportin…
efectn Aug 30, 2024
52a8b8a
Revert "add basic nested binding support (not yet for slices)"
efectn Aug 30, 2024
5f19450
binder_compile: support embedding simple structs
efectn Aug 30, 2024
f19d747
binder_compile: support embedding slice structs and fix text unmarshaler
efectn Aug 31, 2024
1f8459d
bind: add support for simple nested structs
efectn Sep 1, 2024
9fbf830
bind: add benchmark case for simple nested structs
efectn Sep 1, 2024
b851f71
WIP: binder: add slice nested binding support1
efectn Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 34 additions & 20 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,18 @@ type App struct {
newCtxFunc func(app *App) CustomCtx
// TLS handler
tlsHandler *TLSHandler
// bind decoder cache
bindDecoderCache sync.Map
// form decoder cache
formDecoderCache sync.Map
// multipart decoder cache
multipartDecoderCache sync.Map
// Mount fields
mountFields *mountFields
// Route stack divided by HTTP methods
stack [][]*Route
// Route stack divided by HTTP methods and route prefixes
treeStack []map[string][]*Route
// custom binders
customBinders []CustomBinder
// customConstraints is a list of external constraints
customConstraints []CustomConstraint
// sendfiles stores configurations for handling ctx.SendFile operations
Expand Down Expand Up @@ -325,6 +329,23 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
// Default: xml.Marshal
XMLEncoder utils.XMLMarshal `json:"-"`

// XMLDecoder set by an external client of Fiber it will use the provided implementation of a
// XMLUnmarshal
//
// Allowing for flexibility in using another XML library for encoding
// Default: utils.XMLUnmarshal
XMLDecoder utils.XMLUnmarshal `json:"-"`

// App validate. if nil, and context.EnableValidate will always return a error.
// Default: nil
Validator Validator

// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)
// WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose.
//
// Default: NetworkTCP4
Network string

// If you find yourself behind some sort of proxy, like a load balancer,
// then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header.
// For example, the Host HTTP header is usually used to return the requested host.
Expand Down Expand Up @@ -366,12 +387,6 @@ type Config struct { //nolint:govet // Aligning the struct fields is not necessa
// Optional. Default: DefaultColors
ColorScheme Colors `json:"color_scheme"`

// If you want to validate header/form/query... automatically when to bind, you can define struct validator.
// Fiber doesn't have default validator, so it'll skip validator step if you don't use any validator.
//
// Default: nil
StructValidator StructValidator

// RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish.
//
// Optional. Default: DefaultMethods
Expand Down Expand Up @@ -439,12 +454,11 @@ func New(config ...Config) *App {
// Create a new app
app := &App{
// Create config
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
latestRoute: &Route{},
customBinders: []CustomBinder{},
sendfiles: []*sendFileStore{},
config: Config{},
getBytes: utils.UnsafeBytes,
getString: utils.UnsafeString,
latestRoute: &Route{},
sendfiles: []*sendFileStore{},
}

// Create Ctx pool
Expand Down Expand Up @@ -503,9 +517,15 @@ func New(config ...Config) *App {
if app.config.JSONDecoder == nil {
app.config.JSONDecoder = json.Unmarshal
}

if app.config.XMLEncoder == nil {
app.config.XMLEncoder = xml.Marshal
}

if app.config.XMLDecoder == nil {
app.config.XMLDecoder = xml.Unmarshal
}

if len(app.config.RequestMethods) == 0 {
app.config.RequestMethods = DefaultMethods
}
Expand Down Expand Up @@ -554,12 +574,6 @@ func (app *App) RegisterCustomConstraint(constraint CustomConstraint) {
app.customConstraints = append(app.customConstraints, constraint)
}

// RegisterCustomBinder Allows to register custom binders to use as Bind().Custom("name").
// They should be compatible with CustomBinder interface.
func (app *App) RegisterCustomBinder(binder CustomBinder) {
app.customBinders = append(app.customBinders, binder)
}

// SetTLSHandler Can be used to set ClientHelloInfo when using TLS with Listener.
func (app *App) SetTLSHandler(tlsHandler *TLSHandler) {
// Attach the tlsHandler to the config
Expand Down
222 changes: 64 additions & 158 deletions bind.go
Original file line number Diff line number Diff line change
@@ -1,193 +1,99 @@
package fiber

import (
"github.com/gofiber/fiber/v3/binder"
"github.com/gofiber/utils/v2"
)

// CustomBinder An interface to register custom binders.
type CustomBinder interface {
Name() string
MIMETypes() []string
Parse(c Ctx, out any) error
}
"encoding"
"fmt"
"reflect"

// StructValidator is an interface to register custom struct validator for binding.
type StructValidator interface {
Validate(out any) error
}
"github.com/gofiber/fiber/v3/internal/bind"
)

// Bind struct
type Bind struct {
ctx Ctx
should bool
type Binder interface {
UnmarshalFiberCtx(ctx Ctx) error
}

// Should To handle binder errors manually, you can prefer Should method.
// It's default behavior of binder.
func (b *Bind) Should() *Bind {
b.should = true

return b
// decoder should set a field on reqValue
// it's created with field index
type decoder interface {
Decode(ctx Ctx, reqValue reflect.Value) error
Kind() string
}

// Must If you want to handle binder errors automatically, you can use Must.
// If there's an error it'll return error and 400 as HTTP status.
func (b *Bind) Must() *Bind {
b.should = false

return b
type fieldCtxDecoder struct {

Check failure on line 22 in bind.go

View workflow job for this annotation

GitHub Actions / lint

fieldalignment: struct with 40 pointer bytes could be 24 (govet)
index int
fieldName string
fieldType reflect.Type
}

// Check Should/Must errors and return it by usage.
func (b *Bind) returnErr(err error) error {
if !b.should {
b.ctx.Status(StatusBadRequest)
return NewError(StatusBadRequest, "Bad request: "+err.Error())
}

return err
}
func (d *fieldCtxDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
v := reflect.New(d.fieldType)
unmarshaler := v.Interface().(Binder)

Check failure on line 30 in bind.go

View workflow job for this annotation

GitHub Actions / lint

type assertion must be checked (forcetypeassert)

Check failure on line 30 in bind.go

View workflow job for this annotation

GitHub Actions / lint

Error return value is not checked (errcheck)

// Struct validation.
func (b *Bind) validateStruct(out any) error {
validator := b.ctx.App().config.StructValidator
if validator != nil {
return validator.Validate(out)
if err := unmarshaler.UnmarshalFiberCtx(ctx); err != nil {
return err
}

reqValue.Field(d.index).Set(v.Elem())
return nil
}

// Custom To use custom binders, you have to use this method.
// You can register them from RegisterCustomBinder method of Fiber instance.
// They're checked by name, if it's not found, it will return an error.
// NOTE: Should/Must is still valid for Custom binders.
func (b *Bind) Custom(name string, dest any) error {
binders := b.ctx.App().customBinders
for _, customBinder := range binders {
if customBinder.Name() == name {
return b.returnErr(customBinder.Parse(b.ctx, dest))
}
}

return ErrCustomBinderNotFound
}

// Header binds the request header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) Header(out any) error {
if err := b.returnErr(binder.HeaderBinder.Bind(b.ctx.Request(), out)); err != nil {
return err
}

return b.validateStruct(out)
func (d *fieldCtxDecoder) Kind() string {

Check failure on line 40 in bind.go

View workflow job for this annotation

GitHub Actions / lint

unused-receiver: method receiver 'd' is not referenced in method's body, consider removing or renaming it as _ (revive)
return "ctx"
}

// RespHeader binds the response header strings into the struct, map[string]string and map[string][]string.
func (b *Bind) RespHeader(out any) error {
if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil {
return err
}

return b.validateStruct(out)
type fieldTextDecoder struct {

Check failure on line 44 in bind.go

View workflow job for this annotation

GitHub Actions / lint

fieldalignment: struct with 120 pointer bytes could be 104 (govet)
fieldIndex int
fieldName string
tag string // query,param,header,respHeader ...
reqKey string
dec bind.TextDecoder
get func(c Ctx, key string, defaultValue ...string) string
subFieldDecoders []decoder
isTextMarshaler bool
fragments []requestKeyFragment
}

// Cookie binds the requesr cookie strings into the struct, map[string]string and map[string][]string.
// NOTE: If your cookie is like key=val1,val2; they'll be binded as an slice if your map is map[string][]string. Else, it'll use last element of cookie.
func (b *Bind) Cookie(out any) error {
if err := b.returnErr(binder.CookieBinder.Bind(b.ctx.Context(), out)); err != nil {
return err
}
func (d *fieldTextDecoder) Decode(ctx Ctx, reqValue reflect.Value) error {
field := reqValue.Field(d.fieldIndex)

return b.validateStruct(out)
}

// Query binds the query string into the struct, map[string]string and map[string][]string.
func (b *Bind) Query(out any) error {
if err := b.returnErr(binder.QueryBinder.Bind(b.ctx.Context(), out)); err != nil {
return err
}

return b.validateStruct(out)
}

// JSON binds the body string into the struct.
func (b *Bind) JSON(out any) error {
if err := b.returnErr(binder.JSONBinder.Bind(b.ctx.Body(), b.ctx.App().Config().JSONDecoder, out)); err != nil {
return err
// Support for sub fields
if len(d.subFieldDecoders) > 0 {
for _, subFieldDecoder := range d.subFieldDecoders {
err := subFieldDecoder.Decode(ctx, field)
if err != nil {
return err
}
}
return nil
}

return b.validateStruct(out)
}

// XML binds the body string into the struct.
func (b *Bind) XML(out any) error {
if err := b.returnErr(binder.XMLBinder.Bind(b.ctx.Body(), out)); err != nil {
return err
text := d.get(ctx, d.reqKey)
if text == "" {
return nil
}

return b.validateStruct(out)
}

// Form binds the form into the struct, map[string]string and map[string][]string.
func (b *Bind) Form(out any) error {
if err := b.returnErr(binder.FormBinder.Bind(b.ctx.Context(), out)); err != nil {
return err
}
if d.isTextMarshaler {
unmarshaler, ok := field.Addr().Interface().(encoding.TextUnmarshaler)
if !ok {
return fmt.Errorf("field %s does not implement encoding.TextUnmarshaler", d.fieldName)
}

return b.validateStruct(out)
}
err := unmarshaler.UnmarshalText([]byte(text))
if err != nil {
return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err)
}

// URI binds the route parameters into the struct, map[string]string and map[string][]string.
func (b *Bind) URI(out any) error {
if err := b.returnErr(binder.URIBinder.Bind(b.ctx.Route().Params, b.ctx.Params, out)); err != nil {
return err
return nil
}

return b.validateStruct(out)
}

// MultipartForm binds the multipart form into the struct, map[string]string and map[string][]string.
func (b *Bind) MultipartForm(out any) error {
if err := b.returnErr(binder.FormBinder.BindMultipart(b.ctx.Context(), out)); err != nil {
return err
err := d.dec.UnmarshalString(text, field)
if err != nil {
return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.reqKey, err)
}

return b.validateStruct(out)
return nil
}

// Body binds the request body into the struct, map[string]string and map[string][]string.
// It supports decoding the following content types based on the Content-Type header:
// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data
// If none of the content types above are matched, it'll take a look custom binders by checking the MIMETypes() method of custom binder.
// If there're no custom binder for mşme type of body, it will return a ErrUnprocessableEntity error.
func (b *Bind) Body(out any) error {
// Get content-type
ctype := utils.ToLower(utils.UnsafeString(b.ctx.Context().Request.Header.ContentType()))
ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))

// Check custom binders
binders := b.ctx.App().customBinders
for _, customBinder := range binders {
for _, mime := range customBinder.MIMETypes() {
if mime == ctype {
return b.returnErr(customBinder.Parse(b.ctx, out))
}
}
}

// Parse body accordingly
switch ctype {
case MIMEApplicationJSON:
return b.JSON(out)
case MIMETextXML, MIMEApplicationXML:
return b.XML(out)
case MIMEApplicationForm:
return b.Form(out)
case MIMEMultipartForm:
return b.MultipartForm(out)
}

// No suitable content type found
return ErrUnprocessableEntity
func (d *fieldTextDecoder) Kind() string {

Check failure on line 97 in bind.go

View workflow job for this annotation

GitHub Actions / lint

unused-receiver: method receiver 'd' is not referenced in method's body, consider removing or renaming it as _ (revive)
return "text"

Check failure on line 98 in bind.go

View workflow job for this annotation

GitHub Actions / lint

string `text` has 3 occurrences, make it a constant (goconst)
}
Loading
Loading