Skip to content

Commit

Permalink
Implement registering validator tags for custom map and slice types
Browse files Browse the repository at this point in the history
  • Loading branch information
kszafran committed Nov 5, 2021
1 parent 04ccf17 commit f26790b
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 0 deletions.
41 changes: 41 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand Down
48 changes: 48 additions & 0 deletions binding/default_validator.go
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 -
Expand Down
113 changes: 113 additions & 0 deletions binding/default_validator_test.go
Expand Up @@ -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
}

0 comments on commit f26790b

Please sign in to comment.