diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 55435b94..f449a4b2 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -197,6 +197,18 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][ } } +type jsonUnmarshaler interface { + UnmarshalJSON([]byte) error +} + +type textUnmarshaler interface { + UnmarshalText([]byte) error +} + +type binaryUnmarshaler interface { + UnmarshalBinary([]byte) error +} + func setWithProperType(val string, value reflect.Value, field reflect.StructField) error { switch value.Kind() { case reflect.Int: @@ -236,6 +248,15 @@ func setWithProperType(val string, value reflect.Value, field reflect.StructFiel case time.Time: return setTimeField(val, field, value) } + if unmarshaler, ok := value.Addr().Interface().(jsonUnmarshaler); ok { + return unmarshaler.UnmarshalJSON(bytesconv.StringToBytes(val)) + } + if unmarshaler, ok := value.Addr().Interface().(textUnmarshaler); ok { + return unmarshaler.UnmarshalText(bytesconv.StringToBytes(val)) + } + if unmarshaler, ok := value.Addr().Interface().(binaryUnmarshaler); ok { + return unmarshaler.UnmarshalBinary(bytesconv.StringToBytes(val)) + } return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) case reflect.Map: return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface()) diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index acea8f77..5eaaef1f 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -5,7 +5,9 @@ package binding import ( + "errors" "reflect" + "strings" "testing" "time" @@ -321,3 +323,93 @@ func TestMappingIgnoredCircularRef(t *testing.T) { err := mappingByPtr(&s, formSource{}, "form") assert.NoError(t, err) } + +// this structure has special json unmarshaller, in order to parse email (as an example) as specific structure +type withJsonUnmarshaller struct { + Name string + Host string +} + +func (o *withJsonUnmarshaller) UnmarshalJSON(data []byte) error { + elems := strings.Split(string(data), "@") + if len(elems) != 2 { + return errors.New("cannot parse %q as email") + } + o.Name = elems[0] + o.Host = elems[1] + return nil +} + +func TestMappingStructFieldJSONUnmarshaller(t *testing.T) { + var s struct { + Email withJsonUnmarshaller + } + + err := mappingByPtr(&s, formSource{"Email": {`test@example.org`}}, "form") + assert.NoError(t, err) + assert.Equal(t, "test", s.Email.Name) + assert.Equal(t, "example.org", s.Email.Host) + + err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form") + assert.Error(t, err) +} + +// this structure has special text unmarshaller, in order to parse email (as an example) as specific structure +type withTextUnmarshaller struct { + Name string + Host string +} + +func (o *withTextUnmarshaller) UnmarshalText(data []byte) error { + elems := strings.Split(string(data), "@") + if len(elems) != 2 { + return errors.New("cannot parse %q as email") + } + o.Name = elems[0] + o.Host = elems[1] + return nil +} + +func TestMappingStructFieldTextUnmarshaller(t *testing.T) { + var s struct { + Email withTextUnmarshaller + } + + err := mappingByPtr(&s, formSource{"Email": {`test@example.org`}}, "form") + assert.NoError(t, err) + assert.Equal(t, "test", s.Email.Name) + assert.Equal(t, "example.org", s.Email.Host) + + err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form") + assert.Error(t, err) +} + +// this structure has special binary unmarshaller, in order to parse email (as an example) as specific structure +type withBinaryUnmarshaller struct { + Name string + Host string +} + +func (o *withBinaryUnmarshaller) UnmarshalBinary(data []byte) error { + elems := strings.Split(string(data), "@") + if len(elems) != 2 { + return errors.New("cannot parse %q as email") + } + o.Name = elems[0] + o.Host = elems[1] + return nil +} + +func TestMappingStructFieldBinaryUnmarshaller(t *testing.T) { + var s struct { + Email withBinaryUnmarshaller + } + + err := mappingByPtr(&s, formSource{"Email": {`test@example.org`}}, "form") + assert.NoError(t, err) + assert.Equal(t, "test", s.Email.Name) + assert.Equal(t, "example.org", s.Email.Host) + + err = mappingByPtr(&s, formSource{"Email": {`not an email`}}, "form") + assert.Error(t, err) +} diff --git a/docs/doc.md b/docs/doc.md index e48c2ba1..b67205b1 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -57,6 +57,7 @@ - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) - [Try to bind body into different structs](#try-to-bind-body-into-different-structs) - [Bind form-data request with custom struct and custom tag](#bind-form-data-request-with-custom-struct-and-custom-tag) + - [Bind Query with custom unmarshalers](#bind-query-with-custom-unmarshalers) - [http2 server push](#http2-server-push) - [Define format for the log of routes](#define-format-for-the-log-of-routes) - [Set and get a cookie](#set-and-get-a-cookie) @@ -1155,7 +1156,7 @@ func main() { router.StaticFS("/more_static", http.Dir("my_file_system")) router.StaticFile("/favicon.ico", "./resources/favicon.ico") router.StaticFileFS("/more_favicon.ico", "more_favicon.ico", http.Dir("my_file_system")) - + // Listen and serve on 0.0.0.0:8080 router.Run(":8080") } @@ -2002,6 +2003,59 @@ func ListHandler(s *Service) func(ctx *gin.Context) { } ``` +### Bind Query with custom unmarshalers + +Any structure that has custom `UnmarshalJSON` or `UnmarshalText` or `UnmarshalBinary` can be used to parse input as necessary + +```go +package main +import ( + "fmt" + "net/http" + "strings" + "github.com/gin-gonic/gin" +) +// Booking contains data binded using custom unmarshaler. +type Payload struct { + Email EmailDetails `form:"email"` +} +// this structure has special json unmarshaller, in order to parse email (as an example) as specific structure +type EmailDetails struct { + Name string + Host string +} +func (o *EmailDetails) UnmarshalJSON(data []byte) error { + elems := strings.Split(string(data), "@") + if len(elems) != 2 { + return fmt.Errorf("cannot parse %q as email", string(data)) + } + o.Name = elems[0] + o.Host = elems[1] + return nil +} +func main() { + route := gin.Default() + route.GET("/email", getEmail) + route.Run(":8085") +} +func getEmail(c *gin.Context) { + var p Payload + if err := c.ShouldBindQuery(&p); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Email information is correct"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} +``` + +```console +$ curl "localhost:8085/email?email=test@example.org" +{"message":"Email information is correct"} + +$ curl "localhost:8085/email?email=test-something-else" +{"error":"cannot parse \"test-something-else\" as email"} +``` + ### http2 server push http.Pusher is supported only **go1.8+**. See the [golang blog](https://go.dev/blog/h2push) for detail information. @@ -2134,7 +2188,7 @@ or network CIDRs from where clients which their request headers related to clien IP can be trusted. They can be IPv4 addresses, IPv4 CIDRs, IPv6 addresses or IPv6 CIDRs. -**Attention:** Gin trust all proxies by default if you don't specify a trusted +**Attention:** Gin trust all proxies by default if you don't specify a trusted proxy using the function above, **this is NOT safe**. At the same time, if you don't use any proxy, you can disable this feature by using `Engine.SetTrustedProxies(nil)`, then `Context.ClientIP()` will return the remote address directly to avoid some @@ -2163,7 +2217,7 @@ func main() { ``` **Notice:** If you are using a CDN service, you can set the `Engine.TrustedPlatform` -to skip TrustedProxies check, it has a higher priority than TrustedProxies. +to skip TrustedProxies check, it has a higher priority than TrustedProxies. Look at the example below: ```go