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 package binding
import ( import (
"encoding"
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
@ -136,6 +137,7 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
type setOptions struct { type setOptions struct {
isDefaultExists bool isDefaultExists bool
defaultValue string defaultValue string
parser string
} }
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { 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, ";", ",") 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 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) { func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
cfTag := field.Tag.Get("collection_format") cfTag := field.Tag.Get("collection_format")
if cfTag == "" || cfTag == "multi" { if cfTag == "" || cfTag == "multi" {
@ -207,7 +225,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
case "pipes": case "pipes":
sep = "|" sep = "|"
default: 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 totalLength := 0
@ -230,7 +248,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
switch value.Kind() { switch value.Kind() {
case reflect.Slice: case reflect.Slice:
if !ok { if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present // 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 return ok, err
} }
@ -248,9 +268,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
return false, err return false, err
} }
return true, setSlice(vs, value, field) return true, setSlice(vs, value, field, opt)
case reflect.Array: case reflect.Array:
if !ok { if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
vs = []string{opt.defaultValue} vs = []string{opt.defaultValue}
// pre-process the default value for multi if present // 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 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 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: default:
var val string var val string
if !ok { if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
val = opt.defaultValue val = opt.defaultValue
} else if len(vs) > 0 {
val = vs[0]
} }
if len(vs) > 0 { if ok, err = trySetUsingParser(val, value, opt.parser); ok {
val = vs[0] return ok, err
if val == "" { } else if ok, err = trySetCustom(val, value); ok {
val = opt.defaultValue
}
}
if ok, err := trySetCustom(val, value); ok {
return ok, err 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() { switch value.Kind() {
case reflect.Int: case reflect.Int:
return setIntField(val, 0, value) return setIntField(val, 0, value)
@ -340,7 +367,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
if !value.Elem().IsValid() { if !value.Elem().IsValid() {
value.Set(reflect.New(value.Type().Elem())) value.Set(reflect.New(value.Type().Elem()))
} }
return setWithProperType(val, value.Elem(), field) return setWithProperType(val, value.Elem(), field, opt)
default: default:
return errUnknownType return errUnknownType
} }
@ -447,9 +474,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
return nil 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 { for i, s := range vals {
err := setWithProperType(s, value.Index(i), field) err := setWithProperType(s, value.Index(i), field, opt)
if err != nil { if err != nil {
return err return err
} }
@ -457,9 +484,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
return nil 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)) slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
err := setArray(vals, slice, field) err := setArray(vals, slice, field, opt)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,6 +5,7 @@
package binding package binding
import ( import (
"encoding"
"encoding/hex" "encoding/hex"
"errors" "errors"
"mime/multipart" "mime/multipart"
@ -454,44 +455,44 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
require.NoError(t, err) 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) v, err := strconv.ParseInt(param, 16, 64)
if err != nil { if err != nil {
return err return err
} }
*f = customUnmarshalParamHex(v) *f = customHexUnmarshalParam(v)
return nil return nil
} }
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) { func TestMappingCustomHexUnmarshalParam(t *testing.T) {
var s struct { RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customHexUnmarshalParam](
Foo customUnmarshalParamHex `form:"foo"` t,
} `f5`,
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form") func(hex customHexUnmarshalParam, t *testing.T) {
require.NoError(t, err) 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) { type customTypeUnmarshalParam struct {
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 {
Protocol string Protocol string
Path string Path string
Name string Name string
} }
func (f *customUnmarshalParamType) UnmarshalParam(param string) error { func (f *customTypeUnmarshalParam) UnmarshalParam(param string) error {
parts := strings.Split(param, ":") parts := strings.Split(param, ":")
if len(parts) != 3 { if len(parts) != 3 {
return errors.New("invalid format") return errors.New("invalid format")
@ -502,52 +503,28 @@ func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
return nil return nil
} }
func TestMappingCustomStructTypeWithFormTag(t *testing.T) { func TestMappingCustomStructType(t *testing.T) {
var s struct { RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customTypeUnmarshalParam](
FileData customUnmarshalParamType `form:"data"` t,
} `file:/foo:happiness`,
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form") func(data customTypeUnmarshalParam, t *testing.T) {
require.NoError(t, err) assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.EqualValues(t, "happiness", data.Name)
assert.EqualValues(t, "/foo", s.FileData.Path) },
assert.EqualValues(t, "happiness", s.FileData.Name) )
} }
func TestMappingCustomStructTypeWithURITag(t *testing.T) { func TestMappingCustomPointerStructType(t *testing.T) {
var s struct { RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[*customTypeUnmarshalParam](
FileData customUnmarshalParamType `uri:"data"` t,
} `file:/foo:happiness`,
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri") func(data *customTypeUnmarshalParam, t *testing.T) {
require.NoError(t, err) assert.EqualValues(t, "file", data.Protocol)
assert.EqualValues(t, "/foo", data.Path)
assert.EqualValues(t, "file", s.FileData.Protocol) assert.EqualValues(t, "happiness", data.Name)
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)
} }
type customPath []string type customPath []string
@ -563,32 +540,49 @@ func (p *customPath) UnmarshalParam(param string) error {
return nil return nil
} }
func TestMappingCustomSliceUri(t *testing.T) { func TestMappingCustomSlice(t *testing.T) {
var s struct { RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customPath](
FileData customPath `uri:"path"` t,
} `bar/foo`,
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri") func(path customPath, t *testing.T) {
require.NoError(t, err) assert.EqualValues(t, "bar", path[0])
assert.EqualValues(t, "foo", path[1])
assert.EqualValues(t, "bar", s.FileData[0]) },
assert.EqualValues(t, "foo", s.FileData[1]) )
} }
func TestMappingCustomSliceForm(t *testing.T) { func TestMappingCustomSliceStopsWhenError(t *testing.T) {
var s struct { var sForm struct {
FileData customPath `form:"path"` Field1 customPath `form:"field1"`
} }
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form") err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
require.NoError(t, err) require.ErrorContains(t, err, "invalid format")
require.Empty(t, sForm.Field1)
}
assert.EqualValues(t, "bar", s.FileData[0]) func TestMappingCustomSliceOfSlice(t *testing.T) {
assert.EqualValues(t, "foo", s.FileData[1]) 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 type objectID [12]byte
func (o *objectID) UnmarshalParam(param string) error { func (o *objectID) UnmarshalParam(param string) error {
oid, err := convertTo(param) oid, err := convertTo[objectID](param)
if err != nil { if err != nil {
return err return err
} }
@ -597,8 +591,8 @@ func (o *objectID) UnmarshalParam(param string) error {
return nil return nil
} }
func convertTo(s string) (objectID, error) { func convertTo[T ~[12]byte](s string) (T, error) {
var nilObjectID objectID var nilObjectID T
if len(s) != 24 { if len(s) != 24 {
return nilObjectID, errors.New("invalid format") return nilObjectID, errors.New("invalid format")
} }
@ -612,26 +606,351 @@ func convertTo(s string) (objectID, error) {
return oid, nil return oid, nil
} }
func TestMappingCustomArrayUri(t *testing.T) { func TestMappingCustomArray(t *testing.T) {
var s struct { RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[objectID](
FileData objectID `uri:"id"` t,
} `664a062ac74a8ad104e0e80f`,
val := `664a062ac74a8ad104e0e80f` func(oid objectID, t *testing.T) {
err := mappingByPtr(&s, formSource{"id": {val}}, "uri") expected, _ := convertTo[objectID](`664a062ac74a8ad104e0e80f`)
require.NoError(t, err) assert.EqualValues(t, expected, oid)
},
expected, _ := convertTo(val) )
assert.EqualValues(t, expected, s.FileData)
} }
func TestMappingCustomArrayForm(t *testing.T) { func TestMappingCustomArrayOfArray(t *testing.T) {
var s struct { val := `664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`
FileData objectID `form:"id"` 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(&sUri, formSource{"field1": {val}}, "uri")
err := mappingByPtr(&s, formSource{"id": {val}}, "form") 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) require.NoError(t, err)
expected, _ := convertTo(val) assert.EqualValues(t, 0xf5, s.Hex)
assert.EqualValues(t, expected, s.FileData) 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: 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 - 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" - 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 ### 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 ```go
package main package main
import ( import (
"github.com/gin-gonic/gin" "encoding"
"strings" "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 type Birthday string
@ -1023,29 +1079,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
return nil return nil
} }
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
func main() { func main() {
route := gin.Default() route := gin.Default()
var request struct { 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) { route.GET("/test", func(ctx *gin.Context) {
_ = ctx.BindQuery(&request) _ = ctx.BindQuery(&request)
ctx.JSON(200, request.Birthday) ctx.JSON(200, request)
}) })
route.Run(":8088") _ = route.Run(":8088")
} }
``` ```
Test it with: Test it with:
```sh ```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 Result
```sh ```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 ### Bind Header
```go ```go