Merge 842bd597a8d668e7065b179280571eac2f616f0d into 8763f33c65f7df8be5b9fe7504ab7fcf20abb41d

This commit is contained in:
takanuva15 2025-03-27 14:08:53 -04:00 committed by GitHub
commit 1ff750e1d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 541 additions and 131 deletions

View File

@ -5,6 +5,7 @@
package binding
import (
"encoding"
"errors"
"fmt"
"mime/multipart"
@ -136,6 +137,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
type setOptions struct {
isDefaultExists bool
defaultValue string
parser string
}
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
@ -167,6 +169,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
}
}
} else if k, v = head(opt, "="); k == "parser" {
setOpt.parser = v
}
}
@ -190,6 +194,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
return false, nil
}
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
switch parser {
case "encoding.TextUnmarshaler":
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
if !ok {
return false, nil
}
return true, v.UnmarshalText([]byte(val))
}
return false, nil
}
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" {
@ -207,7 +225,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
case "pipes":
sep = "|"
default:
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
}
totalLength := 0
@ -230,7 +248,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() {
case reflect.Slice:
if !ok {
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
@ -240,7 +258,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
}
if ok, err = trySetCustom(vs[0], value); ok {
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
@ -248,9 +268,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, err
}
return true, setSlice(vs, value, field)
return true, setSlice(vs, value, field, opt)
case reflect.Array:
if !ok {
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
vs = []string{opt.defaultValue}
// pre-process the default value for multi if present
@ -260,7 +280,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
}
}
if ok, err = trySetCustom(vs[0], value); ok {
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(vs[0], value); ok {
return ok, err
}
@ -272,27 +294,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
return true, setArray(vs, value, field)
return true, setArray(vs, value, field, opt)
default:
var val string
if !ok {
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
val = opt.defaultValue
} else if len(vs) > 0 {
val = vs[0]
}
if len(vs) > 0 {
val = vs[0]
if val == "" {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
return ok, err
} else if ok, err = trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field)
return true, setWithProperType(val, value, field, opt)
}
}
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
return err
} else if ok, err = trySetCustom(val, value); ok {
return err
}
switch value.Kind() {
case reflect.Int:
return setIntField(val, 0, value)
@ -340,7 +367,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem()))
}
return setWithProperType(val, value.Elem(), field)
return setWithProperType(val, value.Elem(), field, opt)
default:
return errUnknownType
}
@ -447,9 +474,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil
}
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
for i, s := range vals {
err := setWithProperType(s, value.Index(i), field)
err := setWithProperType(s, value.Index(i), field, opt)
if err != nil {
return err
}
@ -457,9 +484,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
return nil
}
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field)
err := setArray(vals, slice, field, opt)
if err != nil {
return err
}

View File

@ -5,6 +5,7 @@
package binding
import (
"encoding"
"encoding/hex"
"errors"
"mime/multipart"
@ -454,44 +455,44 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
require.NoError(t, err)
}
type customUnmarshalParamHex int
// ==== BindUmarshaler tests START ====
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
type customHexUnmarshalParam int
func (f *customHexUnmarshalParam) UnmarshalParam(param string) error {
v, err := strconv.ParseInt(param, 16, 64)
if err != nil {
return err
}
*f = customUnmarshalParamHex(v)
*f = customHexUnmarshalParam(v)
return nil
}
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `form:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
require.NoError(t, err)
func TestMappingCustomHexUnmarshalParam(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customHexUnmarshalParam](
t,
`f5`,
func(hex customHexUnmarshalParam, t *testing.T) {
assert.EqualValues(t, 245, hex)
},
)
assert.EqualValues(t, 245, s.Foo)
// verify default binding works with UnmarshalParam
var sDefaultValue struct {
Field1 customHexUnmarshalParam `form:"field1,default=f5"`
}
err := mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, sDefaultValue.Field1)
}
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
var s struct {
Foo customUnmarshalParamHex `uri:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, 245, s.Foo)
}
type customUnmarshalParamType struct {
type customTypeUnmarshalParam struct {
Protocol string
Path string
Name string
}
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
func (f *customTypeUnmarshalParam) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return errors.New("invalid format")
@ -502,52 +503,28 @@ func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
return nil
}
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
func TestMappingCustomStructType(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customTypeUnmarshalParam](
t,
`file:/foo:happiness`,
func(data customTypeUnmarshalParam, t *testing.T) {
assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "happiness", data.Name)
},
)
}
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
var s struct {
FileData customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `form:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
}
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
var s struct {
FileData *customUnmarshalParamType `uri:"data"`
}
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "file", s.FileData.Protocol)
assert.EqualValues(t, "/foo", s.FileData.Path)
assert.EqualValues(t, "happiness", s.FileData.Name)
func TestMappingCustomPointerStructType(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[*customTypeUnmarshalParam](
t,
`file:/foo:happiness`,
func(data *customTypeUnmarshalParam, t *testing.T) {
assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "happiness", data.Name)
},
)
}
type customPath []string
@ -563,32 +540,49 @@ func (p *customPath) UnmarshalParam(param string) error {
return nil
}
func TestMappingCustomSliceUri(t *testing.T) {
var s struct {
FileData customPath `uri:"path"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
func TestMappingCustomSlice(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customPath](
t,
`bar/foo`,
func(path customPath, t *testing.T) {
assert.EqualValues(t, "bar", path[0])
assert.EqualValues(t, "foo", path[1])
},
)
}
func TestMappingCustomSliceForm(t *testing.T) {
var s struct {
FileData customPath `form:"path"`
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
var sForm struct {
Field1 customPath `form:"field1"`
}
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
require.NoError(t, err)
err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, sForm.Field1)
}
assert.EqualValues(t, "bar", s.FileData[0])
assert.EqualValues(t, "foo", s.FileData[1])
func TestMappingCustomSliceOfSlice(t *testing.T) {
val := `bar/foo,bar/foo/spam`
expected := []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}
var sUri struct {
Field1 []customPath `uri:"field1" collection_format:"csv"`
}
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, expected, sUri.Field1)
var sForm struct {
Field1 []customPath `form:"field1" collection_format:"csv"`
}
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sForm.Field1)
}
type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param)
oid, err := convertTo[objectID](param)
if err != nil {
return err
}
@ -597,8 +591,8 @@ func (o *objectID) UnmarshalParam(param string) error {
return nil
}
func convertTo(s string) (objectID, error) {
var nilObjectID objectID
func convertTo[T ~[12]byte](s string) (T, error) {
var nilObjectID T
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
@ -612,26 +606,351 @@ func convertTo(s string) (objectID, error) {
return oid, nil
}
func TestMappingCustomArrayUri(t *testing.T) {
var s struct {
FileData objectID `uri:"id"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
func TestMappingCustomArray(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[objectID](
t,
`664a062ac74a8ad104e0e80f`,
func(oid objectID, t *testing.T) {
expected, _ := convertTo[objectID](`664a062ac74a8ad104e0e80f`)
assert.EqualValues(t, expected, oid)
},
)
}
func TestMappingCustomArrayForm(t *testing.T) {
var s struct {
FileData objectID `form:"id"`
func TestMappingCustomArrayOfArray(t *testing.T) {
val := `664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`
expected1, _ := convertTo[objectID](`664a062ac74a8ad104e0e80e`)
expected2, _ := convertTo[objectID](`664a062ac74a8ad104e0e80f`)
expected := []objectID{expected1, expected2}
var sUri struct {
Field1 []objectID `uri:"field1" collection_format:"csv"`
}
val := `664a062ac74a8ad104e0e80f`
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, expected, sUri.Field1)
var sForm struct {
Field1 []objectID `form:"field1" collection_format:"csv"`
}
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sForm.Field1)
var sDefaultValue struct {
Field1 []objectID `form:"field1,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f" collection_format:"csv"`
}
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sDefaultValue.Field1)
}
// RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam declares a struct with a field of the given generic type T
// and runs a mapping test using the given value for both the uri and form tag. Any asserts that should be done on the
// result are passed as a function in the last parameter.
//
// This method eliminates the need for writing duplicate tests to verify both form+uri tags for BindUnmarshaler tests
func RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[T any](
t *testing.T,
valueToBind string,
assertsToRunAfterBind func(T, *testing.T),
) {
var sUri struct {
Field1 T `uri:"field1"`
}
err := mappingByPtr(&sUri, formSource{"field1": {valueToBind}}, "uri")
require.NoError(t, err)
assertsToRunAfterBind(sUri.Field1, t)
var sForm struct {
Field1 T `form:"field1"`
}
err = mappingByPtr(&sForm, formSource{"field1": {valueToBind}}, "form")
require.NoError(t, err)
assertsToRunAfterBind(sForm.Field1, t)
}
// ==== BindUmarshaler tests END ====
// ==== TextUnmarshaler tests START ====
type customHexUnmarshalText int
func (f *customHexUnmarshalText) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = customHexUnmarshalText(v)
return nil
}
// verify type implements TextUnmarshaler
var _ encoding.TextUnmarshaler = (*customHexUnmarshalText)(nil)
func TestMappingCustomHexUnmarshalText(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customHexUnmarshalText](
t,
`f5`,
func(hex customHexUnmarshalText, t *testing.T) {
assert.EqualValues(t, 245, hex)
},
)
// verify default binding works with UnmarshalText
var sDefaultValue struct {
Field1 customHexUnmarshalText `form:"field1,default=f5,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, sDefaultValue.Field1)
}
type customTypeUnmarshalText struct {
Protocol string
Path string
Name string
}
func (f *customTypeUnmarshalText) UnmarshalText(text []byte) error {
parts := strings.Split(string(text), ":")
if len(parts) != 3 {
return errors.New("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
f.Name = parts[2]
return nil
}
var _ encoding.TextUnmarshaler = (*customTypeUnmarshalText)(nil)
func TestMappingCustomStructTypeUnmarshalText(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customTypeUnmarshalText](
t,
`file:/foo:happiness`,
func(data customTypeUnmarshalText, t *testing.T) {
assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "happiness", data.Name)
},
)
}
func TestMappingCustomPointerStructTypeUnmarshalText(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[*customTypeUnmarshalText](
t,
`file:/foo:happiness`,
func(data *customTypeUnmarshalText, t *testing.T) {
assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "happiness", data.Name)
},
)
}
type customPathUnmarshalText []string
func (p *customPathUnmarshalText) UnmarshalText(text []byte) error {
elems := strings.Split(string(text), "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
var _ encoding.TextUnmarshaler = (*customPathUnmarshalText)(nil)
func TestMappingCustomSliceUnmarshalText(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[customPathUnmarshalText](
t,
`bar/foo`,
func(path customPathUnmarshalText, t *testing.T) {
assert.EqualValues(t, "bar", path[0])
assert.EqualValues(t, "foo", path[1])
},
)
}
func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) {
var sForm struct {
Field1 customPathUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, sForm.Field1)
}
func TestMappingCustomSliceOfSliceUnmarshalText(t *testing.T) {
val := `bar/foo,bar/foo/spam`
expected := []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}
var sUri struct {
Field1 []customPathUnmarshalText `uri:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, expected, sUri.Field1)
var sForm struct {
Field1 []customPathUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sForm.Field1)
var sDefaultValue struct {
Field1 []customPathUnmarshalText `form:"field1,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sDefaultValue.Field1)
}
type objectIDUnmarshalText [12]byte
func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error {
oid, err := convertTo[objectIDUnmarshalText](string(text))
if err != nil {
return err
}
*o = oid
return nil
}
var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil)
func TestMappingCustomArrayUnmarshalText(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[objectIDUnmarshalText](
t,
`664a062ac74a8ad104e0e80f`,
func(oid objectIDUnmarshalText, t *testing.T) {
expected, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80f`)
assert.EqualValues(t, expected, oid)
},
)
}
func TestMappingCustomArrayOfArrayUnmarshalText(t *testing.T) {
val := `664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`
expected1, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80e`)
expected2, _ := convertTo[objectIDUnmarshalText](`664a062ac74a8ad104e0e80f`)
expected := []objectIDUnmarshalText{expected1, expected2}
var sUri struct {
Field1 []objectIDUnmarshalText `uri:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err := mappingByPtr(&sUri, formSource{"field1": {val}}, "uri")
require.NoError(t, err)
assert.EqualValues(t, expected, sUri.Field1)
var sForm struct {
Field1 []objectIDUnmarshalText `form:"field1,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err = mappingByPtr(&sForm, formSource{"field1": {val}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sForm.Field1)
var sDefaultValue struct {
Field1 []objectIDUnmarshalText `form:"field1,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
err = mappingByPtr(&sDefaultValue, formSource{"field1": {}}, "form")
require.NoError(t, err)
assert.EqualValues(t, expected, sDefaultValue.Field1)
}
// RunMappingUsingUriAndFormTagAndAssertForUnmarshalText declares a struct with a field of the given generic type T
// and runs a mapping test using the given value for both the uri and form tag. Any asserts that should be done on the
// result are passed as a function in the last parameter.
//
// This method eliminates the need for writing duplicate tests to verify both form+uri tags for TextUnmarshaler tests
func RunMappingUsingUriAndFormTagAndAssertForUnmarshalText[T any](
t *testing.T,
valueToBind string,
assertsToRunAfterBind func(T, *testing.T),
) {
var sUri struct {
Field1 T `uri:"field1,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&sUri, formSource{"field1": {valueToBind}}, "uri")
require.NoError(t, err)
assertsToRunAfterBind(sUri.Field1, t)
var sForm struct {
Field1 T `form:"field1,parser=encoding.TextUnmarshaler"`
}
err = mappingByPtr(&sForm, formSource{"field1": {valueToBind}}, "form")
require.NoError(t, err)
assertsToRunAfterBind(sForm.Field1, t)
}
// If someone specifies parser=TextUnmarshaler and it's not defined for the type, gin should revert to using its default
// binding logic.
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDefined(t *testing.T) {
var s struct {
Hex customHexUnmarshalParam `form:"hex"`
HexByUnmarshalText customHexUnmarshalParam `form:"hex2,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`f5`},
"hex2": {`f5`},
}, "form")
require.NoError(t, err)
expected, _ := convertTo(val)
assert.EqualValues(t, expected, s.FileData)
assert.EqualValues(t, 0xf5, s.Hex)
assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding
}
// If someone does not specify parser=TextUnmarshaler even when it's defined for the type, gin should ignore the
// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards
// compatibility)
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) {
var s struct {
Hex customHexUnmarshalText `form:"hex"`
HexByUnmarshalText customHexUnmarshalText `form:"hex2,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`11`},
"hex2": {`11`},
}, "form")
require.NoError(t, err)
assert.EqualValues(t, 11, s.Hex) // this is using default int binding, not our custom hex binding. 0x11 should be 17 in decimal
assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for hex binding
}
type customHexUnmarshalParamAndUnmarshalText int
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalParam(param string) error {
return errors.New("should not be called in unit test if parser tag present")
}
func (f *customHexUnmarshalParamAndUnmarshalText) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = customHexUnmarshalParamAndUnmarshalText(v)
return nil
}
// If a type has both UnmarshalParam and UnmarshalText methods defined, but the parser tag is set to TextUnmarshaler,
// then only the UnmarshalText method should be invoked.
func TestMappingUsingTextUnmarshalerWhenBindUnmarshalerAlsoDefined(t *testing.T) {
var s struct {
Hex customHexUnmarshalParamAndUnmarshalText `form:"hex,parser=encoding.TextUnmarshaler"`
}
err := mappingByPtr(&s, formSource{
"hex": {`f5`},
}, "form")
require.NoError(t, err)
assert.EqualValues(t, 0xf5, s.Hex)
}
// ==== TextUnmarshaler tests END ====

View File

@ -910,7 +910,7 @@ curl -X POST http://localhost:8080/person
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
@ -1008,12 +1008,68 @@ curl -v localhost:8088/thinkerou/not-uuid
### Bind custom unmarshaler
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
```go
package main
import (
"github.com/gin-gonic/gin"
"encoding"
"strings"
"github.com/gin-gonic/gin"
)
type Birthday string
func (b *Birthday) UnmarshalText(text []byte) error {
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
return nil
}
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request)
})
_ = route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
```
Result
```sh
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]
```
Note:
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
---
If a type implements `encoding.TextUnmarshaler` but you still want to customize how gin binds the type separately (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin.
```go
package main
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type Birthday string
@ -1023,29 +1079,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
return nil
}
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
func main() {
route := gin.Default()
var request struct {
Birthday Birthday `form:"birthday"`
Birthday Birthday `form:"birthday"`
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
}
route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday)
ctx.JSON(200, request)
})
route.Run(":8088")
_ = route.Run(":8088")
}
```
Test it with:
```sh
curl 'localhost:8088/test?birthday=2000-01-01'
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
```
Result
```sh
"2000/01/01"
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]
```
Note:
- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag.
- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client.
### Bind Header
```go