From 97da803d61a46240f31c172c4cf90703ced1cdda Mon Sep 17 00:00:00 2001 From: edvardsanta Date: Tue, 25 Mar 2025 13:41:14 -0300 Subject: [PATCH] feat: Add All method to Bind This commit introduces a new `All` method to the `Bind` struct, enabling the binding of request data from multiple sources (URI parameters, body, query parameters, headers, and cookies) into a single struct. The `All` method iterates through the available binding sources, applying them in a predefined precedence order. It merges the values from each source into the output struct, only updating fields that are currently unset. Changes: - Added `All` method to `Bind` struct. - Added `mergeStruct` helper function to merge struct values. - Added `isZero` helper function to check if a value is zero. - Added a test case for the `All` method in `bind_test.go` to validate its functionality. --- bind.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ bind_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/bind.go b/bind.go index 13d9d367..18c94fe6 100644 --- a/bind.go +++ b/bind.go @@ -1,6 +1,8 @@ package fiber import ( + "reflect" + "github.com/gofiber/fiber/v3/binder" "github.com/gofiber/utils/v2" ) @@ -273,3 +275,58 @@ func (b *Bind) Body(out any) error { // No suitable content type found return ErrUnprocessableEntity } + +func (b *Bind) All(out any) error { + outVal := reflect.ValueOf(out) + if outVal.Kind() != reflect.Ptr || outVal.Elem().Kind() != reflect.Struct { + return ErrUnprocessableEntity + } + + outElem := outVal.Elem() + + // Precedence: URL Params -> Body -> Query -> Headers -> Cookies + sources := []func(any) error{ + b.URI, + b.Body, + b.Query, + b.Header, + b.Cookie, + } + + tempStruct := reflect.New(outElem.Type()).Interface() + + // TODO: Support custom precedence with an optional binding_source tag + // TODO: Create WithOverrideEmptyValues + // Bind from each source, but only update unset fields + for _, bindFunc := range sources { + + if err := bindFunc(tempStruct); err != nil { + return err + } + + tempStructVal := reflect.ValueOf(tempStruct).Elem() + mergeStruct(outElem, tempStructVal) + } + + return nil +} + +func mergeStruct(dst, src reflect.Value) { + dstFields := dst.NumField() + for i := 0; i < dstFields; i++ { + dstField := dst.Field(i) + srcField := src.Field(i) + + // Skip if the destination field is already set + if isZero(dstField.Interface()) { + if dstField.CanSet() && srcField.IsValid() { + dstField.Set(srcField) + } + } + } +} + +func isZero(value any) bool { + v := reflect.ValueOf(value) + return v.IsZero() +} diff --git a/bind_test.go b/bind_test.go index 89839e13..3d9ab327 100644 --- a/bind_test.go +++ b/bind_test.go @@ -1885,3 +1885,52 @@ func Test_Bind_RepeatParserWithSameStruct(t *testing.T) { testDecodeParser(MIMEApplicationForm, "body_param=body_param") testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--") } + +func TestBind_All(t *testing.T) { + type User struct { + ID int `param:"id" query:"id" json:"id" form:"id"` + Name string `query:"name" json:"name" form:"name"` + Email string `json:"email" form:"email"` + Role string `header:"x-user-role"` + SessionID string `json:"SessionID" cookie:"session_id"` + Avatar *multipart.FileHeader `form:"avatar"` + } + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + tests := []struct { + name string + bind *Bind + out any + wantErr bool + }{ + { + name: "Invalid output type", + bind: &Bind{}, + out: 123, + wantErr: true, + }, + { + // Validate this test case + name: "Successful binding", + bind: &Bind{ + ctx: c, + }, + out: &User{ + ID: 10, + Name: "Alice", + Email: "alice@example.com", + Role: "admin", + SessionID: "abc123", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.bind.All(tt.out); (err != nil) != tt.wantErr { + t.Errorf("Bind.All() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}