mirror of https://github.com/gin-gonic/gin.git
feat(binding): add support for custom validator / validation tags (#1068)
* feat(binding): Add support for custom validation tags * docs: Add example for custom validation tag * test(binding): Add test for registering custom validation
This commit is contained in:
parent
030b1aaf72
commit
26c3f42095
61
README.md
61
README.md
|
@ -487,6 +487,67 @@ func main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Validators
|
||||||
|
|
||||||
|
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
|
||||||
|
|
||||||
|
[embedmd]:# (examples/custom-validation/server.go go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
validator "gopkg.in/go-playground/validator.v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Booking struct {
|
||||||
|
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||||
|
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func bookableDate(
|
||||||
|
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||||
|
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||||
|
) bool {
|
||||||
|
if date, ok := field.Interface().(time.Time); ok {
|
||||||
|
today := time.Now()
|
||||||
|
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
binding.Validator.RegisterValidation("bookabledate", bookableDate)
|
||||||
|
route.GET("/bookable", getBookable)
|
||||||
|
route.Run(":8085")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBookable(c *gin.Context) {
|
||||||
|
var b Booking
|
||||||
|
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17"
|
||||||
|
{"message":"Booking dates are valid!"}
|
||||||
|
|
||||||
|
$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16"
|
||||||
|
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
|
||||||
|
```
|
||||||
|
|
||||||
### Only Bind Query String
|
### Only Bind Query String
|
||||||
|
|
||||||
`BindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
|
`BindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
|
|
||||||
package binding
|
package binding
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
validator "gopkg.in/go-playground/validator.v8"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MIMEJSON = "application/json"
|
MIMEJSON = "application/json"
|
||||||
|
@ -31,6 +35,11 @@ type StructValidator interface {
|
||||||
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
|
||||||
// Otherwise nil must be returned.
|
// Otherwise nil must be returned.
|
||||||
ValidateStruct(interface{}) error
|
ValidateStruct(interface{}) error
|
||||||
|
|
||||||
|
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
|
||||||
|
// NOTE: if the key already exists, the previous validation function will be replaced.
|
||||||
|
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
|
||||||
|
RegisterValidation(string, validator.Func) error
|
||||||
}
|
}
|
||||||
|
|
||||||
var Validator StructValidator = &defaultValidator{}
|
var Validator StructValidator = &defaultValidator{}
|
||||||
|
|
|
@ -28,6 +28,11 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
|
||||||
|
v.lazyinit()
|
||||||
|
return v.validate.RegisterValidation(key, fn)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *defaultValidator) lazyinit() {
|
func (v *defaultValidator) lazyinit() {
|
||||||
v.once.Do(func() {
|
v.once.Do(func() {
|
||||||
config := &validator.Config{TagName: "binding"}
|
config := &validator.Config{TagName: "binding"}
|
||||||
|
|
|
@ -6,9 +6,12 @@ package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
validator "gopkg.in/go-playground/validator.v8"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,3 +193,42 @@ func TestValidatePrimitives(t *testing.T) {
|
||||||
assert.NoError(t, validate(&str))
|
assert.NoError(t, validate(&str))
|
||||||
assert.Equal(t, str, "value")
|
assert.Equal(t, str, "value")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// structCustomValidation is a helper struct we use to check that
|
||||||
|
// custom validation can be registered on it.
|
||||||
|
// The `notone` binding directive is for custom validation and registered later.
|
||||||
|
type structCustomValidation struct {
|
||||||
|
Integer int `binding:"notone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// notOne is a custom validator meant to be used with `validator.v8` library.
|
||||||
|
// The method signature for `v9` is significantly different and this function
|
||||||
|
// would need to be changed for tests to pass after upgrade.
|
||||||
|
// See https://github.com/gin-gonic/gin/pull/1015.
|
||||||
|
func notOne(
|
||||||
|
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||||
|
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||||
|
) bool {
|
||||||
|
if val, ok := field.Interface().(int); ok {
|
||||||
|
return val != 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegisterValidation(t *testing.T) {
|
||||||
|
// This validates that the function `notOne` matches
|
||||||
|
// the expected function signature by `defaultValidator`
|
||||||
|
// and by extension the validator library.
|
||||||
|
err := Validator.RegisterValidation("notone", notOne)
|
||||||
|
// Check that we can register custom validation without error
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
// Create an instance which will fail validation
|
||||||
|
withOne := structCustomValidation{Integer: 1}
|
||||||
|
errs := validate(withOne)
|
||||||
|
|
||||||
|
// Check that we got back non-nil errs
|
||||||
|
assert.NotNil(t, errs)
|
||||||
|
// Check that the error matches expactation
|
||||||
|
assert.Error(t, errs, "", "", "notone")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
validator "gopkg.in/go-playground/validator.v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Booking struct {
|
||||||
|
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||||
|
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func bookableDate(
|
||||||
|
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
|
||||||
|
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
|
||||||
|
) bool {
|
||||||
|
if date, ok := field.Interface().(time.Time); ok {
|
||||||
|
today := time.Now()
|
||||||
|
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
route := gin.Default()
|
||||||
|
binding.Validator.RegisterValidation("bookabledate", bookableDate)
|
||||||
|
route.GET("/bookable", getBookable)
|
||||||
|
route.Run(":8085")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBookable(c *gin.Context) {
|
||||||
|
var b Booking
|
||||||
|
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue