gin/binding/form_mapping_test.go

963 lines
29 KiB
Go

// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"encoding"
"encoding/hex"
"errors"
"mime/multipart"
"reflect"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMappingBaseTypes(t *testing.T) {
intPtr := func(i int) *int {
return &i
}
for _, tt := range []struct {
name string
value any
form string
expect any
}{
{"base type", struct{ F int }{}, "9", int(9)},
{"base type", struct{ F int8 }{}, "9", int8(9)},
{"base type", struct{ F int16 }{}, "9", int16(9)},
{"base type", struct{ F int32 }{}, "9", int32(9)},
{"base type", struct{ F int64 }{}, "9", int64(9)},
{"base type", struct{ F uint }{}, "9", uint(9)},
{"base type", struct{ F uint8 }{}, "9", uint8(9)},
{"base type", struct{ F uint16 }{}, "9", uint16(9)},
{"base type", struct{ F uint32 }{}, "9", uint32(9)},
{"base type", struct{ F uint64 }{}, "9", uint64(9)},
{"base type", struct{ F bool }{}, "True", true},
{"base type", struct{ F float32 }{}, "9.1", float32(9.1)},
{"base type", struct{ F float64 }{}, "9.1", float64(9.1)},
{"base type", struct{ F string }{}, "test", string("test")},
{"base type", struct{ F *int }{}, "9", intPtr(9)},
// zero values
{"zero value", struct{ F int }{}, "", int(0)},
{"zero value", struct{ F uint }{}, "", uint(0)},
{"zero value", struct{ F bool }{}, "", false},
{"zero value", struct{ F float32 }{}, "", float32(0)},
{"file value", struct{ F *multipart.FileHeader }{}, "", &multipart.FileHeader{}},
} {
tp := reflect.TypeOf(tt.value)
testName := tt.name + ":" + tp.Field(0).Type.String()
val := reflect.New(reflect.TypeOf(tt.value))
val.Elem().Set(reflect.ValueOf(tt.value))
field := val.Elem().Type().Field(0)
_, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form")
require.NoError(t, err, testName)
actual := val.Elem().Field(0).Interface()
assert.Equal(t, tt.expect, actual, testName)
}
}
func TestMappingDefault(t *testing.T) {
var s struct {
Str string `form:",default=defaultVal"`
Int int `form:",default=9"`
Slice []int `form:",default=9"`
Array [1]int `form:",default=9"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, "defaultVal", s.Str)
assert.Equal(t, 9, s.Int)
assert.Equal(t, []int{9}, s.Slice)
assert.Equal(t, [1]int{9}, s.Array)
}
func TestMappingSkipField(t *testing.T) {
var s struct {
A int
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, 0, s.A)
}
func TestMappingIgnoreField(t *testing.T) {
var s struct {
A int `form:"A"`
B int `form:"-"`
}
err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form")
require.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.B)
}
func TestMappingUnexportedField(t *testing.T) {
var s struct {
A int `form:"a"`
b int `form:"b"`
}
err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form")
require.NoError(t, err)
assert.Equal(t, 9, s.A)
assert.Equal(t, 0, s.b)
}
func TestMappingPrivateField(t *testing.T) {
var s struct {
f int `form:"field"`
}
err := mappingByPtr(&s, formSource{"field": {"6"}}, "form")
require.NoError(t, err)
assert.Equal(t, 0, s.f)
}
func TestMappingUnknownFieldType(t *testing.T) {
var s struct {
U uintptr
}
err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form")
require.Error(t, err)
assert.Equal(t, errUnknownType, err)
}
func TestMappingURI(t *testing.T) {
var s struct {
F int `uri:"field"`
}
err := mapURI(&s, map[string][]string{"field": {"6"}})
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
func TestMappingForm(t *testing.T) {
var s struct {
F int `form:"field"`
}
err := mapForm(&s, map[string][]string{"field": {"6"}})
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
func TestMappingFormFieldNotSent(t *testing.T) {
var s struct {
F string `form:"field,default=defVal"`
}
err := mapForm(&s, map[string][]string{})
require.NoError(t, err)
assert.Equal(t, "defVal", s.F)
}
func TestMappingFormWithEmptyToDefault(t *testing.T) {
var s struct {
F string `form:"field,default=DefVal"`
}
err := mapForm(&s, map[string][]string{"field": {""}})
require.NoError(t, err)
assert.Equal(t, "DefVal", s.F)
}
func TestMapFormWithTag(t *testing.T) {
var s struct {
F int `externalTag:"field"`
}
err := MapFormWithTag(&s, map[string][]string{"field": {"6"}}, "externalTag")
require.NoError(t, err)
assert.Equal(t, 6, s.F)
}
func TestMappingTime(t *testing.T) {
var s struct {
Time time.Time
LocalTime time.Time `time_format:"2006-01-02"`
ZeroValue time.Time
ZeroUnixTime time.Time `time_format:"unix"`
ZeroUnixNanoTime time.Time `time_format:"unixnano"`
CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"`
UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"`
}
var err error
time.Local, err = time.LoadLocation("Europe/Berlin")
require.NoError(t, err)
err = mapForm(&s, map[string][]string{
"Time": {"2019-01-20T16:02:58Z"},
"LocalTime": {"2019-01-20"},
"ZeroValue": {},
"ZeroUnixTime": {},
"ZeroUnixNanoTime": {},
"CSTTime": {"2019-01-20"},
"UTCTime": {"2019-01-20"},
})
require.NoError(t, err)
assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String())
assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String())
assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String())
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String())
assert.Equal(t, "1970-01-01 00:00:00 +0000 UTC", s.ZeroUnixTime.UTC().String())
assert.Equal(t, "1970-01-01 00:00:00 +0000 UTC", s.ZeroUnixNanoTime.UTC().String())
assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String())
assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String())
assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String())
// wrong location
var wrongLoc struct {
Time time.Time `time_location:"wrong"`
}
err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}})
require.Error(t, err)
// wrong time value
var wrongTime struct {
Time time.Time
}
err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}})
require.Error(t, err)
}
func TestMappingTimeDuration(t *testing.T) {
var s struct {
D time.Duration
}
// ok
err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form")
require.NoError(t, err)
assert.Equal(t, 5*time.Second, s.D)
// error
err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form")
require.Error(t, err)
}
func TestMappingSlice(t *testing.T) {
var s struct {
Slice []int `form:"slice,default=9"`
}
// default value
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{9}, s.Slice)
// ok
err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form")
require.NoError(t, err)
assert.Equal(t, []int{3, 4}, s.Slice)
// error
err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form")
require.Error(t, err)
}
func TestMappingArray(t *testing.T) {
var s struct {
Array [2]int `form:"array,default=9"`
}
// wrong default
err := mappingByPtr(&s, formSource{}, "form")
require.Error(t, err)
// ok
err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form")
require.NoError(t, err)
assert.Equal(t, [2]int{3, 4}, s.Array)
// error - not enough vals
err = mappingByPtr(&s, formSource{"array": {"3"}}, "form")
require.Error(t, err)
// error - wrong value
err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form")
require.Error(t, err)
}
func TestMappingCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:"slice_multi" collection_format:"multi"`
SliceCsv []int `form:"slice_csv" collection_format:"csv"`
SliceSsv []int `form:"slice_ssv" collection_format:"ssv"`
SliceTsv []int `form:"slice_tsv" collection_format:"tsv"`
SlicePipes []int `form:"slice_pipes" collection_format:"pipes"`
ArrayMulti [2]int `form:"array_multi" collection_format:"multi"`
ArrayCsv [2]int `form:"array_csv" collection_format:"csv"`
ArraySsv [2]int `form:"array_ssv" collection_format:"ssv"`
ArrayTsv [2]int `form:"array_tsv" collection_format:"tsv"`
ArrayPipes [2]int `form:"array_pipes" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{
"slice_multi": {"1", "2"},
"slice_csv": {"1,2"},
"slice_ssv": {"1 2"},
"slice_tsv": {"1 2"},
"slice_pipes": {"1|2"},
"array_multi": {"1", "2"},
"array_csv": {"1,2"},
"array_ssv": {"1 2"},
"array_tsv": {"1 2"},
"array_pipes": {"1|2"},
}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2}, s.SliceMulti)
assert.Equal(t, []int{1, 2}, s.SliceCsv)
assert.Equal(t, []int{1, 2}, s.SliceSsv)
assert.Equal(t, []int{1, 2}, s.SliceTsv)
assert.Equal(t, []int{1, 2}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
}
func TestMappingCollectionFormatInvalid(t *testing.T) {
var s struct {
SliceCsv []int `form:"slice_csv" collection_format:"xxx"`
}
err := mappingByPtr(&s, formSource{
"slice_csv": {"1,2"},
}, "form")
require.Error(t, err)
var s2 struct {
ArrayCsv [2]int `form:"array_csv" collection_format:"xxx"`
}
err = mappingByPtr(&s2, formSource{
"array_csv": {"1,2"},
}, "form")
require.Error(t, err)
}
func TestMappingMultipleDefaultWithCollectionFormat(t *testing.T) {
var s struct {
SliceMulti []int `form:",default=1;2;3" collection_format:"multi"`
SliceCsv []int `form:",default=1;2;3" collection_format:"csv"`
SliceSsv []int `form:",default=1 2 3" collection_format:"ssv"`
SliceTsv []int `form:",default=1\t2\t3" collection_format:"tsv"`
SlicePipes []int `form:",default=1|2|3" collection_format:"pipes"`
ArrayMulti [2]int `form:",default=1;2" collection_format:"multi"`
ArrayCsv [2]int `form:",default=1;2" collection_format:"csv"`
ArraySsv [2]int `form:",default=1 2" collection_format:"ssv"`
ArrayTsv [2]int `form:",default=1\t2" collection_format:"tsv"`
ArrayPipes [2]int `form:",default=1|2" collection_format:"pipes"`
SliceStringMulti []string `form:",default=1;2;3" collection_format:"multi"`
SliceStringCsv []string `form:",default=1;2;3" collection_format:"csv"`
SliceStringSsv []string `form:",default=1 2 3" collection_format:"ssv"`
SliceStringTsv []string `form:",default=1\t2\t3" collection_format:"tsv"`
SliceStringPipes []string `form:",default=1|2|3" collection_format:"pipes"`
ArrayStringMulti [2]string `form:",default=1;2" collection_format:"multi"`
ArrayStringCsv [2]string `form:",default=1;2" collection_format:"csv"`
ArrayStringSsv [2]string `form:",default=1 2" collection_format:"ssv"`
ArrayStringTsv [2]string `form:",default=1\t2" collection_format:"tsv"`
ArrayStringPipes [2]string `form:",default=1|2" collection_format:"pipes"`
}
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, s.SliceMulti)
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceSsv)
assert.Equal(t, []int{1, 2, 3}, s.SliceTsv)
assert.Equal(t, []int{1, 2, 3}, s.SlicePipes)
assert.Equal(t, [2]int{1, 2}, s.ArrayMulti)
assert.Equal(t, [2]int{1, 2}, s.ArrayCsv)
assert.Equal(t, [2]int{1, 2}, s.ArraySsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayTsv)
assert.Equal(t, [2]int{1, 2}, s.ArrayPipes)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringMulti)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringCsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringSsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringTsv)
assert.Equal(t, []string{"1", "2", "3"}, s.SliceStringPipes)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringMulti)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringCsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringSsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringTsv)
assert.Equal(t, [2]string{"1", "2"}, s.ArrayStringPipes)
}
func TestMappingStructField(t *testing.T) {
var s struct {
J struct {
I int
}
}
err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form")
require.NoError(t, err)
assert.Equal(t, 9, s.J.I)
}
func TestMappingPtrField(t *testing.T) {
type ptrStruct struct {
Key int64 `json:"key"`
}
type ptrRequest struct {
Items []*ptrStruct `json:"items" form:"items"`
}
var err error
// With 0 items.
var req0 ptrRequest
err = mappingByPtr(&req0, formSource{}, "form")
require.NoError(t, err)
assert.Empty(t, req0.Items)
// With 1 item.
var req1 ptrRequest
err = mappingByPtr(&req1, formSource{"items": {`{"key": 1}`}}, "form")
require.NoError(t, err)
assert.Len(t, req1.Items, 1)
assert.EqualValues(t, 1, req1.Items[0].Key)
// With 2 items.
var req2 ptrRequest
err = mappingByPtr(&req2, formSource{"items": {`{"key": 1}`, `{"key": 2}`}}, "form")
require.NoError(t, err)
assert.Len(t, req2.Items, 2)
assert.EqualValues(t, 1, req2.Items[0].Key)
assert.EqualValues(t, 2, req2.Items[1].Key)
}
func TestMappingMapField(t *testing.T) {
var s struct {
M map[string]int
}
err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form")
require.NoError(t, err)
assert.Equal(t, map[string]int{"one": 1}, s.M)
}
func TestMappingIgnoredCircularRef(t *testing.T) {
type S struct {
S *S `form:"-"`
}
var s S
err := mappingByPtr(&s, formSource{}, "form")
require.NoError(t, err)
}
// ==== BindUmarshaler tests START ====
type customHexUnmarshalParam int
func (f *customHexUnmarshalParam) UnmarshalParam(param string) error {
v, err := strconv.ParseInt(param, 16, 64)
if err != nil {
return err
}
*f = customHexUnmarshalParam(v)
return nil
}
func TestMappingCustomHexUnmarshalParam(t *testing.T) {
RunMappingUsingUriAndFormTagAndAssertForUnmarshalParam[customHexUnmarshalParam](
t,
`f5`,
func(hex customHexUnmarshalParam, t *testing.T) {
assert.EqualValues(t, 245, hex)
},
)
// 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)
}
type customTypeUnmarshalParam struct {
Protocol string
Path string
Name string
}
func (f *customTypeUnmarshalParam) UnmarshalParam(param string) error {
parts := strings.Split(param, ":")
if len(parts) != 3 {
return errors.New("invalid format")
}
f.Protocol = parts[0]
f.Path = parts[1]
f.Name = parts[2]
return nil
}
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 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
func (p *customPath) UnmarshalParam(param string) error {
elems := strings.Split(param, "/")
n := len(elems)
if n < 2 {
return errors.New("invalid format")
}
*p = elems
return nil
}
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 TestMappingCustomSliceStopsWhenError(t *testing.T) {
var sForm struct {
Field1 customPath `form:"field1"`
}
err := mappingByPtr(&sForm, formSource{"field1": {"invalid"}}, "form")
require.ErrorContains(t, err, "invalid format")
require.Empty(t, sForm.Field1)
}
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[objectID](param)
if err != nil {
return err
}
*o = oid
return nil
}
func convertTo[T ~[12]byte](s string) (T, error) {
var nilObjectID T
if len(s) != 24 {
return nilObjectID, errors.New("invalid format")
}
var oid [12]byte
_, err := hex.Decode(oid[:], []byte(s))
if err != nil {
return nilObjectID, err
}
return oid, nil
}
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 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"`
}
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)
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 ====