Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TextUnmarshaler support for binding #3045

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}