diff --git a/binder.go b/binder.go index 056aacb5..dd24eff5 100644 --- a/binder.go +++ b/binder.go @@ -19,6 +19,12 @@ type ( // DefaultBinder is the default implementation of the Binder interface. DefaultBinder struct{} + + // BindUnmarshaler is the interface used to wrap the UnmarshalParam method. + BindUnmarshaler interface { + // UnmarshalParam decodes and assigns a value from an HTML form. + UnmarshalParam(src string) error + } ) // Bind implements the `Binder#Bind` function. @@ -151,12 +157,35 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V return setFloatField(val, 64, structField) case reflect.String: structField.SetString(val) + case reflect.Ptr: + return unmarshalFieldPtr(val, structField) default: - return errors.New("unknown type") + return unmarshalField(val, structField) } return nil } +func unmarshalField(value string, field reflect.Value) error { + ptr := reflect.New(field.Type()) + if ptr.CanInterface() { + iface := ptr.Interface() + if unmarshaler, ok := iface.(BindUnmarshaler); ok { + err := unmarshaler.UnmarshalParam(value) + field.Set(ptr.Elem()) + return err + } + } + return errors.New("unknown type") +} + +func unmarshalFieldPtr(value string, field reflect.Value) error { + if field.IsNil() { + // Initialize the pointer to a nil value + field.Set(reflect.New(field.Type().Elem())) + } + return unmarshalField(value, field.Elem()) +} + func setIntField(value string, bitSize int, field reflect.Value) error { if value == "" { value = "0" diff --git a/binder_test.go b/binder_test.go index e1bffbbe..6f495fc6 100644 --- a/binder_test.go +++ b/binder_test.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" "testing" + "time" "github.com/stretchr/testify/assert" ) @@ -31,9 +32,22 @@ type ( S string cantSet string DoesntExist string + T Timestamp + Tptr *Timestamp } + + // Timestamp is a simple datatype which implements the Scanner interface + // used for testing. + Timestamp time.Time ) +// UnmarshalParam unmarshals a value from an HTML form into a Timestamp value +func (t *Timestamp) UnmarshalParam(src string) error { + ts, err := time.Parse(time.RFC3339, src) + *t = Timestamp(ts) + return err +} + func (t binderTestStruct) GetCantSet() string { return t.cantSet } @@ -54,6 +68,8 @@ var values = map[string][]string{ "F64": {"64.5"}, "S": {"test"}, "cantSet": {"test"}, + "T": {"2016-12-06T19:09:05+01:00"}, + "Tptr": {"2016-12-06T19:09:05+01:00"}, } func TestBinderJSON(t *testing.T) { @@ -92,6 +108,35 @@ func TestBinderQueryParams(t *testing.T) { } } +func TestBinderScanner(t *testing.T) { + e := New() + req, _ := http.NewRequest(GET, "/?ts=2016-12-06T19:09:05Z", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + var result struct { + T Timestamp `query:"ts"` + } + err := c.Bind(&result) + if assert.NoError(t, err) { + // assert.Equal(t, Timestamp(reflect.TypeOf(&Timestamp{}), time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T) + assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), result.T) + } +} + +func TestBinderScannerPtr(t *testing.T) { + e := New() + req, _ := http.NewRequest(GET, "/?ts=2016-12-06T19:09:05Z", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + var result struct { + Tptr *Timestamp `query:"ts"` + } + err := c.Bind(&result) + if assert.NoError(t, err) { + assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), *result.Tptr) + } +} + func TestBinderMultipartForm(t *testing.T) { body := new(bytes.Buffer) mw := multipart.NewWriter(body) @@ -174,6 +219,10 @@ func TestBinderSetFields(t *testing.T) { if assert.NoError(t, setBoolField("", val.FieldByName("B"))) { assert.Equal(t, false, ts.B) } + + if assert.NoError(t, unmarshalField("2016-12-06T19:09:05Z", val.FieldByName("T"))) { + assert.Equal(t, Timestamp(time.Date(2016, 12, 6, 19, 9, 5, 0, time.UTC)), ts.T) + } } func assertBinderTestStruct(t *testing.T, ts *binderTestStruct) { diff --git a/website/content/guide.md b/website/content/guide.md index 3900ebd0..4a6c9a27 100644 --- a/website/content/guide.md +++ b/website/content/guide.md @@ -172,13 +172,14 @@ ls avatar.png ### Handling Request -- Bind `JSON` or `XML` or `form` payload into Go struct based on `Content-Type` request header. +- Bind `JSON`, `XML`, `form` or `query` payload into Go struct based on `Content-Type` request header. - Render response as `JSON` or `XML` with status code. +- Use the [BindUnmarshaler](https://godoc.org/github.com/labstack/echo#ParamUnmarshaler) interface to bind custom data types for `form` or `query` payloads. The standard [json.Unmarshaler](https://golang.org/pkg/encoding/json/#Unmarshaler) and [xml.Unmarshaler](https://golang.org/pkg/encoding/xml/#Unmarshaler) can of course be used for JSON and XML payloads, respectively. ```go type User struct { - Name string `json:"name" xml:"name" form:"name"` - Email string `json:"email" xml:"email" form:"email"` + Name string `json:"name" xml:"name" form:"name" query:"name"` + Email string `json:"email" xml:"email" form:"email" query:"name"` } e.POST("/users", func(c echo.Context) error {