mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-12 00:28:37 +08:00
Compare commits
1 Commits
78e4fe63d8
...
28eed40da2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28eed40da2 |
@ -5,7 +5,6 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
@ -138,8 +137,6 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
||||
type setOptions struct {
|
||||
isDefaultExists bool
|
||||
defaultValue string
|
||||
// parser specifies what interface to use for reading the request & default values (e.g. `encoding.TextUnmarshaler`)
|
||||
parser string
|
||||
}
|
||||
|
||||
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
||||
@ -171,8 +168,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,20 +191,6 @@ 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" {
|
||||
@ -227,7 +208,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. (multi, csv, ssv, tsv, pipes)", cfTag)
|
||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
|
||||
}
|
||||
|
||||
totalLength := 0
|
||||
@ -263,9 +244,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
@ -273,7 +252,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, setSlice(vs, value, field, opt)
|
||||
return true, setSlice(vs, value, field)
|
||||
case reflect.Array:
|
||||
if len(vs) == 0 {
|
||||
if !opt.isDefaultExists {
|
||||
@ -288,9 +267,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||
if ok, err = trySetCustom(vs[0], value); ok {
|
||||
return ok, err
|
||||
}
|
||||
|
||||
@ -302,32 +279,27 @@ 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, opt)
|
||||
return true, setArray(vs, value, field)
|
||||
default:
|
||||
var val string
|
||||
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||
if !ok {
|
||||
val = opt.defaultValue
|
||||
} else if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
}
|
||||
|
||||
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||
return ok, err
|
||||
} else if ok, err = trySetCustom(val, value); ok {
|
||||
if len(vs) > 0 {
|
||||
val = vs[0]
|
||||
if val == "" {
|
||||
val = opt.defaultValue
|
||||
}
|
||||
}
|
||||
if ok, err := trySetCustom(val, value); ok {
|
||||
return ok, err
|
||||
}
|
||||
return true, setWithProperType(val, value, field, opt)
|
||||
return true, setWithProperType(val, value, field)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
||||
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||
if value.Kind() != reflect.String {
|
||||
val = strings.TrimSpace(val)
|
||||
@ -380,7 +352,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, opt)
|
||||
return setWithProperType(val, value.Elem(), field)
|
||||
default:
|
||||
return errUnknownType
|
||||
}
|
||||
@ -487,9 +459,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
return nil
|
||||
}
|
||||
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
for i, s := range vals {
|
||||
err := setWithProperType(s, value.Index(i), field, opt)
|
||||
err := setWithProperType(s, value.Index(i), field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -497,9 +469,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField, opt
|
||||
return nil
|
||||
}
|
||||
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
|
||||
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||
err := setArray(vals, slice, field, opt)
|
||||
err := setArray(vals, slice, field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
package binding
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"mime/multipart"
|
||||
@ -525,16 +524,6 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
||||
assert.EqualValues(t, 245, s.Foo)
|
||||
}
|
||||
|
||||
func TestMappingCustomUnmarshalParamHexDefault(t *testing.T) {
|
||||
var s struct {
|
||||
Foo customUnmarshalParamHex `form:"foo,default=f5"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"foo": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, 0xf5, s.Foo)
|
||||
}
|
||||
|
||||
type customUnmarshalParamType struct {
|
||||
Protocol string
|
||||
Path string
|
||||
@ -635,33 +624,6 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceStopsWhenError(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPath `form:"path"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
|
||||
require.ErrorContains(t, err, "invalid format")
|
||||
require.Empty(t, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData []customPath `uri:"path" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData []customPath `form:"path" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []customPath{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||
}
|
||||
|
||||
type objectID [12]byte
|
||||
|
||||
func (o *objectID) UnmarshalParam(param string) error {
|
||||
@ -713,358 +675,6 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayUri(t *testing.T) {
|
||||
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
|
||||
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
|
||||
|
||||
var s struct {
|
||||
FileData []objectID `uri:"ids" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []objectID{id1, id2}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayForm(t *testing.T) {
|
||||
id1, _ := convertTo(`664a062ac74a8ad104e0e80e`)
|
||||
id2, _ := convertTo(`664a062ac74a8ad104e0e80f`)
|
||||
|
||||
var s struct {
|
||||
FileData []objectID `form:"ids" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []objectID{id1, id2}, s.FileData)
|
||||
}
|
||||
|
||||
// ==== TextUnmarshaler tests START ====
|
||||
|
||||
type customUnmarshalTextHex int
|
||||
|
||||
func (f *customUnmarshalTextHex) UnmarshalText(text []byte) error {
|
||||
v, err := strconv.ParseInt(string(text), 16, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*f = customUnmarshalTextHex(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify type implements TextUnmarshaler
|
||||
var _ encoding.TextUnmarshaler = (*customUnmarshalTextHex)(nil)
|
||||
|
||||
func TestMappingCustomUnmarshalTextHexUri(t *testing.T) {
|
||||
var s struct {
|
||||
Field customUnmarshalTextHex `uri:"field,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 245, s.Field)
|
||||
}
|
||||
|
||||
func TestMappingCustomUnmarshalTextHexForm(t *testing.T) {
|
||||
var s struct {
|
||||
Field customUnmarshalTextHex `form:"field,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"field": {`f5`}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 245, s.Field)
|
||||
}
|
||||
|
||||
func TestMappingCustomUnmarshalTextHexDefault(t *testing.T) {
|
||||
var s struct {
|
||||
Field customUnmarshalTextHex `form:"field,default=f5,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"field1": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0xf5, s.Field)
|
||||
}
|
||||
|
||||
type customUnmarshalTextType struct {
|
||||
Protocol string
|
||||
Path string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (f *customUnmarshalTextType) 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 = (*customUnmarshalTextType)(nil)
|
||||
|
||||
func TestMappingCustomStructTypeUnmarshalTextForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomStructTypeUnmarshalTextUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeUnmarshalTextForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData *customUnmarshalTextType `form:"data,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.Name)
|
||||
}
|
||||
|
||||
func TestMappingCustomPointerStructTypeUnmarshalTextUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData *customUnmarshalTextType `uri:"data,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "file", s.FileData.Protocol)
|
||||
assert.Equal(t, "/foo", s.FileData.Path)
|
||||
assert.Equal(t, "happiness", s.FileData.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 TestMappingCustomSliceUnmarshalTextUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceUnmarshalTextForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "bar", s.FileData[0])
|
||||
assert.Equal(t, "foo", s.FileData[1])
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceUnmarshalTextStopsWhenError(t *testing.T) {
|
||||
var s struct {
|
||||
FileData customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"invalid"}}, "form")
|
||||
require.ErrorContains(t, err, "invalid format")
|
||||
require.Empty(t, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceUnmarshalTextUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData []customPathUnmarshalText `uri:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceUnmarshalTextForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData []customPathUnmarshalText `form:"path,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {"bar/foo,bar/foo/spam"}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomSliceOfSliceUnmarshalTextDefault(t *testing.T) {
|
||||
var s struct {
|
||||
FileData []customPathUnmarshalText `form:"path,default=bar/foo;bar/foo/spam,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"path": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []customPathUnmarshalText{{"bar", "foo"}, {"bar", "foo", "spam"}}, s.FileData)
|
||||
}
|
||||
|
||||
type objectIDUnmarshalText [12]byte
|
||||
|
||||
func (o *objectIDUnmarshalText) UnmarshalText(text []byte) error {
|
||||
oid, err := convertToOidUnmarshalText(string(text))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = oid
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToOidUnmarshalText(s string) (objectIDUnmarshalText, error) {
|
||||
oid, err := convertTo(s)
|
||||
return objectIDUnmarshalText(oid), err
|
||||
}
|
||||
|
||||
var _ encoding.TextUnmarshaler = (*objectIDUnmarshalText)(nil)
|
||||
|
||||
func TestMappingCustomArrayUnmarshalTextUri(t *testing.T) {
|
||||
var s struct {
|
||||
FileData objectIDUnmarshalText `uri:"id,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
val := `664a062ac74a8ad104e0e80f`
|
||||
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertToOidUnmarshalText(val)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayUnmarshalTextForm(t *testing.T) {
|
||||
var s struct {
|
||||
FileData objectIDUnmarshalText `form:"id,parser=encoding.TextUnmarshaler"`
|
||||
}
|
||||
val := `664a062ac74a8ad104e0e80f`
|
||||
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, _ := convertToOidUnmarshalText(val)
|
||||
assert.Equal(t, expected, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayUnmarshalTextUri(t *testing.T) {
|
||||
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
|
||||
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
|
||||
|
||||
var s struct {
|
||||
FileData []objectIDUnmarshalText `uri:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "uri")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayUnmarshalTextForm(t *testing.T) {
|
||||
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
|
||||
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
|
||||
|
||||
var s struct {
|
||||
FileData []objectIDUnmarshalText `form:"ids,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"ids": {`664a062ac74a8ad104e0e80e,664a062ac74a8ad104e0e80f`}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
|
||||
}
|
||||
|
||||
func TestMappingCustomArrayOfArrayUnmarshalTextDefault(t *testing.T) {
|
||||
id1, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80e`)
|
||||
id2, _ := convertToOidUnmarshalText(`664a062ac74a8ad104e0e80f`)
|
||||
|
||||
var s struct {
|
||||
FileData []objectIDUnmarshalText `form:"ids,default=664a062ac74a8ad104e0e80e;664a062ac74a8ad104e0e80f,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||
}
|
||||
err := mappingByPtr(&s, formSource{"ids": {}}, "form")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []objectIDUnmarshalText{id1, id2}, s.FileData)
|
||||
}
|
||||
|
||||
// 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 customUnmarshalParamHex `form:"hex"`
|
||||
HexByUnmarshalText customUnmarshalParamHex `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 customUnmarshalTextHex `form:"hex"`
|
||||
HexByUnmarshalText customUnmarshalTextHex `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 normal 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 ====
|
||||
|
||||
func TestMappingEmptyValues(t *testing.T) {
|
||||
t.Run("slice with default", func(t *testing.T) {
|
||||
var s struct {
|
||||
|
||||
10
context.go
10
context.go
@ -386,11 +386,6 @@ func (c *Context) GetDuration(key any) time.Duration {
|
||||
return getTyped[time.Duration](c, key)
|
||||
}
|
||||
|
||||
// GetError returns the value associated with the key as an error.
|
||||
func (c *Context) GetError(key any) error {
|
||||
return getTyped[error](c, key)
|
||||
}
|
||||
|
||||
// GetIntSlice returns the value associated with the key as a slice of integers.
|
||||
func (c *Context) GetIntSlice(key any) []int {
|
||||
return getTyped[[]int](c, key)
|
||||
@ -456,11 +451,6 @@ func (c *Context) GetStringSlice(key any) []string {
|
||||
return getTyped[[]string](c, key)
|
||||
}
|
||||
|
||||
// GetErrorSlice returns the value associated with the key as a slice of errors.
|
||||
func (c *Context) GetErrorSlice(key any) []error {
|
||||
return getTyped[[]error](c, key)
|
||||
}
|
||||
|
||||
// GetStringMap returns the value associated with the key as a map of interfaces.
|
||||
func (c *Context) GetStringMap(key any) map[string]any {
|
||||
return getTyped[map[string]any](c, key)
|
||||
|
||||
@ -516,14 +516,6 @@ func TestContextGetDuration(t *testing.T) {
|
||||
assert.Equal(t, time.Second, c.GetDuration("duration"))
|
||||
}
|
||||
|
||||
func TestContextGetError(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
key := "error"
|
||||
value := errors.New("test error")
|
||||
c.Set(key, value)
|
||||
assert.Equal(t, value, c.GetError(key))
|
||||
}
|
||||
|
||||
func TestContextGetIntSlice(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
key := "int-slice"
|
||||
@ -626,14 +618,6 @@ func TestContextGetStringSlice(t *testing.T) {
|
||||
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
|
||||
}
|
||||
|
||||
func TestContextGetErrorSlice(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
key := "error-slice"
|
||||
value := []error{errors.New("error1"), errors.New("error2")}
|
||||
c.Set(key, value)
|
||||
assert.Equal(t, value, c.GetErrorSlice(key))
|
||||
}
|
||||
|
||||
func TestContextGetStringMap(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
m := make(map[string]any)
|
||||
|
||||
76
docs/doc.md
76
docs/doc.md
@ -911,7 +911,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 delimit default values
|
||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited 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"
|
||||
|
||||
|
||||
@ -1009,68 +1009,12 @@ 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 (
|
||||
"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 already implements `encoding.TextUnmarshaler` but you want to customize how gin binds the type differently (eg to change what error message is returned), you can implement the dedicated `BindUnmarshaler` interface provided by gin instead.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
)
|
||||
|
||||
type Birthday string
|
||||
@ -1080,37 +1024,29 @@ 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"`
|
||||
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
|
||||
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
|
||||
Birthday Birthday `form:"birthday"`
|
||||
}
|
||||
route.GET("/test", func(ctx *gin.Context) {
|
||||
_ = ctx.BindQuery(&request)
|
||||
ctx.JSON(200, request)
|
||||
ctx.JSON(200, request.Birthday)
|
||||
})
|
||||
_ = route.Run(":8088")
|
||||
route.Run(":8088")
|
||||
}
|
||||
```
|
||||
|
||||
Test it with:
|
||||
|
||||
```sh
|
||||
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||
curl 'localhost:8088/test?birthday=2000-01-01'
|
||||
```
|
||||
Result
|
||||
```sh
|
||||
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||
"2000/01/01"
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
2
go.mod
2
go.mod
@ -7,7 +7,7 @@ require (
|
||||
github.com/gin-contrib/sse v1.1.0
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
github.com/goccy/go-yaml v1.19.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/modern-go/reflect2 v1.0.2
|
||||
|
||||
4
go.sum
4
go.sum
@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user