Implement registering validator tags for custom map and slice types

This commit is contained in:
Krzysztof Szafrański 2021-11-05 18:28:36 +01:00
parent 04ccf172a6
commit f26790bbda
3 changed files with 202 additions and 0 deletions

View File

@ -43,6 +43,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Controlling Log output coloring](#controlling-log-output-coloring)
- [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators)
- [Custom Map and Slice Validator Tags](#custom-map-and-slice-validator-tags)
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
@ -838,6 +839,46 @@ $ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more.
### Custom Map and Slice Validator Tags
It is possible to register validator tags for custom map and slice types.
```go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type Person struct {
FirstName string `json:"firstName" binding:"required,lte=64"`
LastName string `json:"lastName" binding:"required,lte=64"`
}
type Managers map[string]Person
func main() {
route := gin.Default()
binding.RegisterValidatorTag("dive,keys,oneof=accounting finance operations,endkeys", Managers{})
route.POST("/managers", configureManagers)
route.Run(":8085")
}
func configureManagers(c *gin.Context) {
var m Managers
if err := c.ShouldBindJSON(&m); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Manager configuration is valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
```
### Only Bind Query String
`ShouldBindQuery` 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).

View File

@ -12,6 +12,33 @@ import (
"github.com/go-playground/validator/v10"
)
var validatorTags = make(map[reflect.Type]string)
// RegisterValidatorTag registers a validator tag against a number of types.
// This allows defining validation for custom slice, array, and map types. For example:
// type CustomMap map[int]string
// ...
// binding.RegisterValidatorTag("gt=0", CustomMap{})
//
// Do not use the "dive" tag (unless in conjunction with "keys"/"endkeys").
// Slice/array/map elements are validated independently.
//
// This function will not have any effect is binding.Validator has been replaced.
//
// NOTE: This function is not thread-safe. It is intended that these all be registered prior to any validation.
func RegisterValidatorTag(tag string, types ...interface{}) {
for _, typ := range types {
t := reflect.TypeOf(typ)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Slice && t.Kind() != reflect.Array && t.Kind() != reflect.Map {
panic("validator tags can be registered only for slices, arrays, and maps")
}
validatorTags[t] = tag
}
}
type defaultValidator struct {
once sync.Once
validate *validator.Validate
@ -81,6 +108,13 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
return v.validateStruct(obj)
case reflect.Slice, reflect.Array:
var errs validator.ValidationErrors
if tag, ok := validatorTags[value.Type()]; ok {
if err := v.validateVar(obj, tag); err != nil {
errs = append(errs, err.(validator.ValidationErrors)...) // nolint: errorlint
}
}
count := value.Len()
for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
@ -89,12 +123,20 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
}
}
}
if len(errs) > 0 {
return errs
}
return nil
case reflect.Map:
var errs validator.ValidationErrors
if tag, ok := validatorTags[value.Type()]; ok {
if err := v.validateVar(obj, tag); err != nil {
errs = append(errs, err.(validator.ValidationErrors)...) // nolint: errorlint
}
}
for _, key := range value.MapKeys() {
if err := v.ValidateStruct(value.MapIndex(key).Interface()); err != nil {
for _, fieldError := range err.(validator.ValidationErrors) { // nolint: errorlint
@ -117,6 +159,12 @@ func (v *defaultValidator) validateStruct(obj interface{}) error {
return v.validate.Struct(obj)
}
// validateStruct receives slice, array, and map types
func (v *defaultValidator) validateVar(obj interface{}, tag string) error {
v.lazyinit()
return v.validate.Var(obj, tag)
}
// Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -

View File

@ -96,3 +96,116 @@ func TestDefaultValidator(t *testing.T) {
})
}
}
func TestRegisterValidatorTag(t *testing.T) {
type CustomSlice []struct {
A string
}
type CustomArray [10]struct {
A string
}
type CustomMap map[string]struct {
A string
}
type CustomStruct struct {
A string
}
type CustomInt int
// only slice, array, and map types are accepted
RegisterValidatorTag("gt=0", CustomSlice{})
RegisterValidatorTag("gt=0", &CustomSlice{})
RegisterValidatorTag("gt=0", CustomArray{})
RegisterValidatorTag("gt=0", &CustomArray{})
RegisterValidatorTag("gt=0", CustomMap{})
RegisterValidatorTag("gt=0", &CustomMap{})
assert.Panics(t, func() { RegisterValidatorTag("gt=0", CustomStruct{}) })
assert.Panics(t, func() { RegisterValidatorTag("gt=0", &CustomStruct{}) })
assert.Panics(t, func() { var i CustomInt; RegisterValidatorTag("gt=0", i) })
assert.Panics(t, func() { var i CustomInt; RegisterValidatorTag("gt=0", &i) })
}
func TestValidatorTagsSlice(t *testing.T) {
type CustomSlice []struct {
A string `binding:"max=8"`
}
var (
invalidSlice = CustomSlice{{"12345678"}}
invalidVal = CustomSlice{{"123456789"}, {"abcdefgh"}}
validSlice = CustomSlice{{"12345678"}, {"abcdefgh"}}
invalidSliceVal = CustomSlice{{"123456789"}}
)
v := &defaultValidator{}
// no tags registered for the slice itself yet, so only elements are validated
assert.NoError(t, v.ValidateStruct(invalidSlice))
assert.Error(t, v.ValidateStruct(invalidVal))
assert.NoError(t, v.ValidateStruct(validSlice))
assert.NoError(t, v.ValidateStruct(&invalidSlice))
assert.Error(t, v.ValidateStruct(&invalidVal))
assert.NoError(t, v.ValidateStruct(&validSlice))
err := v.ValidateStruct(invalidSliceVal)
assert.Error(t, err)
assert.Len(t, err, 1) // only value error
RegisterValidatorTag("gt=1", CustomSlice{})
assert.Error(t, v.ValidateStruct(invalidSlice))
assert.Error(t, v.ValidateStruct(invalidVal))
assert.NoError(t, v.ValidateStruct(validSlice))
assert.Error(t, v.ValidateStruct(&invalidSlice))
assert.Error(t, v.ValidateStruct(&invalidVal))
assert.NoError(t, v.ValidateStruct(&validSlice))
err = v.ValidateStruct(invalidSliceVal)
assert.Error(t, err)
assert.Len(t, err, 2) // both slice length and value error
}
func TestValidatorTagsMap(t *testing.T) {
type CustomMap map[string]struct {
B int `binding:"gt=0"`
}
var (
invalidMap = CustomMap{"12345678": {1}}
invalidKey = CustomMap{"123456789": {1}, "abcdefgh": {2}}
invalidVal = CustomMap{"12345678": {0}, "abcdefgh": {2}}
invalidMapVal = CustomMap{"12345678": {0}}
validMap = CustomMap{"12345678": {1}, "abcdefgh": {2}}
)
v := &defaultValidator{}
// no tags registered for the map itself yet, so only values are validated
assert.NoError(t, v.ValidateStruct(invalidMap))
assert.NoError(t, v.ValidateStruct(invalidKey))
assert.Error(t, v.ValidateStruct(invalidVal))
assert.NoError(t, v.ValidateStruct(validMap))
assert.NoError(t, v.ValidateStruct(&invalidMap))
assert.NoError(t, v.ValidateStruct(&invalidKey))
assert.Error(t, v.ValidateStruct(&invalidVal))
assert.NoError(t, v.ValidateStruct(&validMap))
err := v.ValidateStruct(invalidMapVal)
assert.Error(t, err)
assert.Len(t, err, 1) // only value error
RegisterValidatorTag("gt=1,dive,keys,max=8,endkeys", CustomMap{})
assert.Error(t, v.ValidateStruct(invalidMap))
assert.Error(t, v.ValidateStruct(invalidKey))
assert.Error(t, v.ValidateStruct(invalidVal))
assert.NoError(t, v.ValidateStruct(validMap))
assert.Error(t, v.ValidateStruct(&invalidMap))
assert.Error(t, v.ValidateStruct(&invalidKey))
assert.Error(t, v.ValidateStruct(&invalidVal))
assert.NoError(t, v.ValidateStruct(&validMap))
err = v.ValidateStruct(invalidMapVal)
assert.Error(t, err)
assert.Len(t, err, 2) // both map size and value errors
}