diff --git a/README.md b/README.md index a79e1c78..0259370c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Package validator ================ [![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 diff --git a/_examples/dive/main.go b/_examples/dive/main.go new file mode 100644 index 00000000..6c7de1e7 --- /dev/null +++ b/_examples/dive/main.go @@ -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) +} diff --git a/baked_in.go b/baked_in.go index af407d15..66540946 100644 --- a/baked_in.go +++ b/baked_in.go @@ -32,6 +32,8 @@ func wrapFunc(fn Func) FuncCtx { var ( restrictedTags = map[string]struct{}{ diveTag: {}, + keysTag: {}, + endKeysTag: {}, structOnlyTag: {}, omitempty: {}, skipValidationTag: {}, diff --git a/benchmarks_test.go b/benchmarks_test.go index b2a1c424..3e7e79f8 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -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() diff --git a/cache.go b/cache.go index 5e9f6d0f..8c59e165 100644 --- a/cache.go +++ b/cache.go @@ -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 diff --git a/doc.go b/doc.go index c9af494d..d3a69f74 100644 --- a/doc.go +++ b/doc.go @@ -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. diff --git a/validator.go b/validator.go index 5aac46cb..f180a9c7 100644 --- a/validator.go +++ b/validator.go @@ -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: diff --git a/validator_instance.go b/validator_instance.go index b5ba720b..d3a15438 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -25,6 +25,8 @@ const ( isdefault = "isdefault" skipValidationTag = "-" diveTag = "dive" + keysTag = "keys" + endKeysTag = "endkeys" requiredTag = "required" namespaceSeparator = "." leftBracket = "[" diff --git a/validator_test.go b/validator_test.go index 8c8d9bc5..7d085e85 100644 --- a/validator_test.go +++ b/validator_test.go @@ -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") +}