add map key validation support (#324)

pull/330/head v9.9.0
Dean Karn 7 years ago committed by GitHub
parent 1304298bf1
commit 61caf9d303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 117
      README.md
  2. 39
      _examples/dive/main.go
  3. 2
      baked_in.go
  4. 114
      benchmarks_test.go
  5. 50
      cache.go
  6. 27
      doc.go
  7. 14
      validator.go
  8. 2
      validator_instance.go
  9. 258
      validator_test.go

@ -1,7 +1,7 @@
Package validator
================
<img align="right" src="https://raw.githubusercontent.com/go-playground/validator/v9/logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
![Project status](https://img.shields.io/badge/version-9.8.0-green.svg)
![Project status](https://img.shields.io/badge/version-9.9.0-green.svg)
[![Build Status](https://semaphoreci.com/api/v1/joeybloggs/validator/branches/v9/badge.svg)](https://semaphoreci.com/joeybloggs/validator)
[![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=v9&service=github)](https://coveralls.io/github/go-playground/validator?branch=v9)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
@ -13,7 +13,8 @@ Package validator implements value validations for structs and individual fields
It has the following **unique** features:
- Cross Field and Cross Struct validations by using validation tags or custom validators.
- Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated.
- Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated.
- Ability to dive into both map keys and values for validation
- Handles type interface by determining it's underlying type prior to validation.
- Handles custom field types such as sql driver Valuer see [Valuer](https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29)
- Alias validation tags, which allows for mapping of several validations to a single tag for easier defining of validations on structs
@ -65,61 +66,69 @@ Please see http://godoc.org/gopkg.in/go-playground/validator.v9 for detailed usa
Benchmarks
------
###### Run on MacBook Pro (15-inch, 2017) Go version go1.9.1 linux/amd64
###### Run on MacBook Pro (15-inch, 2017) Go version go1.9.2 darwin/amd64
```go
go test -bench=. -benchmem=true
BenchmarkFieldSuccess-8 20000000 87.2 ns/op 0 B/op 0 allocs/op
BenchmarkFieldSuccessParallel-8 50000000 26.1 ns/op 0 B/op 0 allocs/op
BenchmarkFieldFailure-8 5000000 299 ns/op 208 B/op 4 allocs/op
BenchmarkFieldFailureParallel-8 20000000 100 ns/op 208 B/op 4 allocs/op
BenchmarkFieldDiveSuccess-8 2000000 645 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveSuccessParallel-8 10000000 198 ns/op 201 B/op 11 allocs/op
BenchmarkFieldDiveFailure-8 2000000 876 ns/op 412 B/op 16 allocs/op
BenchmarkFieldDiveFailureParallel-8 5000000 268 ns/op 413 B/op 16 allocs/op
BenchmarkFieldCustomTypeSuccess-8 10000000 228 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeSuccessParallel-8 20000000 70.0 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-8 5000000 286 ns/op 208 B/op 4 allocs/op
BenchmarkFieldCustomTypeFailureParallel-8 20000000 95.6 ns/op 208 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-8 2000000 857 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagSuccessParallel-8 3000000 397 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagFailure-8 3000000 495 ns/op 224 B/op 5 allocs/op
BenchmarkFieldOrTagFailureParallel-8 5000000 376 ns/op 224 B/op 5 allocs/op
BenchmarkStructLevelValidationSuccess-8 10000000 226 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationSuccessParallel-8 20000000 68.4 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationFailure-8 3000000 497 ns/op 304 B/op 8 allocs/op
BenchmarkStructLevelValidationFailureParallel-8 10000000 170 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-8 3000000 420 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeSuccessParallel-8 20000000 124 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeFailure-8 2000000 681 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleCustomTypeFailureParallel-8 10000000 244 ns/op 440 B/op 10 allocs/op
BenchmarkStructFilteredSuccess-8 2000000 659 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredSuccessParallel-8 10000000 211 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredFailure-8 3000000 482 ns/op 256 B/op 7 allocs/op
BenchmarkStructFilteredFailureParallel-8 10000000 162 ns/op 256 B/op 7 allocs/op
BenchmarkStructPartialSuccess-8 3000000 564 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialSuccessParallel-8 10000000 180 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialFailure-8 2000000 779 ns/op 480 B/op 11 allocs/op
BenchmarkStructPartialFailureParallel-8 5000000 268 ns/op 480 B/op 11 allocs/op
BenchmarkStructExceptSuccess-8 2000000 879 ns/op 496 B/op 12 allocs/op
BenchmarkFieldSuccess-8 20000000 79.9 ns/op 0 B/op 0 allocs/op
BenchmarkFieldSuccessParallel-8 50000000 25.0 ns/op 0 B/op 0 allocs/op
BenchmarkFieldFailure-8 5000000 281 ns/op 208 B/op 4 allocs/op
BenchmarkFieldFailureParallel-8 20000000 97.0 ns/op 208 B/op 4 allocs/op
BenchmarkFieldArrayDiveSuccess-8 3000000 591 ns/op 201 B/op 11 allocs/op
BenchmarkFieldArrayDiveSuccessParallel-8 10000000 195 ns/op 201 B/op 11 allocs/op
BenchmarkFieldArrayDiveFailure-8 2000000 878 ns/op 412 B/op 16 allocs/op
BenchmarkFieldArrayDiveFailureParallel-8 5000000 274 ns/op 413 B/op 16 allocs/op
BenchmarkFieldMapDiveSuccess-8 1000000 1279 ns/op 432 B/op 18 allocs/op
BenchmarkFieldMapDiveSuccessParallel-8 5000000 401 ns/op 432 B/op 18 allocs/op
BenchmarkFieldMapDiveFailure-8 1000000 1060 ns/op 512 B/op 16 allocs/op
BenchmarkFieldMapDiveFailureParallel-8 5000000 334 ns/op 512 B/op 16 allocs/op
BenchmarkFieldMapDiveWithKeysSuccess-8 1000000 1462 ns/op 480 B/op 21 allocs/op
BenchmarkFieldMapDiveWithKeysSuccessParallel-8 3000000 463 ns/op 480 B/op 21 allocs/op
BenchmarkFieldMapDiveWithKeysFailure-8 1000000 1414 ns/op 721 B/op 21 allocs/op
BenchmarkFieldMapDiveWithKeysFailureParallel-8 3000000 446 ns/op 721 B/op 21 allocs/op
BenchmarkFieldCustomTypeSuccess-8 10000000 211 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeSuccessParallel-8 20000000 65.9 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-8 5000000 270 ns/op 208 B/op 4 allocs/op
BenchmarkFieldCustomTypeFailureParallel-8 20000000 93.3 ns/op 208 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-8 2000000 729 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagSuccessParallel-8 5000000 367 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagFailure-8 3000000 472 ns/op 224 B/op 5 allocs/op
BenchmarkFieldOrTagFailureParallel-8 5000000 373 ns/op 224 B/op 5 allocs/op
BenchmarkStructLevelValidationSuccess-8 10000000 201 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationSuccessParallel-8 20000000 66.3 ns/op 32 B/op 2 allocs/op
BenchmarkStructLevelValidationFailure-8 3000000 468 ns/op 304 B/op 8 allocs/op
BenchmarkStructLevelValidationFailureParallel-8 10000000 172 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-8 5000000 376 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeSuccessParallel-8 20000000 126 ns/op 32 B/op 2 allocs/op
BenchmarkStructSimpleCustomTypeFailure-8 2000000 646 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleCustomTypeFailureParallel-8 10000000 240 ns/op 440 B/op 10 allocs/op
BenchmarkStructFilteredSuccess-8 3000000 582 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredSuccessParallel-8 10000000 198 ns/op 288 B/op 9 allocs/op
BenchmarkStructFilteredFailure-8 3000000 447 ns/op 256 B/op 7 allocs/op
BenchmarkStructFilteredFailureParallel-8 10000000 156 ns/op 256 B/op 7 allocs/op
BenchmarkStructPartialSuccess-8 3000000 536 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialSuccessParallel-8 10000000 175 ns/op 256 B/op 6 allocs/op
BenchmarkStructPartialFailure-8 2000000 738 ns/op 480 B/op 11 allocs/op
BenchmarkStructPartialFailureParallel-8 5000000 256 ns/op 480 B/op 11 allocs/op
BenchmarkStructExceptSuccess-8 2000000 835 ns/op 496 B/op 12 allocs/op
BenchmarkStructExceptSuccessParallel-8 10000000 163 ns/op 240 B/op 5 allocs/op
BenchmarkStructExceptFailure-8 2000000 734 ns/op 464 B/op 10 allocs/op
BenchmarkStructExceptFailureParallel-8 5000000 259 ns/op 464 B/op 10 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-8 3000000 432 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldSuccessParallel-8 10000000 129 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldFailure-8 2000000 671 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossFieldFailureParallel-8 10000000 229 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 2000000 628 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 10000000 182 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-8 2000000 872 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 5000000 267 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleSuccess-8 5000000 274 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleSuccessParallel-8 20000000 79.0 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleFailure-8 2000000 647 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleFailureParallel-8 10000000 224 ns/op 424 B/op 9 allocs/op
BenchmarkStructComplexSuccess-8 1000000 1557 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexSuccessParallel-8 3000000 473 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexFailure-8 300000 4373 ns/op 3041 B/op 53 allocs/op
BenchmarkStructComplexFailureParallel-8 1000000 1554 ns/op 3041 B/op 53 allocs/op
BenchmarkStructExceptFailure-8 2000000 682 ns/op 464 B/op 10 allocs/op
BenchmarkStructExceptFailureParallel-8 10000000 244 ns/op 464 B/op 10 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-8 5000000 392 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldSuccessParallel-8 20000000 126 ns/op 72 B/op 3 allocs/op
BenchmarkStructSimpleCrossFieldFailure-8 2000000 611 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossFieldFailureParallel-8 10000000 214 ns/op 304 B/op 8 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-8 3000000 567 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccessParallel-8 10000000 177 ns/op 80 B/op 4 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-8 2000000 807 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailureParallel-8 5000000 268 ns/op 320 B/op 9 allocs/op
BenchmarkStructSimpleSuccess-8 5000000 256 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleSuccessParallel-8 20000000 76.3 ns/op 0 B/op 0 allocs/op
BenchmarkStructSimpleFailure-8 2000000 625 ns/op 424 B/op 9 allocs/op
BenchmarkStructSimpleFailureParallel-8 10000000 219 ns/op 424 B/op 9 allocs/op
BenchmarkStructComplexSuccess-8 1000000 1431 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexSuccessParallel-8 3000000 427 ns/op 128 B/op 8 allocs/op
BenchmarkStructComplexFailure-8 300000 4065 ns/op 3041 B/op 53 allocs/op
BenchmarkStructComplexFailureParallel-8 1000000 1478 ns/op 3041 B/op 53 allocs/op
```
Complementary Software

@ -0,0 +1,39 @@
package main
import (
"fmt"
"gopkg.in/go-playground/validator.v9"
)
// Test ...
type Test struct {
Array []string `validate:"required,gt=0,dive,required"`
Map map[string]string `validate:"required,gt=0,dive,keys,keymax,endkeys,required,max=1000"`
}
// use a single instance of Validate, it caches struct info
var validate *validator.Validate
func main() {
validate = validator.New()
// registering alias so we can see the differences between
// map key, value validation errors
validate.RegisterAlias("keymax", "max=10")
var test Test
val(test)
test.Array = []string{""}
test.Map = map[string]string{"test > than 10": ""}
val(test)
}
func val(test Test) {
fmt.Println("testing")
err := validate.Struct(test)
fmt.Println(err)
}

@ -32,6 +32,8 @@ func wrapFunc(fn Func) FuncCtx {
var (
restrictedTags = map[string]struct{}{
diveTag: {},
keysTag: {},
endKeysTag: {},
structOnlyTag: {},
omitempty: {},
skipValidationTag: {},

@ -59,7 +59,7 @@ func BenchmarkFieldFailureParallel(b *testing.B) {
})
}
func BenchmarkFieldDiveSuccess(b *testing.B) {
func BenchmarkFieldArrayDiveSuccess(b *testing.B) {
validate := New()
@ -72,7 +72,7 @@ func BenchmarkFieldDiveSuccess(b *testing.B) {
}
}
func BenchmarkFieldDiveSuccessParallel(b *testing.B) {
func BenchmarkFieldArrayDiveSuccessParallel(b *testing.B) {
validate := New()
@ -86,7 +86,7 @@ func BenchmarkFieldDiveSuccessParallel(b *testing.B) {
})
}
func BenchmarkFieldDiveFailure(b *testing.B) {
func BenchmarkFieldArrayDiveFailure(b *testing.B) {
validate := New()
@ -98,7 +98,7 @@ func BenchmarkFieldDiveFailure(b *testing.B) {
}
}
func BenchmarkFieldDiveFailureParallel(b *testing.B) {
func BenchmarkFieldArrayDiveFailureParallel(b *testing.B) {
validate := New()
@ -112,6 +112,112 @@ func BenchmarkFieldDiveFailureParallel(b *testing.B) {
})
}
func BenchmarkFieldMapDiveSuccess(b *testing.B) {
validate := New()
m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.Var(m, "required,dive,required")
}
}
func BenchmarkFieldMapDiveSuccessParallel(b *testing.B) {
validate := New()
m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.Var(m, "required,dive,required")
}
})
}
func BenchmarkFieldMapDiveFailure(b *testing.B) {
validate := New()
m := map[string]string{"": "", "val3": "val3"}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.Var(m, "required,dive,required")
}
}
func BenchmarkFieldMapDiveFailureParallel(b *testing.B) {
validate := New()
m := map[string]string{"": "", "val3": "val3"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.Var(m, "required,dive,required")
}
})
}
func BenchmarkFieldMapDiveWithKeysSuccess(b *testing.B) {
validate := New()
m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.Var(m, "required,dive,keys,required,endkeys,required")
}
}
func BenchmarkFieldMapDiveWithKeysSuccessParallel(b *testing.B) {
validate := New()
m := map[string]string{"val1": "val1", "val2": "val2", "val3": "val3"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.Var(m, "required,dive,keys,required,endkeys,required")
}
})
}
func BenchmarkFieldMapDiveWithKeysFailure(b *testing.B) {
validate := New()
m := map[string]string{"": "", "val3": "val3"}
b.ResetTimer()
for n := 0; n < b.N; n++ {
validate.Var(m, "required,dive,keys,required,endkeys,required")
}
}
func BenchmarkFieldMapDiveWithKeysFailureParallel(b *testing.B) {
validate := New()
m := map[string]string{"": "", "val3": "val3"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
validate.Var(m, "required,dive,keys,required,endkeys,required")
}
})
}
func BenchmarkFieldCustomTypeSuccess(b *testing.B) {
validate := New()

@ -18,11 +18,14 @@ const (
typeStructOnly
typeDive
typeOr
typeKeys
typeEndKeys
)
const (
invalidValidation = "Invalid validation tag on field '%s'"
undefinedValidation = "Undefined validation function '%s' on field '%s'"
keysTagNotDefined = "'" + endKeysTag + "' tag encountered without a corresponding '" + keysTag + "' tag"
)
type structCache struct {
@ -88,11 +91,12 @@ type cTag struct {
aliasTag string
actualAliasTag string
param string
hasAlias bool
typeof tagType
keys *cTag // only populated when using tag's 'keys' and 'endkeys' for map key validation
next *cTag
hasTag bool
hasAlias bool
fn FuncCtx
next *cTag
}
func (v *Validate) extractStructCache(current reflect.Value, sName string) *cStruct {
@ -185,7 +189,6 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
// check map for alias and process new tags, otherwise process as usual
if tagsVal, found := v.aliases[t]; found {
if i == 0 {
firstCtag, current = v.parseFieldTagsRecursive(tagsVal, fieldName, t, true)
} else {
@ -197,10 +200,13 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
continue
}
var prevTag tagType
if i == 0 {
current = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true}
firstCtag = current
} else {
prevTag = current.typeof
current.next = &cTag{aliasTag: alias, hasAlias: hasAlias, hasTag: true}
current = current.next
}
@ -211,6 +217,44 @@ func (v *Validate) parseFieldTagsRecursive(tag string, fieldName string, alias s
current.typeof = typeDive
continue
case keysTag:
current.typeof = typeKeys
if i == 0 || prevTag != typeDive {
panic(fmt.Sprintf("'%s' tag must be immediately preceeded by the '%s' tag", keysTag, diveTag))
}
current.typeof = typeKeys
// need to pass along only keys tag
// need to increment i to skip over the keys tags
b := make([]byte, 0, 64)
i++
for ; i < len(tags); i++ {
b = append(b, tags[i]...)
b = append(b, ',')
if tags[i] == endKeysTag {
break
}
}
current.keys, _ = v.parseFieldTagsRecursive(string(b[:len(b)-1]), fieldName, "", false)
continue
case endKeysTag:
current.typeof = typeEndKeys
// if there are more in tags then there was no keysTag defined
// and an error should be thrown
if i != len(tags)-1 {
panic(keysTagNotDefined)
}
return
case omitempty:
current.typeof = typeOmitEmpty
continue

@ -193,7 +193,8 @@ Dive
This tells the validator to dive into a slice, array or map and validate that
level of the slice, array or map with the validation tags that follow.
Multidimensional nesting is also supported, each level you wish to dive will
require another dive tag.
require another dive tag. dive has some sub-tags, 'keys' & 'endkeys', please see
the Keys & EndKeys section just below.
Usage: dive
@ -211,6 +212,30 @@ Example #2
// []string will be spared validation
// required will be applied to string
Keys & EndKeys
These are to be used together directly after the dive tag and tells the validator
that anything between 'keys' and 'endkeys' applies to the keys of a map and not the
values; think of it like the 'dive' tag, but for map keys instead of values.
Multidimensional nesting is also supported, each level you wish to validate will
require another 'keys' and 'endkeys' tag. These tags are only valid for maps.
Usage: dive,keys,othertagvalidation(s),endkeys,valuevalidationtags
Example #1
map[string]string with validation tag "gt=0,dive,keys,eg=1|eq=2,endkeys,required"
// gt=0 will be applied to the map itself
// eg=1|eq=2 will be applied to the map keys
// required will be applied to map values
Example #2
map[[2]string]string with validation tag "gt=0,dive,keys,dive,eq=1|eq=2,endkeys,required"
// gt=0 will be applied to the map itself
// eg=1|eq=2 will be applied to each array element in the the map keys
// required will be applied to map values
Required
This validates that the value is not the data types default zero value.

@ -260,6 +260,9 @@ OUTER:
ct = ct.next
continue
case typeEndKeys:
return
case typeDive:
ct = ct.next
@ -294,7 +297,6 @@ OUTER:
reusableCF.altName = string(v.misc)
}
v.traverseField(ctx, parent, current.Index(i), ns, structNs, reusableCF, ct)
}
@ -325,7 +327,15 @@ OUTER:
reusableCF.altName = string(v.misc)
}
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct)
if ct != nil && ct.typeof == typeKeys && ct.keys != nil {
v.traverseField(ctx, parent, key, ns, structNs, reusableCF, ct.keys)
// can be nil when just keys being validated
if ct.next != nil {
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct.next)
}
} else {
v.traverseField(ctx, parent, current.MapIndex(key), ns, structNs, reusableCF, ct)
}
}
default:

@ -25,6 +25,8 @@ const (
isdefault = "isdefault"
skipValidationTag = "-"
diveTag = "dive"
keysTag = "keys"
endKeysTag = "endkeys"
requiredTag = "required"
namespaceSeparator = "."
leftBracket = "["

@ -12,12 +12,11 @@ import (
"testing"
"time"
. "gopkg.in/go-playground/assert.v1"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/fr"
"github.com/go-playground/locales/nl"
ut "github.com/go-playground/universal-translator"
. "gopkg.in/go-playground/assert.v1"
)
// NOTES:
@ -128,6 +127,26 @@ func AssertError(t *testing.T, err error, nsKey, structNsKey, field, structField
EqualSkip(t, 2, fe.Tag(), expectedTag)
}
func AssertDeepError(t *testing.T, err error, nsKey, structNsKey, field, structField, expectedTag, actualTag string) {
errs := err.(ValidationErrors)
found := false
var fe FieldError
for i := 0; i < len(errs); i++ {
if errs[i].Namespace() == nsKey && errs[i].StructNamespace() == structNsKey && errs[i].Tag() == expectedTag && errs[i].ActualTag() == actualTag {
found = true
fe = errs[i]
break
}
}
EqualSkip(t, 2, found, true)
NotEqualSkip(t, 2, fe, nil)
EqualSkip(t, 2, fe.Field(), field)
EqualSkip(t, 2, fe.StructField(), structField)
}
func getError(err error, nsKey, structNsKey string) FieldError {
errs := err.(ValidationErrors)
@ -7313,3 +7332,238 @@ func TestUniqueValidation(t *testing.T) {
}
PanicMatches(t, func() { validate.Var(1.0, "unique") }, "Bad field type float64")
}
func TestKeys(t *testing.T) {
type Test struct {
Test1 map[string]string `validate:"gt=0,dive,keys,eq=testkey,endkeys,eq=testval" json:"test1"`
Test2 map[int]int `validate:"gt=0,dive,keys,eq=3,endkeys,eq=4" json:"test2"`
Test3 map[int]int `validate:"gt=0,dive,keys,eq=3,endkeys" json:"test3"`
}
var tst Test
validate := New()
err := validate.Struct(tst)
NotEqual(t, err, nil)
Equal(t, len(err.(ValidationErrors)), 3)
AssertError(t, err.(ValidationErrors), "Test.Test1", "Test.Test1", "Test1", "Test1", "gt")
AssertError(t, err.(ValidationErrors), "Test.Test2", "Test.Test2", "Test2", "Test2", "gt")
AssertError(t, err.(ValidationErrors), "Test.Test3", "Test.Test3", "Test3", "Test3", "gt")
tst.Test1 = map[string]string{
"testkey": "testval",
}
tst.Test2 = map[int]int{
3: 4,
}
tst.Test3 = map[int]int{
3: 4,
}
err = validate.Struct(tst)
Equal(t, err, nil)
tst.Test1["badtestkey"] = "badtestvalue"
tst.Test2[10] = 11
err = validate.Struct(tst)
NotEqual(t, err, nil)
errs := err.(ValidationErrors)
Equal(t, len(errs), 4)
AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
AssertDeepError(t, errs, "Test.Test2[10]", "Test.Test2[10]", "Test2[10]", "Test2[10]", "eq", "eq")
AssertDeepError(t, errs, "Test.Test2[10]", "Test.Test2[10]", "Test2[10]", "Test2[10]", "eq", "eq")
type Test2 struct {
NestedKeys map[[1]string]string `validate:"gt=0,dive,keys,dive,eq=innertestkey,endkeys,eq=outertestval"`
}
var tst2 Test2
err = validate.Struct(tst2)
NotEqual(t, err, nil)
Equal(t, len(err.(ValidationErrors)), 1)
AssertError(t, err.(ValidationErrors), "Test2.NestedKeys", "Test2.NestedKeys", "NestedKeys", "NestedKeys", "gt")
tst2.NestedKeys = map[[1]string]string{
[1]string{"innertestkey"}: "outertestval",
}
err = validate.Struct(tst2)
Equal(t, err, nil)
tst2.NestedKeys[[1]string{"badtestkey"}] = "badtestvalue"
err = validate.Struct(tst2)
NotEqual(t, err, nil)
errs = err.(ValidationErrors)
Equal(t, len(errs), 2)
AssertDeepError(t, errs, "Test2.NestedKeys[[badtestkey]][0]", "Test2.NestedKeys[[badtestkey]][0]", "NestedKeys[[badtestkey]][0]", "NestedKeys[[badtestkey]][0]", "eq", "eq")
AssertDeepError(t, errs, "Test2.NestedKeys[[badtestkey]]", "Test2.NestedKeys[[badtestkey]]", "NestedKeys[[badtestkey]]", "NestedKeys[[badtestkey]]", "eq", "eq")
// test bad tag definitions
PanicMatches(t, func() { validate.Var(map[string]string{"key": "val"}, "endkeys,dive,eq=val") }, "'endkeys' tag encountered without a corresponding 'keys' tag")
PanicMatches(t, func() { validate.Var(1, "keys,eq=1,endkeys") }, "'keys' tag must be immediately preceeded by the 'dive' tag")
// test custom tag name
validate = New()
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
err = validate.Struct(tst)
NotEqual(t, err, nil)
errs = err.(ValidationErrors)
Equal(t, len(errs), 4)
AssertDeepError(t, errs, "Test.test1[badtestkey]", "Test.Test1[badtestkey]", "test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
AssertDeepError(t, errs, "Test.test1[badtestkey]", "Test.Test1[badtestkey]", "test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
AssertDeepError(t, errs, "Test.test2[10]", "Test.Test2[10]", "test2[10]", "Test2[10]", "eq", "eq")
AssertDeepError(t, errs, "Test.test2[10]", "Test.Test2[10]", "test2[10]", "Test2[10]", "eq", "eq")
}
// Thanks @adrian-sgn specific test for your specific scenario
func TestKeysCustomValidation(t *testing.T) {
type LangCode string
type Label map[LangCode]string
type TestMapStructPtr struct {
Label Label `validate:"dive,keys,lang_code,endkeys,required"`
}
validate := New()
validate.RegisterValidation("lang_code", func(fl FieldLevel) bool {
validLangCodes := map[LangCode]struct{}{
"en": {},
"es": {},
"pt": {},
}
_, ok := validLangCodes[fl.Field().Interface().(LangCode)]
return ok
})
label := Label{
"en": "Good morning!",
"pt": "",
"es": "¡Buenos días!",
"xx": "Bad key",
"xxx": "",
}
err := validate.Struct(TestMapStructPtr{label})
NotEqual(t, err, nil)
errs := err.(ValidationErrors)
Equal(t, len(errs), 4)
AssertDeepError(t, errs, "TestMapStructPtr.Label[xx]", "TestMapStructPtr.Label[xx]", "Label[xx]", "Label[xx]", "lang_code", "lang_code")
AssertDeepError(t, errs, "TestMapStructPtr.Label[pt]", "TestMapStructPtr.Label[pt]", "Label[pt]", "Label[pt]", "required", "required")
AssertDeepError(t, errs, "TestMapStructPtr.Label[xxx]", "TestMapStructPtr.Label[xxx]", "Label[xxx]", "Label[xxx]", "lang_code", "lang_code")
AssertDeepError(t, errs, "TestMapStructPtr.Label[xxx]", "TestMapStructPtr.Label[xxx]", "Label[xxx]", "Label[xxx]", "required", "required")
// find specific error
var e FieldError
for _, e = range errs {
if e.Namespace() == "TestMapStructPtr.Label[xxx]" {
break
}
}
Equal(t, e.Param(), "")
Equal(t, e.Value().(LangCode), LangCode("xxx"))
for _, e = range errs {
if e.Namespace() == "TestMapStructPtr.Label[xxx]" && e.Tag() == "required" {
break
}
}
Equal(t, e.Param(), "")
Equal(t, e.Value().(string), "")
}
func TestKeyOrs(t *testing.T) {
type Test struct {
Test1 map[string]string `validate:"gt=0,dive,keys,eq=testkey|eq=testkeyok,endkeys,eq=testval" json:"test1"`
}
var tst Test
validate := New()
err := validate.Struct(tst)
NotEqual(t, err, nil)
Equal(t, len(err.(ValidationErrors)), 1)
AssertError(t, err.(ValidationErrors), "Test.Test1", "Test.Test1", "Test1", "Test1", "gt")
tst.Test1 = map[string]string{
"testkey": "testval",
}
err = validate.Struct(tst)
Equal(t, err, nil)
tst.Test1["badtestkey"] = "badtestval"
err = validate.Struct(tst)
NotEqual(t, err, nil)
errs := err.(ValidationErrors)
Equal(t, len(errs), 2)
AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq=testkey|eq=testkeyok", "eq=testkey|eq=testkeyok")
AssertDeepError(t, errs, "Test.Test1[badtestkey]", "Test.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
validate.RegisterAlias("okkey", "eq=testkey|eq=testkeyok")
type Test2 struct {
Test1 map[string]string `validate:"gt=0,dive,keys,okkey,endkeys,eq=testval" json:"test1"`
}
var tst2 Test2
err = validate.Struct(tst2)
NotEqual(t, err, nil)
Equal(t, len(err.(ValidationErrors)), 1)
AssertError(t, err.(ValidationErrors), "Test2.Test1", "Test2.Test1", "Test1", "Test1", "gt")
tst2.Test1 = map[string]string{
"testkey": "testval",
}
err = validate.Struct(tst2)
Equal(t, err, nil)
tst2.Test1["badtestkey"] = "badtestval"
err = validate.Struct(tst2)
NotEqual(t, err, nil)
errs = err.(ValidationErrors)
Equal(t, len(errs), 2)
AssertDeepError(t, errs, "Test2.Test1[badtestkey]", "Test2.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "okkey", "eq=testkey|eq=testkeyok")
AssertDeepError(t, errs, "Test2.Test1[badtestkey]", "Test2.Test1[badtestkey]", "Test1[badtestkey]", "Test1[badtestkey]", "eq", "eq")
}

Loading…
Cancel
Save