Skip to content

Commit a182195

Browse files
authored
feat(binding): Support custom BindUnmarshaler for binding. (#3933)
1 parent b4f66e9 commit a182195

File tree

4 files changed

+184
-0
lines changed

4 files changed

+184
-0
lines changed

binding/form_mapping.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
165165
return setter.TrySet(value, field, tagValue, setOpt)
166166
}
167167

168+
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
169+
type BindUnmarshaler interface {
170+
// UnmarshalParam decodes and assigns a value from an form or query param.
171+
UnmarshalParam(param string) error
172+
}
173+
174+
// trySetCustom tries to set a custom type value
175+
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
176+
// to skip the default value setting.
177+
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
178+
switch v := value.Addr().Interface().(type) {
179+
case BindUnmarshaler:
180+
return true, v.UnmarshalParam(val)
181+
}
182+
return false, nil
183+
}
184+
168185
func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {
169186
vs, ok := form[tagValue]
170187
if !ok && !opt.isDefaultExists {
@@ -194,6 +211,9 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
194211
if len(vs) > 0 {
195212
val = vs[0]
196213
}
214+
if ok, err := trySetCustom(val, value); ok {
215+
return ok, err
216+
}
197217
return true, setWithProperType(val, value, field)
198218
}
199219
}

binding/form_mapping_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
package binding
66

77
import (
8+
"fmt"
89
"mime/multipart"
910
"reflect"
11+
"strconv"
12+
"strings"
1013
"testing"
1114
"time"
1215

@@ -323,3 +326,99 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
323326
err := mappingByPtr(&s, formSource{}, "form")
324327
assert.NoError(t, err)
325328
}
329+
330+
type customUnmarshalParamHex int
331+
332+
func (f *customUnmarshalParamHex) UnmarshalParam(param string) error {
333+
v, err := strconv.ParseInt(param, 16, 64)
334+
if err != nil {
335+
return err
336+
}
337+
*f = customUnmarshalParamHex(v)
338+
return nil
339+
}
340+
341+
func TestMappingCustomUnmarshalParamHexWithFormTag(t *testing.T) {
342+
var s struct {
343+
Foo customUnmarshalParamHex `form:"foo"`
344+
}
345+
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
346+
assert.NoError(t, err)
347+
348+
assert.EqualValues(t, 245, s.Foo)
349+
}
350+
351+
func TestMappingCustomUnmarshalParamHexWithURITag(t *testing.T) {
352+
var s struct {
353+
Foo customUnmarshalParamHex `uri:"foo"`
354+
}
355+
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
356+
assert.NoError(t, err)
357+
358+
assert.EqualValues(t, 245, s.Foo)
359+
}
360+
361+
type customUnmarshalParamType struct {
362+
Protocol string
363+
Path string
364+
Name string
365+
}
366+
367+
func (f *customUnmarshalParamType) UnmarshalParam(param string) error {
368+
parts := strings.Split(param, ":")
369+
if len(parts) != 3 {
370+
return fmt.Errorf("invalid format")
371+
}
372+
f.Protocol = parts[0]
373+
f.Path = parts[1]
374+
f.Name = parts[2]
375+
return nil
376+
}
377+
378+
func TestMappingCustomStructTypeWithFormTag(t *testing.T) {
379+
var s struct {
380+
FileData customUnmarshalParamType `form:"data"`
381+
}
382+
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
383+
assert.NoError(t, err)
384+
385+
assert.EqualValues(t, "file", s.FileData.Protocol)
386+
assert.EqualValues(t, "/foo", s.FileData.Path)
387+
assert.EqualValues(t, "happiness", s.FileData.Name)
388+
}
389+
390+
func TestMappingCustomStructTypeWithURITag(t *testing.T) {
391+
var s struct {
392+
FileData customUnmarshalParamType `uri:"data"`
393+
}
394+
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
395+
assert.NoError(t, err)
396+
397+
assert.EqualValues(t, "file", s.FileData.Protocol)
398+
assert.EqualValues(t, "/foo", s.FileData.Path)
399+
assert.EqualValues(t, "happiness", s.FileData.Name)
400+
}
401+
402+
func TestMappingCustomPointerStructTypeWithFormTag(t *testing.T) {
403+
var s struct {
404+
FileData *customUnmarshalParamType `form:"data"`
405+
}
406+
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "form")
407+
assert.NoError(t, err)
408+
409+
assert.EqualValues(t, "file", s.FileData.Protocol)
410+
assert.EqualValues(t, "/foo", s.FileData.Path)
411+
assert.EqualValues(t, "happiness", s.FileData.Name)
412+
}
413+
414+
func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
415+
var s struct {
416+
FileData *customUnmarshalParamType `uri:"data"`
417+
}
418+
err := mappingByPtr(&s, formSource{"data": {`file:/foo:happiness`}}, "uri")
419+
assert.NoError(t, err)
420+
421+
assert.EqualValues(t, "file", s.FileData.Protocol)
422+
assert.EqualValues(t, "/foo", s.FileData.Path)
423+
assert.EqualValues(t, "happiness", s.FileData.Name)
424+
}

docs/doc.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
- [Only Bind Query String](#only-bind-query-string)
2828
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
2929
- [Bind Uri](#bind-uri)
30+
- [Bind custom unmarshaler](#bind-custom-unmarshaler)
3031
- [Bind Header](#bind-header)
3132
- [Bind HTML checkboxes](#bind-html-checkboxes)
3233
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
@@ -899,6 +900,46 @@ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
899900
curl -v localhost:8088/thinkerou/not-uuid
900901
```
901902

903+
### Bind custom unmarshaler
904+
905+
```go
906+
package main
907+
908+
import (
909+
"github.com/gin-gonic/gin"
910+
"strings"
911+
)
912+
913+
type Birthday string
914+
915+
func (b *Birthday) UnmarshalParam(param string) error {
916+
*b = Birthday(strings.Replace(param, "-", "/", -1))
917+
return nil
918+
}
919+
920+
func main() {
921+
route := gin.Default()
922+
var request struct {
923+
Birthday Birthday `form:"birthday"`
924+
}
925+
route.GET("/test", func(ctx *gin.Context) {
926+
_ = ctx.BindQuery(&request)
927+
ctx.JSON(200, request.Birthday)
928+
})
929+
route.Run(":8088")
930+
}
931+
```
932+
933+
Test it with:
934+
935+
```sh
936+
curl 'localhost:8088/test?birthday=2000-01-01'
937+
```
938+
Result
939+
```sh
940+
"2000/01/01"
941+
```
942+
902943
### Bind Header
903944

904945
```go

gin_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"net/http/httptest"
1515
"reflect"
1616
"strconv"
17+
"strings"
1718
"sync/atomic"
1819
"testing"
1920
"time"
@@ -730,3 +731,26 @@ func TestWithOptionFunc(t *testing.T) {
730731
assertRoutePresent(t, routes, RouteInfo{Path: "/test1", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest1"})
731732
assertRoutePresent(t, routes, RouteInfo{Path: "/test2", Method: "GET", Handler: "github.com/gin-gonic/gin.handlerTest2"})
732733
}
734+
735+
type Birthday string
736+
737+
func (b *Birthday) UnmarshalParam(param string) error {
738+
*b = Birthday(strings.Replace(param, "-", "/", -1))
739+
return nil
740+
}
741+
742+
func TestCustomUnmarshalStruct(t *testing.T) {
743+
route := Default()
744+
var request struct {
745+
Birthday Birthday `form:"birthday"`
746+
}
747+
route.GET("/test", func(ctx *Context) {
748+
_ = ctx.BindQuery(&request)
749+
ctx.JSON(200, request.Birthday)
750+
})
751+
req := httptest.NewRequest("GET", "/test?birthday=2000-01-01", nil)
752+
w := httptest.NewRecorder()
753+
route.ServeHTTP(w, req)
754+
assert.Equal(t, 200, w.Code)
755+
assert.Equal(t, `"2000/01/01"`, w.Body.String())
756+
}

0 commit comments

Comments
 (0)