Skip to content

Commit

Permalink
TextUnmarshaller support for binding
Browse files Browse the repository at this point in the history
  • Loading branch information
ssoroka committed Apr 9, 2022
1 parent c458094 commit 1dca789
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 0 deletions.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,67 @@ func ListHandler(s *Service) func(ctx *gin.Context) {
}
```

### Bind form-data request with custom field type

Gin can support the encoding.TextUnmarshaler interface for non-struct types

```go
type HexInteger int

func (f *HexInteger) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = HexInteger(v)
return nil
}

type FormA struct {
FieldA HexInteger `form:"field_a"`
}

// query with field_a = "0f"
func GetDataA(c *gin.Context) {
var a FormA
c.Bind(&a)
// a.FieldA == 15
}
```

For struct types, you can implement your own custom Unmarshaler using the `binding.BindUnmarshaler`
interface, which has the interface signature of `UnmarshalParam(param string) error`.

```go
type customType struct {
Protocol string
Path string
Name string
}

func (f *customType) 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
}

type FormA struct {
FieldA customType `form:"field_a"`
}

// query with field_a = "file:/:foo"
func GetDataA(c *gin.Context) {
var a FormA
c.Bind(&a)
// a.FieldA.Protocol == "file", a.FieldA.Path == "/", and a.FieldA.Name == "foo"
}
```

### http2 server push

http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
Expand Down
4 changes: 4 additions & 0 deletions binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ type BindingUri interface {
BindUri(map[string][]string, any) error
}

type BindUnmarshaler interface {
UnmarshalParam(param string) error
}

// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
Expand Down
23 changes: 23 additions & 0 deletions binding/form_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package binding

import (
"encoding"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -175,11 +176,17 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
if !ok {
vs = []string{opt.defaultValue}
}
if ok, err := trySetCustom(vs[0], value); ok || err != nil {
return ok, err
}
return true, setSlice(vs, value, field)
case reflect.Array:
if !ok {
vs = []string{opt.defaultValue}
}
if ok, err := trySetCustom(vs[0], value); ok || err != nil {
return ok, err
}
if len(vs) != value.Len() {
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
}
Expand All @@ -193,10 +200,26 @@ 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 || err != nil {
return ok, err
}
return true, setWithProperType(val, value, field)
}
}

func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {
switch v := value.Addr().Interface().(type) {
case encoding.TextUnmarshaler:
if value.Kind() != reflect.Struct {
return true, v.UnmarshalText([]byte(val))
}
case BindUnmarshaler:
return true, v.UnmarshalParam(val)
}

return false, nil
}

func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {
switch value.Kind() {
case reflect.Int:
Expand Down
110 changes: 110 additions & 0 deletions binding/form_mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
package binding

import (
"fmt"
"reflect"
"strconv"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -288,3 +291,110 @@ func TestMappingIgnoredCircularRef(t *testing.T) {
err := mappingByPtr(&s, formSource{}, "form")
assert.NoError(t, err)
}

type foohex int

func (f *foohex) UnmarshalText(text []byte) error {
v, err := strconv.ParseInt(string(text), 16, 64)
if err != nil {
return err
}
*f = foohex(v)
return nil
}

func TestMappingCustomFieldType(t *testing.T) {
var s struct {
Foo foohex `form:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "form")
assert.NoError(t, err)

assert.EqualValues(t, 245, s.Foo)
}

func TestMappingCustomFieldTypeWithURI(t *testing.T) {
var s struct {
Foo foohex `uri:"foo"`
}
err := mappingByPtr(&s, formSource{"foo": {`f5`}}, "uri")
assert.NoError(t, err)

assert.EqualValues(t, 245, s.Foo)
}

type customType struct {
Protocol string
Path string
Name string
}

func (f *customType) 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 TestMappingCustomStructType(t *testing.T) {
var s struct {
FileData customType `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 TestMappingCustomPointerStructType(t *testing.T) {
var s struct {
FileData *customType `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)
}

type MySlice []string

func (s *MySlice) UnmarshalParam(param string) error {
*s = MySlice(strings.Split(param, ","))
return nil
}

func TestMappingCustomSliceType(t *testing.T) {
var s struct {
Permissions MySlice `form:"permissions"`
}
err := mappingByPtr(&s, formSource{"permissions": {"read,write,delete"}}, "form")
assert.NoError(t, err)

assert.EqualValues(t, []string{"read", "write", "delete"}, s.Permissions)
}

type MyArray [3]string

func (s *MyArray) UnmarshalParam(param string) error {
parts := strings.Split(param, ",")
*s = MyArray([3]string{parts[0], parts[1], parts[2]})
return nil
}

func TestMappingCustomArrayType(t *testing.T) {
var s struct {
Permissions MyArray `form:"permissions"`
}
err := mappingByPtr(&s, formSource{"permissions": {"read,write,delete"}}, "form")
assert.NoError(t, err)

assert.EqualValues(t, [3]string{"read", "write", "delete"}, s.Permissions)
}

0 comments on commit 1dca789

Please sign in to comment.