From fb3d374b2f975cb206569c293dfffb3ae6da9869 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 13 Feb 2025 16:23:10 +0300 Subject: [PATCH 1/6] deps: update schema to v1.3.0 --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 37f61dbc3f..3e1e106a84 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/gofiber/fiber/v3 go 1.23 require ( - github.com/gofiber/schema v1.2.0 + github.com/gofiber/schema v1.3.0 github.com/gofiber/utils/v2 v2.0.0-beta.7 github.com/google/uuid v1.6.0 github.com/mattn/go-colorable v0.1.14 @@ -24,7 +24,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ae49ec8011..9892bff85d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg= github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= +github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= +github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ= github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -38,6 +40,8 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From ec7c89a368a028a43c596131324b775091b509f8 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 13 Feb 2025 17:27:50 +0300 Subject: [PATCH 2/6] bind: add support for multipart file binding --- binder/form.go | 12 ++++++++- binder/form_test.go | 63 +++++++++++++++++++++++++++++++++++++++++---- binder/mapping.go | 19 +++++++++----- docs/api/bind.md | 33 ++++++++++++++++++++++++ docs/whats_new.md | 1 + 5 files changed, 115 insertions(+), 13 deletions(-) diff --git a/binder/form.go b/binder/form.go index a8f5b85270..fab280342f 100644 --- a/binder/form.go +++ b/binder/form.go @@ -1,6 +1,8 @@ package binder import ( + "mime/multipart" + "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) @@ -59,7 +61,15 @@ func (b *FormBinding) bindMultipart(req *fasthttp.Request, out any) error { } } - return parse(b.Name(), out, data) + files := make(map[string][]*multipart.FileHeader) + for key, values := range multipartForm.File { + err = formatBindData(out, files, key, values, b.EnableSplitting, true) + if err != nil { + return err + } + } + + return parse(b.Name(), out, data, files) } // Reset resets the FormBinding binder. diff --git a/binder/form_test.go b/binder/form_test.go index 55023cb30f..98526b5307 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -2,6 +2,7 @@ package binder import ( "bytes" + "io" "mime/multipart" "testing" @@ -98,10 +99,12 @@ func Test_FormBinder_BindMultipart(t *testing.T) { } type User struct { - Name string `form:"name"` - Names []string `form:"names"` - Posts []Post `form:"posts"` - Age int `form:"age"` + Name string `form:"name"` + Names []string `form:"names"` + Posts []Post `form:"posts"` + Age int `form:"age"` + Avatar *multipart.FileHeader `form:"avatar"` + Avatars []*multipart.FileHeader `form:"avatars"` } var user User @@ -118,6 +121,24 @@ func Test_FormBinder_BindMultipart(t *testing.T) { require.NoError(t, mw.WriteField("posts[1][title]", "post2")) require.NoError(t, mw.WriteField("posts[2][title]", "post3")) + writer, err := mw.CreateFormFile("avatar", "avatar.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar")) + require.NoError(t, err) + + writer, err = mw.CreateFormFile("avatars", "avatar1.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar1")) + require.NoError(t, err) + + writer, err = mw.CreateFormFile("avatars", "avatar2.txt") + require.NoError(t, err) + + _, err = writer.Write([]byte("avatar2")) + require.NoError(t, err) + require.NoError(t, mw.Close()) req.Header.SetContentType(mw.FormDataContentType()) @@ -127,7 +148,7 @@ func Test_FormBinder_BindMultipart(t *testing.T) { fasthttp.ReleaseRequest(req) }) - err := b.Bind(req, &user) + err = b.Bind(req, &user) require.NoError(t, err) require.Equal(t, "john", user.Name) @@ -139,6 +160,38 @@ func Test_FormBinder_BindMultipart(t *testing.T) { require.Equal(t, "post1", user.Posts[0].Title) require.Equal(t, "post2", user.Posts[1].Title) require.Equal(t, "post3", user.Posts[2].Title) + + require.NotNil(t, user.Avatar) + require.Equal(t, "avatar.txt", user.Avatar.Filename) + require.Equal(t, "application/octet-stream", user.Avatar.Header.Get("Content-Type")) + + file, err := user.Avatar.Open() + require.NoError(t, err) + + content, err := io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar", string(content)) + + require.Len(t, user.Avatars, 2) + require.Equal(t, "avatar1.txt", user.Avatars[0].Filename) + require.Equal(t, "application/octet-stream", user.Avatars[0].Header.Get("Content-Type")) + + file, err = user.Avatars[0].Open() + require.NoError(t, err) + + content, err = io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar1", string(content)) + + require.Equal(t, "avatar2.txt", user.Avatars[1].Filename) + require.Equal(t, "application/octet-stream", user.Avatars[1].Header.Get("Content-Type")) + + file, err = user.Avatars[1].Open() + require.NoError(t, err) + + content, err = io.ReadAll(file) + require.NoError(t, err) + require.Equal(t, "avatar2", string(content)) } func Benchmark_FormBinder_BindMultipart(b *testing.B) { diff --git a/binder/mapping.go b/binder/mapping.go index 70cb9cbc2d..061929f4f0 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -3,6 +3,7 @@ package binder import ( "errors" "fmt" + "mime/multipart" "reflect" "strings" "sync" @@ -69,7 +70,7 @@ func init() { } // parse data into the map or struct -func parse(aliasTag string, out any, data map[string][]string) error { +func parse(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { ptrVal := reflect.ValueOf(out) // Get pointer value @@ -83,11 +84,11 @@ func parse(aliasTag string, out any, data map[string][]string) error { } // Parse into the struct - return parseToStruct(aliasTag, out, data) + return parseToStruct(aliasTag, out, data, files...) } // Parse data into the struct with gorilla/schema -func parseToStruct(aliasTag string, out any, data map[string][]string) error { +func parseToStruct(aliasTag string, out any, data map[string][]string, files ...map[string][]*multipart.FileHeader) error { // Get decoder from pool schemaDecoder := decoderPoolMap[aliasTag].Get().(*schema.Decoder) //nolint:errcheck,forcetypeassert // not needed defer decoderPoolMap[aliasTag].Put(schemaDecoder) @@ -95,7 +96,7 @@ func parseToStruct(aliasTag string, out any, data map[string][]string) error { // Set alias tag schemaDecoder.SetAliasTag(aliasTag) - if err := schemaDecoder.Decode(out, data); err != nil { + if err := schemaDecoder.Decode(out, data, files...); err != nil { return fmt.Errorf("bind: %w", err) } @@ -250,7 +251,7 @@ func FilterFlags(content string) string { return content } -func formatBindData[T any](out any, data map[string][]string, key string, value T, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay +func formatBindData[T, K any](out any, data map[string][]T, key string, value K, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay var err error if supportBracketNotation && strings.Contains(key, "[") { key, err = parseParamSquareBrackets(key) @@ -261,10 +262,14 @@ func formatBindData[T any](out any, data map[string][]string, key string, value switch v := any(value).(type) { case string: - assignBindData(out, data, key, v, enableSplitting) + assignBindData(out, any(data).(map[string][]string), key, v, enableSplitting) case []string: for _, val := range v { - assignBindData(out, data, key, val, enableSplitting) + assignBindData(out, any(data).(map[string][]string), key, val, enableSplitting) + } + case []*multipart.FileHeader: + for _, val := range v { + data[key] = append(data[key], any(val).(T)) } default: return fmt.Errorf("unsupported value type: %T", value) diff --git a/docs/api/bind.md b/docs/api/bind.md index d2b336310d..973db0f295 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -120,6 +120,39 @@ curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=j curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" localhost:3000 ``` +:::info +If you need to bind multipart file, you can use `*multipart.FileHeader`, `*[]*multipart.FileHeader` or `[]*multipart.FileHeader` as a field type. +::: + +```go title="Example" +type Person struct { + Name string `form:"name"` + Pass string `form:"pass"` + Avatar *multipart.FileHeader `form:"avatar"` +} + +app.Post("/", func(c fiber.Ctx) error { + p := new(Person) + + if err := c.Bind().Form(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Avatar.Filename) // file.txt + + // ... +}) +``` + +Run tests with the following `curl` command: + +```bash +curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" -F 'avatar=@filename' localhost:3000 +``` + + ### JSON Binds the request JSON body to a struct. diff --git a/docs/whats_new.md b/docs/whats_new.md index 1958632a29..362be34629 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -487,6 +487,7 @@ Fiber v3 introduces a new binding mechanism that simplifies the process of bindi - Unified binding from URL parameters, query parameters, headers, and request bodies. - Support for custom binders and constraints. - Improved error handling and validation. +- Support multipart file binding for `*multipart.FileHeader`, `*[]*multipart.FileHeader`, and `[]*multipart.FileHeader` field types.
Example From a4209172b9b701436b225e5515e2d9a4772a4229 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Thu, 13 Feb 2025 17:33:40 +0300 Subject: [PATCH 3/6] bind: fix linter --- binder/form_test.go | 4 ++-- binder/mapping.go | 20 +++++++++++++++++--- docs/api/bind.md | 1 - 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/binder/form_test.go b/binder/form_test.go index 98526b5307..d961f87346 100644 --- a/binder/form_test.go +++ b/binder/form_test.go @@ -99,12 +99,12 @@ func Test_FormBinder_BindMultipart(t *testing.T) { } type User struct { + Avatar *multipart.FileHeader `form:"avatar"` Name string `form:"name"` Names []string `form:"names"` Posts []Post `form:"posts"` - Age int `form:"age"` - Avatar *multipart.FileHeader `form:"avatar"` Avatars []*multipart.FileHeader `form:"avatars"` + Age int `form:"age"` } var user User diff --git a/binder/mapping.go b/binder/mapping.go index 061929f4f0..bc95d02822 100644 --- a/binder/mapping.go +++ b/binder/mapping.go @@ -262,14 +262,28 @@ func formatBindData[T, K any](out any, data map[string][]T, key string, value K, switch v := any(value).(type) { case string: - assignBindData(out, any(data).(map[string][]string), key, v, enableSplitting) + dataMap, ok := any(data).(map[string][]string) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + + assignBindData(out, dataMap, key, v, enableSplitting) case []string: + dataMap, ok := any(data).(map[string][]string) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + for _, val := range v { - assignBindData(out, any(data).(map[string][]string), key, val, enableSplitting) + assignBindData(out, dataMap, key, val, enableSplitting) } case []*multipart.FileHeader: for _, val := range v { - data[key] = append(data[key], any(val).(T)) + valT, ok := any(val).(T) + if !ok { + return fmt.Errorf("unsupported value type: %T", value) + } + data[key] = append(data[key], valT) } default: return fmt.Errorf("unsupported value type: %T", value) diff --git a/docs/api/bind.md b/docs/api/bind.md index 973db0f295..eaad63050c 100644 --- a/docs/api/bind.md +++ b/docs/api/bind.md @@ -152,7 +152,6 @@ Run tests with the following `curl` command: curl -X POST -H "Content-Type: multipart/form-data" -F "name=john" -F "pass=doe" -F 'avatar=@filename' localhost:3000 ``` - ### JSON Binds the request JSON body to a struct. From 784f8e20ed1510c134e74a278d33284a110aa00c Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 19 Feb 2025 21:10:30 +0300 Subject: [PATCH 4/6] improve coverage --- binder/mapping_test.go | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/binder/mapping_test.go b/binder/mapping_test.go index 75cdc78305..ea63cbdcf9 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -2,6 +2,7 @@ package binder import ( "errors" + "mime/multipart" "reflect" "testing" @@ -177,3 +178,76 @@ func Test_FilterFlags(t *testing.T) { }) } } + +func TestFormatBindData(t *testing.T) { + t.Run("string value with valid key", func(t *testing.T) { + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "name", "John", false, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data["name"]) != 1 || data["name"][0] != "John" { + t.Fatalf("expected data[\"name\"] = [John], got %v", data["name"]) + } + }) + + t.Run("unsupported value type", func(t *testing.T) { + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "age", 30, false, false) // int is unsupported + if err == nil { + t.Fatal("expected an error, got nil") + } + }) + + t.Run("bracket notation parsing error", func(t *testing.T) { + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation + if err == nil { + t.Fatal("expected an error, got nil") + } + }) + + t.Run("handling multipart file headers", func(t *testing.T) { + out := struct{}{} + data := make(map[string][]*multipart.FileHeader) + files := []*multipart.FileHeader{ + {Filename: "file1.txt"}, + {Filename: "file2.txt"}, + } + err := formatBindData(out, data, "files", files, false, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data["files"]) != 2 { + t.Fatalf("expected 2 files, got %d", len(data["files"])) + } + }) + + t.Run("type casting error", func(t *testing.T) { + out := struct{}{} + data := map[string][]int{} // Incorrect type to force a casting error + err := formatBindData(out, data, "key", "value", false, false) + require.Equal(t, err.Error(), "unsupported value type: string") + }) +} + +func TestAssignBindData(t *testing.T) { + t.Run("splitting enabled with comma", func(t *testing.T) { + out := struct { + Colors []string `query:"colors"` + }{} + data := make(map[string][]string) + assignBindData(&out, data, "colors", "red,blue,green", true) + require.Len(t, data["colors"], 3) + }) + + t.Run("splitting disabled", func(t *testing.T) { + out := []string{} + data := make(map[string][]string) + assignBindData(out, data, "color", "red,blue", false) + require.Len(t, data["color"], 1) + }) +} From a875ec66a9e4460554f835ab7522cd610b09a359 Mon Sep 17 00:00:00 2001 From: Muhammed Efe Cetin Date: Wed, 19 Feb 2025 21:16:45 +0300 Subject: [PATCH 5/6] fix linter --- binder/mapping_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binder/mapping_test.go b/binder/mapping_test.go index ea63cbdcf9..2d74dd7dec 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -230,7 +230,7 @@ func TestFormatBindData(t *testing.T) { out := struct{}{} data := map[string][]int{} // Incorrect type to force a casting error err := formatBindData(out, data, "key", "value", false, false) - require.Equal(t, err.Error(), "unsupported value type: string") + require.Equal(t, "unsupported value type: string", err.Error()) }) } From a99d7dc5ac9a91d6d286a7a59ccefab55e054c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Tue, 25 Feb 2025 19:27:55 +0100 Subject: [PATCH 6/6] add test cases --- binder/mapping_test.go | 94 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/binder/mapping_test.go b/binder/mapping_test.go index 2d74dd7dec..9c7b92eeb5 100644 --- a/binder/mapping_test.go +++ b/binder/mapping_test.go @@ -10,6 +10,8 @@ import ( ) func Test_EqualFieldType(t *testing.T) { + t.Parallel() + var out int require.False(t, equalFieldType(&out, reflect.Int, "key")) @@ -48,6 +50,8 @@ func Test_EqualFieldType(t *testing.T) { } func Test_ParseParamSquareBrackets(t *testing.T) { + t.Parallel() + tests := []struct { err error input string @@ -102,6 +106,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() + result, err := parseParamSquareBrackets(tt.input) if tt.err != nil { require.Error(t, err) @@ -115,6 +121,8 @@ func Test_ParseParamSquareBrackets(t *testing.T) { } func Test_parseToMap(t *testing.T) { + t.Parallel() + inputMap := map[string][]string{ "key1": {"value1", "value2"}, "key2": {"value3"}, @@ -148,6 +156,8 @@ func Test_parseToMap(t *testing.T) { } func Test_FilterFlags(t *testing.T) { + t.Parallel() + tests := []struct { input string expected string @@ -173,6 +183,8 @@ func Test_FilterFlags(t *testing.T) { for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { + t.Parallel() + result := FilterFlags(tt.input) require.Equal(t, tt.expected, result) }) @@ -180,7 +192,11 @@ func Test_FilterFlags(t *testing.T) { } func TestFormatBindData(t *testing.T) { + t.Parallel() + t.Run("string value with valid key", func(t *testing.T) { + t.Parallel() + out := struct{}{} data := make(map[string][]string) err := formatBindData(out, data, "name", "John", false, false) @@ -193,6 +209,8 @@ func TestFormatBindData(t *testing.T) { }) t.Run("unsupported value type", func(t *testing.T) { + t.Parallel() + out := struct{}{} data := make(map[string][]string) err := formatBindData(out, data, "age", 30, false, false) // int is unsupported @@ -202,6 +220,8 @@ func TestFormatBindData(t *testing.T) { }) t.Run("bracket notation parsing error", func(t *testing.T) { + t.Parallel() + out := struct{}{} data := make(map[string][]string) err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation @@ -211,6 +231,8 @@ func TestFormatBindData(t *testing.T) { }) t.Run("handling multipart file headers", func(t *testing.T) { + t.Parallel() + out := struct{}{} data := make(map[string][]*multipart.FileHeader) files := []*multipart.FileHeader{ @@ -227,6 +249,8 @@ func TestFormatBindData(t *testing.T) { }) t.Run("type casting error", func(t *testing.T) { + t.Parallel() + out := struct{}{} data := map[string][]int{} // Incorrect type to force a casting error err := formatBindData(out, data, "key", "value", false, false) @@ -235,7 +259,11 @@ func TestFormatBindData(t *testing.T) { } func TestAssignBindData(t *testing.T) { + t.Parallel() + t.Run("splitting enabled with comma", func(t *testing.T) { + t.Parallel() + out := struct { Colors []string `query:"colors"` }{} @@ -245,9 +273,73 @@ func TestAssignBindData(t *testing.T) { }) t.Run("splitting disabled", func(t *testing.T) { - out := []string{} + t.Parallel() + + var out []string data := make(map[string][]string) assignBindData(out, data, "color", "red,blue", false) require.Len(t, data["color"], 1) }) } + +func Test_parseToStruct_MismatchedData(t *testing.T) { + t.Parallel() + + type User struct { + Name string `query:"name"` + Age int `query:"age"` + } + + data := map[string][]string{ + "name": {"John"}, + "age": {"invalidAge"}, + } + + err := parseToStruct("query", &User{}, data) + require.Error(t, err) + require.EqualError(t, err, "bind: schema: error converting value for \"age\"") +} + +func Test_formatBindData_ErrorCases(t *testing.T) { + t.Parallel() + + t.Run("unsupported value type int", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "age", 30, false, false) // int is unsupported + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: int") + }) + + t.Run("unsupported value type map", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "map", map[string]string{"key": "value"}, false, false) // map is unsupported + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: map[string]string") + }) + + t.Run("bracket notation parsing error", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "invalid[", "value", false, true) // malformed bracket notation + require.Error(t, err) + require.EqualError(t, err, "unmatched brackets") + }) + + t.Run("type casting error for []string", func(t *testing.T) { + t.Parallel() + + out := struct{}{} + data := make(map[string][]string) + err := formatBindData(out, data, "names", 123, false, false) // invalid type for []string + require.Error(t, err) + require.EqualError(t, err, "unsupported value type: int") + }) +}