mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-04 09:48:17 +08:00
feat(binding): auto-bind types implementing encoding.TextUnmarshaler
Types like uuid.UUID implement encoding.TextUnmarshaler but previously required an explicit `parser=encoding.TextUnmarshaler` struct tag to be used during form/query/URI binding. This meant ShouldBind/ShouldBindQuery would fail with unhelpful errors like: ["45e1f85e-bca5-458d-bd9c-c56edd8f847b"] is not valid value for uuid.UUID This change makes trySetCustom automatically check for encoding.TextUnmarshaler as a fallback after BindUnmarshaler, so any type implementing TextUnmarshaler (uuid.UUID, net.IP, custom types, etc.) works out of the box with no extra tags. Precedence order: 1. BindUnmarshaler (UnmarshalParam) 2. encoding.TextUnmarshaler (UnmarshalText) 3. Default type-based binding time.Time is explicitly excluded from automatic TextUnmarshaler handling since gin has dedicated time parsing with time_format, time_utc, and time_location struct tags. Fixes #2423 Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
This commit is contained in:
parent
f5c267d2f8
commit
10b7b64912
@ -185,13 +185,23 @@ type BindUnmarshaler interface {
|
|||||||
UnmarshalParam(param string) error
|
UnmarshalParam(param string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// trySetCustom tries to set a custom type value
|
// trySetCustom tries to set a custom type value.
|
||||||
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
|
// It checks for BindUnmarshaler first, then falls back to encoding.TextUnmarshaler.
|
||||||
// to skip the default value setting.
|
// This allows types like uuid.UUID (which implement TextUnmarshaler) to be bound
|
||||||
|
// automatically without requiring an explicit parser tag.
|
||||||
|
//
|
||||||
|
// Note: time.Time is excluded from automatic TextUnmarshaler handling because gin
|
||||||
|
// provides dedicated time parsing via time_format, time_utc, and time_location tags.
|
||||||
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
||||||
switch v := value.Addr().Interface().(type) {
|
switch v := value.Addr().Interface().(type) {
|
||||||
case BindUnmarshaler:
|
case BindUnmarshaler:
|
||||||
return true, v.UnmarshalParam(val)
|
return true, v.UnmarshalParam(val)
|
||||||
|
case encoding.TextUnmarshaler:
|
||||||
|
// Skip time.Time — it has dedicated handling in setWithProperType via setTimeField,
|
||||||
|
// which supports time_format, time_utc, and time_location struct tags.
|
||||||
|
if _, isTime := value.Interface().(time.Time); !isTime {
|
||||||
|
return true, v.UnmarshalText([]byte(val))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1016,9 +1016,8 @@ func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyBindUnmarshalerDef
|
|||||||
assert.EqualValues(t, 0xf5, s.HexByUnmarshalText) // reverts to BindUnmarshaler binding
|
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
|
// When a type implements encoding.TextUnmarshaler, it is used automatically even without
|
||||||
// UnmarshalText logic and continue using its default binding logic. (This ensures gin does not break backwards
|
// the parser tag. Both fields should produce the same result.
|
||||||
// compatibility)
|
|
||||||
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) {
|
func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDefined(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
Hex customUnmarshalTextHex `form:"hex"`
|
Hex customUnmarshalTextHex `form:"hex"`
|
||||||
@ -1030,8 +1029,8 @@ func TestMappingUsingBindUnmarshalerAndTextUnmarshalerWhenOnlyTextUnmarshalerDef
|
|||||||
}, "form")
|
}, "form")
|
||||||
require.NoError(t, err)
|
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.Hex) // TextUnmarshaler is now used automatically
|
||||||
assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // correct expected value for normal hex binding
|
assert.EqualValues(t, 0x11, s.HexByUnmarshalText) // explicit parser tag also uses TextUnmarshaler
|
||||||
}
|
}
|
||||||
|
|
||||||
type customHexUnmarshalParamAndUnmarshalText int
|
type customHexUnmarshalParamAndUnmarshalText int
|
||||||
@ -1144,3 +1143,126 @@ func TestMappingEmptyValues(t *testing.T) {
|
|||||||
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
|
assert.Equal(t, []int{1, 2, 3}, s.SliceCsv)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==== Automatic TextUnmarshaler binding tests (no parser tag required) ====
|
||||||
|
|
||||||
|
// testUUID mimics uuid.UUID — a [16]byte array implementing encoding.TextUnmarshaler
|
||||||
|
type testUUID [16]byte
|
||||||
|
|
||||||
|
func (u *testUUID) UnmarshalText(text []byte) error {
|
||||||
|
s := string(text)
|
||||||
|
// simple hex-dash parser for 8-4-4-4-12 format
|
||||||
|
s = strings.ReplaceAll(s, "-", "")
|
||||||
|
if len(s) != 32 {
|
||||||
|
return errors.New("invalid UUID length: " + strconv.Itoa(len(s)))
|
||||||
|
}
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
b, err := strconv.ParseUint(s[i*2:i*2+2], 16, 8)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid UUID hex at position " + strconv.Itoa(i*2))
|
||||||
|
}
|
||||||
|
u[i] = byte(b)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ encoding.TextUnmarshaler = (*testUUID)(nil)
|
||||||
|
|
||||||
|
func mustParseTestUUID(s string) testUUID {
|
||||||
|
var u testUUID
|
||||||
|
if err := u.UnmarshalText([]byte(s)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindForm(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
ID testUUID `form:"id"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"id": {"45e1f85e-bca5-458d-bd9c-c56edd8f847b"}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, mustParseTestUUID("45e1f85e-bca5-458d-bd9c-c56edd8f847b"), s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindURI(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
ID testUUID `uri:"id"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"id": {"45e1f85e-bca5-458d-bd9c-c56edd8f847b"}}, "uri")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, mustParseTestUUID("45e1f85e-bca5-458d-bd9c-c56edd8f847b"), s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindSlice(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
IDs []testUUID `form:"ids" collection_format:"csv"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"ids": {"45e1f85e-bca5-458d-bd9c-c56edd8f847b,68dc3815-6ab5-4883-81a1-96eff25b659f"}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := []testUUID{
|
||||||
|
mustParseTestUUID("45e1f85e-bca5-458d-bd9c-c56edd8f847b"),
|
||||||
|
mustParseTestUUID("68dc3815-6ab5-4883-81a1-96eff25b659f"),
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, s.IDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindMultipleValues(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
IDs []testUUID `form:"ids"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"ids": {
|
||||||
|
"45e1f85e-bca5-458d-bd9c-c56edd8f847b",
|
||||||
|
"68dc3815-6ab5-4883-81a1-96eff25b659f",
|
||||||
|
}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
expected := []testUUID{
|
||||||
|
mustParseTestUUID("45e1f85e-bca5-458d-bd9c-c56edd8f847b"),
|
||||||
|
mustParseTestUUID("68dc3815-6ab5-4883-81a1-96eff25b659f"),
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, s.IDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindDefault(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
ID testUUID `form:"id,default=45e1f85e-bca5-458d-bd9c-c56edd8f847b"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, mustParseTestUUID("45e1f85e-bca5-458d-bd9c-c56edd8f847b"), s.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingTextUnmarshalerAutoBindInvalidValue(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
ID testUUID `form:"id"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"id": {"not-a-uuid"}}, "form")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindUnmarshaler should take precedence over TextUnmarshaler
|
||||||
|
type testDualUnmarshaler struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *testDualUnmarshaler) UnmarshalParam(param string) error {
|
||||||
|
d.Value = "param:" + param
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *testDualUnmarshaler) UnmarshalText(text []byte) error {
|
||||||
|
d.Value = "text:" + string(text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ BindUnmarshaler = (*testDualUnmarshaler)(nil)
|
||||||
|
var _ encoding.TextUnmarshaler = (*testDualUnmarshaler)(nil)
|
||||||
|
|
||||||
|
func TestMappingBindUnmarshalerTakesPrecedenceOverTextUnmarshaler(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Field testDualUnmarshaler `form:"field"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"field": {"hello"}}, "form")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "param:hello", s.Field.Value) // BindUnmarshaler wins
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user