mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-16 13:22:09 +08:00
Implement registering validator tags for custom map and slice types
This commit is contained in:
parent
04ccf172a6
commit
f26790bbda
41
README.md
41
README.md
@ -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).
|
||||
|
@ -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 -
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user