Skip to content

Commit

Permalink
feat(binding): Support custom BindUnmarshaler for binding. (#3933)
Browse files Browse the repository at this point in the history
  • Loading branch information
dkkb authored May 7, 2024
1 parent b4f66e9 commit a182195
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 0 deletions.
20 changes: 20 additions & 0 deletions binding/form_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
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) {
vs, ok := form[tagValue]
if !ok && !opt.isDefaultExists {
Expand Down Expand Up @@ -194,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if len(vs) > 0 {
val = vs[0]
}
if ok, err := trySetCustom(val, value); ok {
return ok, err
}
return true, setWithProperType(val, value, field)
}
}
Expand Down
99 changes: 99 additions & 0 deletions binding/form_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package binding

import (
"fmt"
"mime/multipart"
"reflect"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -323,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
err := mappingByPtr(&s, formSource{}, "form")
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 changes: 41 additions & 0 deletions docs/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
- [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
Expand Down Expand Up @@ -899,6 +900,46 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
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

```go
Expand Down
24 changes: 24 additions & 0 deletions gin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net/http/httptest"
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -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: "/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())
}

0 comments on commit a182195

Please sign in to comment.