From b4d09053db1ba73dc0a5184caf162befd2e91373 Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Fri, 14 Nov 2025 23:44:50 +0530 Subject: [PATCH 1/7] fix(binding): support custom unmarshaler for slices & arrays --- binding/form_mapping.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 1244b522..6a07330a 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -186,7 +186,10 @@ type BindUnmarshaler interface { func trySetCustom(val string, value reflect.Value) (isSet bool, err error) { switch v := value.Addr().Interface().(type) { case BindUnmarshaler: - return true, v.UnmarshalParam(val) + if err := v.UnmarshalParam(val); err != nil { + return true, fmt.Errorf("invalid value %q: %w", val, err) + } + return true, nil } return false, nil } @@ -245,7 +248,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } if ok, err = trySetCustom(vs[0], value); ok { - return ok, err + if err != nil { + return true, fmt.Errorf("field %q: %w", field.Name, err) + } + return true, nil } if vs, err = trySplit(vs, field); err != nil { @@ -268,7 +274,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } if ok, err = trySetCustom(vs[0], value); ok { - return ok, err + if err != nil { + return true, fmt.Errorf("field %q: %w", field.Name, err) + } + return true, nil } if vs, err = trySplit(vs, field); err != nil { @@ -293,7 +302,10 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } if ok, err := trySetCustom(val, value); ok { - return ok, err + if err != nil { + return true, fmt.Errorf("field %q: %w", field.Name, err) + } + return true, nil } return true, setWithProperType(val, value, field) } From 513a86e17fa3ffed325c7f4f41fa3a919ce32afd Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sat, 15 Nov 2025 22:28:32 +0530 Subject: [PATCH 2/7] added tests for feature and coverage --- binding/form_mapping_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 006eddf1..d0086d31 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -715,3 +715,22 @@ func TestMappingEmptyValues(t *testing.T) { assert.Equal(t, []int{1, 2, 3}, s.SliceCsv) }) } + +type testCustom struct { + Value string +} + +func (t *testCustom) UnmarshalParam(param string) error { + t.Value = "prefix_" + param + return nil +} + +func TestTrySetCustomIntegration(t *testing.T) { + var s struct { + F testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_hello", s.F.Value) +} From ee1f501d4a4c4fcfc2068a62183c06bcdc352e76 Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sun, 30 Nov 2025 15:58:56 +0530 Subject: [PATCH 3/7] more tests for code coverage --- binding/form_mapping_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index d0086d31..d336e0f0 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -7,6 +7,7 @@ package binding import ( "encoding/hex" "errors" + "fmt" "mime/multipart" "reflect" "strconv" @@ -734,3 +735,29 @@ func TestTrySetCustomIntegration(t *testing.T) { require.NoError(t, err) assert.Equal(t, "prefix_hello", s.F.Value) } + +type badCustom struct{} + +func (b *badCustom) UnmarshalParam(s string) error { + return fmt.Errorf("boom") +} + +func TestTrySetCustomError(t *testing.T) { + var s struct { + F badCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") +} + +func TestTrySetCustomNotApplicable(t *testing.T) { + var s struct { + N int `form:"n"` + } + + err := mappingByPtr(&s, formSource{"n": {"42"}}, "form") + require.NoError(t, err) + assert.Equal(t, 42, s.N) +} From 6b6ac46ae37676cebe0bef1a72ad1819ab6d555e Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sun, 30 Nov 2025 16:01:27 +0530 Subject: [PATCH 4/7] using errors instead of fmt --- binding/form_mapping_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index d336e0f0..8437c69b 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -7,7 +7,6 @@ package binding import ( "encoding/hex" "errors" - "fmt" "mime/multipart" "reflect" "strconv" @@ -739,7 +738,7 @@ func TestTrySetCustomIntegration(t *testing.T) { type badCustom struct{} func (b *badCustom) UnmarshalParam(s string) error { - return fmt.Errorf("boom") + return errors.New("boom") } func TestTrySetCustomError(t *testing.T) { From 822cf17bf93acae554a0241c466b96915dd659a9 Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sun, 30 Nov 2025 16:10:58 +0530 Subject: [PATCH 5/7] modifying setArray and setSlice --- binding/form_mapping.go | 6 ++++++ binding/form_mapping_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 6a07330a..1067ca21 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -468,6 +468,12 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val func setArray(vals []string, value reflect.Value, field reflect.StructField) error { for i, s := range vals { + if ok, err := trySetCustom(s, value.Index(i)); ok { + if err != nil { + return fmt.Errorf("field %q: %w", field.Name, err) + } + continue + } err := setWithProperType(s, value.Index(i), field) if err != nil { return err diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 8437c69b..6c1df3cc 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -760,3 +760,35 @@ func TestTrySetCustomNotApplicable(t *testing.T) { require.NoError(t, err) assert.Equal(t, 42, s.N) } + +func TestTrySetCustomIntegrationSuccess(t *testing.T) { + var s struct { + F testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_hello", s.F.Value) +} + +func TestTrySetCustomSlice(t *testing.T) { + var s struct { + F []testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"one", "two"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_one", s.F[0].Value) + assert.Equal(t, "prefix_two", s.F[1].Value) +} + +func TestTrySetCustomArray(t *testing.T) { + var s struct { + F [2]testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello", "world"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_hello", s.F[0].Value) + assert.Equal(t, "prefix_world", s.F[1].Value) +} From b6e5603530b76d92f752a293ec957e0d1e1783c4 Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sun, 30 Nov 2025 16:26:33 +0530 Subject: [PATCH 6/7] more tests because coverage is failing --- binding/form_mapping_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 6c1df3cc..94b1dcb7 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -792,3 +792,23 @@ func TestTrySetCustomArray(t *testing.T) { assert.Equal(t, "prefix_hello", s.F[0].Value) assert.Equal(t, "prefix_world", s.F[1].Value) } + +func TestTrySetCustomSliceError(t *testing.T) { + var s struct { + F []badCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"oops1", "oops2"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") +} + +func TestTrySetCustomArrayError(t *testing.T) { + var s struct { + F [2]badCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"fail1", "fail2"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") +} From 527f128f96bf4d9e412b8e7f01323c33910d069e Mon Sep 17 00:00:00 2001 From: Pratham Gadkari Date: Sun, 30 Nov 2025 16:35:13 +0530 Subject: [PATCH 7/7] combining and adding tests --- binding/form_mapping_test.go | 136 ++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 94b1dcb7..799c36c6 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -741,74 +741,78 @@ func (b *badCustom) UnmarshalParam(s string) error { return errors.New("boom") } -func TestTrySetCustomError(t *testing.T) { - var s struct { - F badCustom `form:"f"` - } +func TestTrySetCustom_SingleValues(t *testing.T) { + t.Run("Error case", func(t *testing.T) { + var s struct { + F badCustom `form:"f"` + } - err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid value") + err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") + }) + + t.Run("Integration success", func(t *testing.T) { + var s struct { + F testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_hello", s.F.Value) + }) + + t.Run("Not applicable type", func(t *testing.T) { + var s struct { + N int `form:"n"` + } + + err := mappingByPtr(&s, formSource{"n": {"42"}}, "form") + require.NoError(t, err) + assert.Equal(t, 42, s.N) + }) } -func TestTrySetCustomNotApplicable(t *testing.T) { - var s struct { - N int `form:"n"` - } +func TestTrySetCustom_Collections(t *testing.T) { + t.Run("Slice success", func(t *testing.T) { + var s struct { + F []testCustom `form:"f"` + } - err := mappingByPtr(&s, formSource{"n": {"42"}}, "form") - require.NoError(t, err) - assert.Equal(t, 42, s.N) -} - -func TestTrySetCustomIntegrationSuccess(t *testing.T) { - var s struct { - F testCustom `form:"f"` - } - - err := mappingByPtr(&s, formSource{"f": {"hello"}}, "form") - require.NoError(t, err) - assert.Equal(t, "prefix_hello", s.F.Value) -} - -func TestTrySetCustomSlice(t *testing.T) { - var s struct { - F []testCustom `form:"f"` - } - - err := mappingByPtr(&s, formSource{"f": {"one", "two"}}, "form") - require.NoError(t, err) - assert.Equal(t, "prefix_one", s.F[0].Value) - assert.Equal(t, "prefix_two", s.F[1].Value) -} - -func TestTrySetCustomArray(t *testing.T) { - var s struct { - F [2]testCustom `form:"f"` - } - - err := mappingByPtr(&s, formSource{"f": {"hello", "world"}}, "form") - require.NoError(t, err) - assert.Equal(t, "prefix_hello", s.F[0].Value) - assert.Equal(t, "prefix_world", s.F[1].Value) -} - -func TestTrySetCustomSliceError(t *testing.T) { - var s struct { - F []badCustom `form:"f"` - } - - err := mappingByPtr(&s, formSource{"f": {"oops1", "oops2"}}, "form") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid value") -} - -func TestTrySetCustomArrayError(t *testing.T) { - var s struct { - F [2]badCustom `form:"f"` - } - - err := mappingByPtr(&s, formSource{"f": {"fail1", "fail2"}}, "form") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid value") + err := mappingByPtr(&s, formSource{"f": {"one", "two"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_one", s.F[0].Value) + assert.Equal(t, "prefix_two", s.F[1].Value) + }) + + t.Run("Array success", func(t *testing.T) { + var s struct { + F [2]testCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"hello", "world"}}, "form") + require.NoError(t, err) + assert.Equal(t, "prefix_hello", s.F[0].Value) + assert.Equal(t, "prefix_world", s.F[1].Value) + }) + + t.Run("Slice error", func(t *testing.T) { + var s struct { + F []badCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"oops1", "oops2"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") + }) + + t.Run("Array error", func(t *testing.T) { + var s struct { + F [2]badCustom `form:"f"` + } + + err := mappingByPtr(&s, formSource{"f": {"fail1", "fail2"}}, "form") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid value") + }) }