feat(binding): Support custom BindUnmarshaler for binding. (#3933)
This commit is contained in:
parent
b4f66e965b
commit
a18219566c
@ -165,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
|
|||||||
return setter.TrySet(value, field, tagValue, setOpt)
|
return setter.TrySet(value, field, tagValue, setOpt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
|
||||||
|
type BindUnmarshaler interface {
|
||||||
|
// UnmarshalParam decodes and assigns a value from an form or query param.
|
||||||
|
UnmarshalParam(param string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`
|
||||||
|
// to skip the default value setting.
|
||||||
|
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
|
||||||
|
switch v := value.Addr().Interface().(type) {
|
||||||
|
case BindUnmarshaler:
|
||||||
|
return true, v.UnmarshalParam(val)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
|
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
|
||||||
vs, ok := form[tagValue]
|
vs, ok := form[tagValue]
|
||||||
if !ok && !opt.isDefaultExists {
|
if !ok && !opt.isDefaultExists {
|
||||||
@ -194,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
if len(vs) > 0 {
|
if len(vs) > 0 {
|
||||||
val = vs[0]
|
val = vs[0]
|
||||||
}
|
}
|
||||||
|
if ok, err := trySetCustom(val, value); ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
return true, setWithProperType(val, value, field)
|
return true, setWithProperType(val, value, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,11 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -323,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
|
|||||||
err := mappingByPtr(&s, formSource{}, "form")
|
err := mappingByPtr(&s, formSource{}, "form")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type customUnmarshalParamHex int
|
||||||
|
|
||||||
|
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
|
||||||
|
v, err := strconv.ParseInt(param, 16, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*f = customUnmarshalParamHex(v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo customUnmarshalParamHex `form:"foo"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 245, s.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
Foo customUnmarshalParamHex `uri:"foo"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, 245, s.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
|
type customUnmarshalParamType struct {
|
||||||
|
Protocol string
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
|
||||||
|
parts := strings.Split(param, ":")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return fmt.Errorf("invalid format")
|
||||||
|
}
|
||||||
|
f.Protocol = parts[0]
|
||||||
|
f.Path = parts[1]
|
||||||
|
f.Name = parts[2]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customUnmarshalParamType `form:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customUnmarshalParamType `uri:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData *customUnmarshalParamType `form:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData *customUnmarshalParamType `uri:"data"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "file", s.FileData.Protocol)
|
||||||
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
|
}
|
||||||
|
41
docs/doc.md
41
docs/doc.md
@ -27,6 +27,7 @@
|
|||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
|
||||||
- [Bind Uri](#bind-uri)
|
- [Bind Uri](#bind-uri)
|
||||||
|
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
|
||||||
- [Bind Header](#bind-header)
|
- [Bind Header](#bind-header)
|
||||||
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
- [Bind HTML checkboxes](#bind-html-checkboxes)
|
||||||
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
|
||||||
@ -899,6 +900,46 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
|
|||||||
curl -v localhost:8088/thinkerou/not-uuid
|
curl -v localhost:8088/thinkerou/not-uuid
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bind custom unmarshaler
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Birthday string
|
||||||
|
|
||||||
|
func (b *Birthday) UnmarshalParam(param string) error {
|
||||||
|
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
var request struct {
|
||||||
|
Birthday Birthday `form:"birthday"`
|
||||||
|
}
|
||||||
|
route.GET("/test", func(ctx *gin.Context) {
|
||||||
|
_ = ctx.BindQuery(&request)
|
||||||
|
ctx.JSON(200, request.Birthday)
|
||||||
|
})
|
||||||
|
route.Run(":8088")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Test it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl 'localhost:8088/test?birthday=2000-01-01'
|
||||||
|
```
|
||||||
|
Result
|
||||||
|
```sh
|
||||||
|
"2000/01/01"
|
||||||
|
```
|
||||||
|
|
||||||
### Bind Header
|
### Bind Header
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
24
gin_test.go
24
gin_test.go
@ -14,6 +14,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -730,3 +731,26 @@ func TestWithOptionFunc(t *testing.T) {
|
|||||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
|
||||||
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Birthday string
|
||||||
|
|
||||||
|
func (b *Birthday) UnmarshalParam(param string) error {
|
||||||
|
*b = Birthday(strings.Replace(param, "-", "/", -1))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomUnmarshalStruct(t *testing.T) {
|
||||||
|
route := Default()
|
||||||
|
var request struct {
|
||||||
|
Birthday Birthday `form:"birthday"`
|
||||||
|
}
|
||||||
|
route.GET("/test", func(ctx *Context) {
|
||||||
|
_ = ctx.BindQuery(&request)
|
||||||
|
ctx.JSON(200, request.Birthday)
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
route.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Equal(t, `"2000/01/01"`, w.Body.String())
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user