mirror of
https://github.com/gin-gonic/gin.git
synced 2025-04-06 03:57:46 +08:00
Merge 842bd597a8d668e7065b179280571eac2f616f0d into 8763f33c65f7df8be5b9fe7504ab7fcf20abb41d
This commit is contained in:
commit
1ff750e1d1
@ -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
|
||||
}
|
||||
|
@ -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 ====
|
||||
|
78
docs/doc.md
78
docs/doc.md
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user