mirror of
https://github.com/gin-gonic/gin.git
synced 2026-01-10 08:27:00 +08:00
Merge 6ef8cfbfb87799243d69f3592c7bfc27bf0a376b into 9914178584e42458ff7d23891463a880f58c9d86
This commit is contained in:
commit
49ca45a5e5
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
@ -137,6 +138,8 @@ func mapping(value reflect.Value, field reflect.StructField, setter setter, tag
|
|||||||
type setOptions struct {
|
type setOptions struct {
|
||||||
isDefaultExists bool
|
isDefaultExists bool
|
||||||
defaultValue string
|
defaultValue string
|
||||||
|
// parser 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) {
|
func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {
|
||||||
@ -168,6 +171,8 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
setOpt.defaultValue = strings.ReplaceAll(v, ";", ",")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if k, v = head(opt, "="); k == "parser" {
|
||||||
|
setOpt.parser = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +196,20 @@ func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trySetUsingParser tries to set a custom type value based on the presence of the "parser" tag on the field.
|
||||||
|
// If the parser tag does not exist or does not match any of the supported parsers, gin will skip over this.
|
||||||
|
func trySetUsingParser(val string, value reflect.Value, parser string) (isSet bool, err error) {
|
||||||
|
switch parser {
|
||||||
|
case "encoding.TextUnmarshaler":
|
||||||
|
v, ok := value.Addr().Interface().(encoding.TextUnmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, v.UnmarshalText([]byte(val))
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
func trySplit(vs []string, field reflect.StructField) (newVs []string, err error) {
|
||||||
cfTag := field.Tag.Get("collection_format")
|
cfTag := field.Tag.Get("collection_format")
|
||||||
if cfTag == "" || cfTag == "multi" {
|
if cfTag == "" || cfTag == "multi" {
|
||||||
@ -208,7 +227,7 @@ func trySplit(vs []string, field reflect.StructField) (newVs []string, err error
|
|||||||
case "pipes":
|
case "pipes":
|
||||||
sep = "|"
|
sep = "|"
|
||||||
default:
|
default:
|
||||||
return vs, fmt.Errorf("%s is not supported in the collection_format. (csv, ssv, pipes)", cfTag)
|
return vs, fmt.Errorf("%s is not supported in the collection_format. (multi, csv, ssv, tsv, pipes)", cfTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalLength := 0
|
totalLength := 0
|
||||||
@ -244,7 +263,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err = trySetCustom(vs[0], value); ok {
|
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||||
|
return ok, err
|
||||||
|
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +273,7 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setSlice(vs, value, field)
|
return true, setSlice(vs, value, field, opt)
|
||||||
case reflect.Array:
|
case reflect.Array:
|
||||||
if len(vs) == 0 {
|
if len(vs) == 0 {
|
||||||
if !opt.isDefaultExists {
|
if !opt.isDefaultExists {
|
||||||
@ -267,7 +288,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err = trySetCustom(vs[0], value); ok {
|
if ok, err = trySetUsingParser(vs[0], value, opt.parser); ok {
|
||||||
|
return ok, err
|
||||||
|
} else if ok, err = trySetCustom(vs[0], value); ok {
|
||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,27 +302,32 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setArray(vs, value, field)
|
return true, setArray(vs, value, field, opt)
|
||||||
default:
|
default:
|
||||||
var val string
|
var val string
|
||||||
if !ok {
|
if !ok || len(vs) == 0 || (len(vs) > 0 && vs[0] == "") {
|
||||||
val = opt.defaultValue
|
val = opt.defaultValue
|
||||||
|
} else if len(vs) > 0 {
|
||||||
|
val = vs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(vs) > 0 {
|
if ok, err = trySetUsingParser(val, value, opt.parser); ok {
|
||||||
val = vs[0]
|
return ok, err
|
||||||
if val == "" {
|
} else if ok, err = trySetCustom(val, value); ok {
|
||||||
val = opt.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ok, err := trySetCustom(val, value); ok {
|
|
||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
return true, setWithProperType(val, value, field)
|
return true, setWithProperType(val, value, field, opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
|
func setWithProperType(val string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
|
// this if-check is required for parsing nested types like []MyId, where MyId is [12]byte
|
||||||
|
if ok, err := trySetUsingParser(val, value, opt.parser); ok {
|
||||||
|
return err
|
||||||
|
} else if ok, err = trySetCustom(val, value); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// If it is a string type, no spaces are removed, and the user data is not modified here
|
// If it is a string type, no spaces are removed, and the user data is not modified here
|
||||||
if value.Kind() != reflect.String {
|
if value.Kind() != reflect.String {
|
||||||
val = strings.TrimSpace(val)
|
val = strings.TrimSpace(val)
|
||||||
@ -352,7 +380,7 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel
|
|||||||
if !value.Elem().IsValid() {
|
if !value.Elem().IsValid() {
|
||||||
value.Set(reflect.New(value.Type().Elem()))
|
value.Set(reflect.New(value.Type().Elem()))
|
||||||
}
|
}
|
||||||
return setWithProperType(val, value.Elem(), field)
|
return setWithProperType(val, value.Elem(), field, opt)
|
||||||
default:
|
default:
|
||||||
return errUnknownType
|
return errUnknownType
|
||||||
}
|
}
|
||||||
@ -459,9 +487,9 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setArray(vals []string, value reflect.Value, field reflect.StructField) error {
|
func setArray(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
for i, s := range vals {
|
for i, s := range vals {
|
||||||
err := setWithProperType(s, value.Index(i), field)
|
err := setWithProperType(s, value.Index(i), field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -469,9 +497,9 @@ func setArray(vals []string, value reflect.Value, field reflect.StructField) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSlice(vals []string, value reflect.Value, field reflect.StructField) error {
|
func setSlice(vals []string, value reflect.Value, field reflect.StructField, opt setOptions) error {
|
||||||
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
slice := reflect.MakeSlice(value.Type(), len(vals), len(vals))
|
||||||
err := setArray(vals, slice, field)
|
err := setArray(vals, slice, field, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
@ -524,6 +525,16 @@ func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
|||||||
assert.EqualValues(t, 245, s.Foo)
|
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 {
|
type customUnmarshalParamType struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
Path string
|
Path string
|
||||||
@ -624,6 +635,33 @@ func TestMappingCustomSliceForm(t *testing.T) {
|
|||||||
assert.Equal(t, "foo", s.FileData[1])
|
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
|
type objectID [12]byte
|
||||||
|
|
||||||
func (o *objectID) UnmarshalParam(param string) error {
|
func (o *objectID) UnmarshalParam(param string) error {
|
||||||
@ -675,6 +713,358 @@ func TestMappingCustomArrayForm(t *testing.T) {
|
|||||||
assert.Equal(t, expected, s.FileData)
|
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) {
|
func TestMappingEmptyValues(t *testing.T) {
|
||||||
t.Run("slice with default", func(t *testing.T) {
|
t.Run("slice with default", func(t *testing.T) {
|
||||||
var s struct {
|
var s struct {
|
||||||
|
|||||||
78
docs/doc.md
78
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:
|
NOTE: For default [collection values](#collection-format-for-arrays), the following rules apply:
|
||||||
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
- Since commas are used to delimit tag options, they are not supported within a default value and will result in undefined behavior
|
||||||
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimited default values
|
- For the collection formats "multi" and "csv", a semicolon should be used in place of a comma to delimit default values
|
||||||
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
- Since semicolons are used to delimit default values for "multi" and "csv", they are not supported within a default value for "multi" and "csv"
|
||||||
|
|
||||||
|
|
||||||
@ -1009,12 +1009,68 @@ curl -v localhost:8088/thinkerou/not-uuid
|
|||||||
|
|
||||||
### Bind custom unmarshaler
|
### Bind custom unmarshaler
|
||||||
|
|
||||||
|
To override gin's default binding logic, define a function on your type that satisfies the `encoding.TextUnmarshaler` interface from the Golang standard library. Then specify `parser=encoding.TextUnmarshaler` in the `uri`/`form` tag of the field being bound.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"encoding"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Birthday string
|
||||||
|
|
||||||
|
func (b *Birthday) UnmarshalText(text []byte) error {
|
||||||
|
*b = Birthday(strings.Replace(string(text), "-", "/", -1))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ encoding.TextUnmarshaler = (*Birthday)(nil) //assert Birthday implements encoding.TextUnmarshaler
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
var request struct {
|
||||||
|
Birthday Birthday `form:"birthday,parser=encoding.TextUnmarshaler"`
|
||||||
|
Birthdays []Birthday `form:"birthdays,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||||
|
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02,parser=encoding.TextUnmarshaler" collection_format:"csv"`
|
||||||
|
}
|
||||||
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
|
_ = ctx.BindQuery(&request)
|
||||||
|
ctx.JSON(200, request)
|
||||||
|
})
|
||||||
|
_ = route.Run(":8088")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||||
|
```
|
||||||
|
Result
|
||||||
|
```sh
|
||||||
|
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- If `parser=encoding.TextUnmarshaler` is specified for a type that does **not** implement `encoding.TextUnmarshaler`, gin will ignore it and proceed with its default binding logic.
|
||||||
|
- If `parser=encoding.TextUnmarshaler` is specified for a type and that type's implementation of `encoding.TextUnmarshaler` returns an error, gin will stop binding and return the error to the client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If a type 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
|
type Birthday string
|
||||||
@ -1024,29 +1080,37 @@ func (b *Birthday) UnmarshalParam(param string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ binding.BindUnmarshaler = (*Birthday)(nil) //assert Birthday implements binding.BindUnmarshaler
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
route := gin.Default()
|
route := gin.Default()
|
||||||
var request struct {
|
var request struct {
|
||||||
Birthday Birthday `form:"birthday"`
|
Birthday Birthday `form:"birthday"`
|
||||||
|
Birthdays []Birthday `form:"birthdays" collection_format:"csv"`
|
||||||
|
BirthdaysDefault []Birthday `form:"birthdaysDef,default=2020-09-01;2020-09-02" collection_format:"csv"`
|
||||||
}
|
}
|
||||||
route.GET("/test", func(ctx *gin.Context) {
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
_ = ctx.BindQuery(&request)
|
_ = ctx.BindQuery(&request)
|
||||||
ctx.JSON(200, request.Birthday)
|
ctx.JSON(200, request)
|
||||||
})
|
})
|
||||||
route.Run(":8088")
|
_ = route.Run(":8088")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Test it with:
|
Test it with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl 'localhost:8088/test?birthday=2000-01-01'
|
curl 'localhost:8088/test?birthday=2000-01-01&birthdays=2000-01-01,2000-01-02'
|
||||||
```
|
```
|
||||||
Result
|
Result
|
||||||
```sh
|
```sh
|
||||||
"2000/01/01"
|
{"Birthday":"2000/01/01","Birthdays":["2000/01/01","2000/01/02"],"BirthdaysDefault":["2020/09/01","2020/09/02"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
- If a type implements both `encoding.TextUnmarshaler` and `BindUnmarshaler`, gin will use `BindUnmarshaler` by default unless you specify `parser=encoding.TextUnmarshaler` in the binding tag.
|
||||||
|
- If a type returns an error from its implementation of `BindUnmarshaler`, gin will stop binding and return the error to the client.
|
||||||
|
|
||||||
### Bind Header
|
### Bind Header
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user