diff --git a/.gitignore b/.gitignore index 4159020..7e9b500 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ _testmain.go *.test *.prof *.test -*.out \ No newline at end of file +*.out +cover.html +README.html \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 68398d9..389db22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,14 @@ notificaitons: on_failure: always go: - - 1.2 - 1.3 - 1.4 - - tip \ No newline at end of file + - tip + +script: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go test -v -covermode=count -coverprofile=cover.out + +after_success: + - goveralls -coverprofile=cover.out -service=travis-ci -repotoken I6M8FiXZzErImgwMotJ7fwFlHOX8Hqdq1 \ No newline at end of file diff --git a/README.md b/README.md index 4c1991a..36ac9da 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,25 @@ Package validator ================ + +[![Join the chat at https://gitter.im/bluesuncorp/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bluesuncorp/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bluesuncorp/validator.svg?branch=v5.1)](https://travis-ci.org/bluesuncorp/validator) +[![Coverage Status](https://coveralls.io/repos/bluesuncorp/validator/badge.svg?branch=v5)](https://coveralls.io/r/bluesuncorp/validator?branch=v5) [![GoDoc](https://godoc.org/gopkg.in/bluesuncorp/validator.v5?status.svg)](https://godoc.org/gopkg.in/bluesuncorp/validator.v5) Package validator implements value validations for structs and individual fields based on tags. -It is also capable of Cross Field and Cross Struct validations. + +It has the following **unique** features: + +- Cross Field and Cross Struct validations. +- Slice, Array and Map diving, which allows any or all levels of a multidimensional field to be validated. +- Handles type interface by determining it's underlying type prior to validation. Installation -============ +------------ Use go get. - go get -u gopkg.in/bluesuncorp/validator.v5 + go get gopkg.in/bluesuncorp/validator.v5 or to update @@ -22,12 +30,114 @@ Then import the validator package into your own code. import "gopkg.in/bluesuncorp/validator.v5" Usage and documentation -======================= +------ Please see http://godoc.org/gopkg.in/bluesuncorp/validator.v5 for detailed usage docs. +##### Example: +```go +package main + +import ( + "fmt" + + "gopkg.in/bluesuncorp/validator.v5" +) + +// User contains user information +type User struct { + FirstName string `validate:"required"` + LastName string `validate:"required"` + Age uint8 `validate:"gte=0,lte=130"` + Email string `validate:"required,email"` + FavouriteColor string `validate:"hexcolor|rgb|rgba"` + Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage... +} + +// Address houses a users address information +type Address struct { + Street string `validate:"required"` + City string `validate:"required"` + Planet string `validate:"required"` + Phone string `validate:"required"` +} + +var validate *validator.Validate + +func main() { + + validate = validator.New("validate", validator.BakedInValidators) + + address := &Address{ + Street: "Eavesdown Docks", + Planet: "Persphone", + Phone: "none", + } + + user := &User{ + FirstName: "Badger", + LastName: "Smith", + Age: 135, + Email: "Badger.Smith@gmail.com", + FavouriteColor: "#000", + Addresses: []*Address{address}, + } + + // returns nil or *StructErrors + errs := validate.Struct(user) + + if errs != nil { + + // err will be of type *FieldError + err := errs.Errors["Age"] + fmt.Println(err.Error()) // output: Field validation for "Age" failed on the "lte" tag + fmt.Println(err.Field) // output: Age + fmt.Println(err.Tag) // output: lte + fmt.Println(err.Kind) // output: uint8 + fmt.Println(err.Type) // output: uint8 + fmt.Println(err.Param) // output: 130 + fmt.Println(err.Value) // output: 135 + + // or if you prefer you can use the Flatten function + // NOTE: I find this usefull when using a more hard static approach of checking field errors. + // The above, is best for passing to some generic code to say parse the errors. i.e. I pass errs + // to a routine which loops through the errors, creates and translates the error message into the + // users locale and returns a map of map[string]string // field and error which I then use + // within the HTML rendering. + + flat := errs.Flatten() + fmt.Println(flat) // output: map[Age:Field validation for "Age" failed on the "lte" tag Addresses[0].Address.City:Field validation for "City" failed on the "required" tag] + err = flat["Addresses[0].Address.City"] + fmt.Println(err.Field) // output: City + fmt.Println(err.Tag) // output: required + fmt.Println(err.Kind) // output: string + fmt.Println(err.Type) // output: string + fmt.Println(err.Param) // output: + fmt.Println(err.Value) // output: + + // from here you can create your own error messages in whatever language you wish + return + } + + // save user to database +} +``` + +Benchmarks +------ +###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 +```go +$ go test -cpu=4 -bench=. -benchmem=true +PASS +BenchmarkValidateField-4 3000000 429 ns/op 192 B/op 2 allocs/op +BenchmarkValidateStructSimple-4 500000 2877 ns/op 657 B/op 10 allocs/op +BenchmarkTemplateParallelSimple-4 500000 3097 ns/op 657 B/op 10 allocs/op +BenchmarkValidateStructLarge-4 100000 15228 ns/op 4350 B/op 62 allocs/op +BenchmarkTemplateParallelLarge-4 100000 14257 ns/op 4354 B/op 62 allocs/op +``` + How to Contribute -================= +------ There will always be a development branch for each version i.e. `v1-development`. In order to contribute, please make your pull requests against those branches. @@ -40,5 +150,5 @@ I strongly encourage everyone whom creates a custom validation function to contr help make this package even better. License -======= +------ Distributed under MIT License, please see license file in code for more details. diff --git a/baked_in.go b/baked_in.go index 9ccfa9e..701757c 100644 --- a/baked_in.go +++ b/baked_in.go @@ -50,6 +50,141 @@ var BakedInValidators = map[string]Func{ "excludes": excludes, "excludesall": excludesAll, "excludesrune": excludesRune, + "isbn": isISBN, + "isbn10": isISBN10, + "isbn13": isISBN13, + "uuid": isUUID, + "uuid3": isUUID3, + "uuid4": isUUID4, + "uuid5": isUUID5, + "ascii": isASCII, + "printascii": isPrintableASCII, + "multibyte": hasMultiByteCharacter, + "datauri": isDataURI, + "latitude": isLatitude, + "longitude": isLongitude, + "ssn": isSSN, +} + +func isSSN(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) != 11 { + return false + } + + return matchesRegex(sSNRegex, field) +} + +func isLongitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(longitudeRegex, field) +} + +func isLatitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(latitudeRegex, field) +} + +func isDataURI(top interface{}, current interface{}, field interface{}, param string) bool { + + uri := strings.SplitN(field.(string), ",", 2) + + if len(uri) != 2 { + return false + } + + if !matchesRegex(dataURIRegex, uri[0]) { + return false + } + + return isBase64(top, current, uri[1], param) +} + +func hasMultiByteCharacter(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) == 0 { + return true + } + + return matchesRegex(multibyteRegex, field) +} + +func isPrintableASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(printableASCIIRegex, field) +} + +func isASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(aSCIIRegex, field) +} + +func isUUID5(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID5Regex, field) +} + +func isUUID4(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID4Regex, field) +} + +func isUUID3(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID3Regex, field) +} + +func isUUID(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUIDRegex, field) +} + +func isISBN(top interface{}, current interface{}, field interface{}, param string) bool { + return isISBN10(top, current, field, param) || isISBN13(top, current, field, param) +} + +func isISBN13(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 4), " ", "", 4) + + if !matchesRegex(iSBN13Regex, s) { + return false + } + + var checksum int32 + var i int32 + + factor := []int32{1, 3} + + for i = 0; i < 12; i++ { + checksum += factor[i%2] * int32(s[i]-'0') + } + + if (int32(s[12]-'0'))-((10-(checksum%10))%10) == 0 { + return true + } + + return false +} + +func isISBN10(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 3), " ", "", 3) + + if !matchesRegex(iSBN10Regex, s) { + return false + } + + var checksum int32 + var i int32 + + for i = 0; i < 9; i++ { + checksum += (i + 1) * int32(s[i]-'0') + } + + if s[9] == 'X' { + checksum += 10 * 10 + } else { + checksum += 10 * int32(s[9]-'0') + } + + if checksum%11 == 0 { + return true + } + + return false } func excludesRune(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/benchmarks_test.go b/benchmarks_test.go new file mode 100644 index 0000000..ee836c2 --- /dev/null +++ b/benchmarks_test.go @@ -0,0 +1,163 @@ +package validator + +import "testing" + +func BenchmarkValidateField(b *testing.B) { + for n := 0; n < b.N; n++ { + validate.Field("1", "len=1") + } +} + +func BenchmarkValidateStructSimple(b *testing.B) { + + type Foo struct { + StringValue string `validate:"min=5,max=10"` + IntValue int `validate:"min=5,max=10"` + } + + validFoo := &Foo{StringValue: "Foobar", IntValue: 7} + invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} + + for n := 0; n < b.N; n++ { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } +} + +func BenchmarkTemplateParallelSimple(b *testing.B) { + + type Foo struct { + StringValue string `validate:"min=5,max=10"` + IntValue int `validate:"min=5,max=10"` + } + + validFoo := &Foo{StringValue: "Foobar", IntValue: 7} + invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } + }) +} + +func BenchmarkValidateStructLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + for n := 0; n < b.N; n++ { + validate.Struct(tSuccess) + validate.Struct(tFail) + } +} + +func BenchmarkTemplateParallelLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(tSuccess) + validate.Struct(tFail) + } + }) +} diff --git a/doc.go b/doc.go index 1b02c0f..f45ef34 100644 --- a/doc.go +++ b/doc.go @@ -10,7 +10,7 @@ Validate A simple example usage: - type UserDetail { + type UserDetail struct { Details string `validate:"-"` } @@ -143,6 +143,11 @@ NOTE: Baked In Cross field validation only compares fields on the same struct, if cross field + cross struct validation is needed your own custom validator should be implemented. +NOTE2: comma is the default separator of validation tags, if you wish to have a comma +included within the parameter i.e. excludesall=, you will need to use the UTF-8 hex +representation 0x2C, which is replaced in the code as a comma, so the above will +become excludesall=0x2C + Here is a list of the current built in validators: - @@ -163,11 +168,30 @@ Here is a list of the current built in validators: verify it has been assigned. omitempty - Allows conitional validation, for example if a field is not set with + Allows conditional validation, for example if a field is not set with a value (Determined by the required validator) then other validation such as min or max won't run, but if a value is set validation will run. (Usage: omitempty) + 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. (Usage: dive) + Example: [][]string with validation tag "gt=0,dive,len=1,dive,required" + gt=0 will be applied to [] + len=1 will be applied to []string + required will be applied to string + Example2: [][]string with validation tag "gt=0,dive,dive,required" + gt=0 will be applied to [] + []string will be spared validation + required will be applied to string + NOTE: in Example2 if the required validation failed, but all others passed + the hierarchy of FieldError's in the middle with have their IsPlaceHolder field + set to true. If a FieldError has IsSliceOrMap=true or IsMap=true then the + FieldError is a Slice or Map field and if IsPlaceHolder=true then contains errors + within its SliceOrArrayErrs or MapErrs fields. + required This validates that the value is not the data types default value. For numbers ensures value is not zero. For strings ensures value is @@ -357,6 +381,66 @@ Here is a list of the current built in validators: This validates that a string value does not contain the supplied rune value. (Usage: excludesrune=@) + isbn + This validates that a string value contains a valid isbn10 or isbn13 value. + (Usage: isbn) + + isbn10 + This validates that a string value contains a valid isbn10 value. + (Usage: isbn10) + + isbn13 + This validates that a string value contains a valid isbn13 value. + (Usage: isbn13) + + uuid + This validates that a string value contains a valid UUID. + (Usage: uuid) + + uuid3 + This validates that a string value contains a valid version 3 UUID. + (Usage: uuid3) + + uuid4 + This validates that a string value contains a valid version 4 UUID. + (Usage: uuid4) + + uuid5 + This validates that a string value contains a valid version 5 UUID. + (Usage: uuid5) + + ascii + This validates that a string value contains only ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: ascii) + + asciiprint + This validates that a string value contains only printable ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: asciiprint) + + multibyte + This validates that a string value contains one or more multibyte characters. + NOTE: if the string is blank, this validates as true. + (Usage: multibyte) + + datauri + This validates that a string value contains a valid DataURI. + NOTE: this will also validate that the data portion is valid base64 + (Usage: datauri) + + latitude + This validates that a string value contains a valid latitude. + (Usage: latitude) + + longitude + This validates that a string value contains a valid longitude. + (Usage: longitude) + + ssn + This validates that a string value contains a valid U.S. Social Security Number. + (Usage: ssn) + Validator notes: regex diff --git a/examples/simple.go b/examples/simple.go new file mode 100644 index 0000000..59cb1a9 --- /dev/null +++ b/examples/simple.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + + "gopkg.in/bluesuncorp/validator.v5" +) + +// User contains user information +type User struct { + FirstName string `validate:"required"` + LastName string `validate:"required"` + Age uint8 `validate:"gte=0,lte=130"` + Email string `validate:"required,email"` + FavouriteColor string `validate:"hexcolor|rgb|rgba"` + Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage... +} + +// Address houses a users address information +type Address struct { + Street string `validate:"required"` + City string `validate:"required"` + Planet string `validate:"required"` + Phone string `validate:"required"` +} + +var validate *validator.Validate + +func main() { + + validate = validator.New("validate", validator.BakedInValidators) + + address := &Address{ + Street: "Eavesdown Docks", + Planet: "Persphone", + Phone: "none", + } + + user := &User{ + FirstName: "Badger", + LastName: "Smith", + Age: 135, + Email: "Badger.Smith@gmail.com", + FavouriteColor: "#000", + Addresses: []*Address{address}, + } + + // returns nil or *StructErrors + errs := validate.Struct(user) + + if errs != nil { + + // err will be of type *FieldError + err := errs.Errors["Age"] + fmt.Println(err.Error()) // output: Field validation for "Age" failed on the "lte" tag + fmt.Println(err.Field) // output: Age + fmt.Println(err.Tag) // output: lte + fmt.Println(err.Kind) // output: uint8 + fmt.Println(err.Type) // output: uint8 + fmt.Println(err.Param) // output: 130 + fmt.Println(err.Value) // output: 135 + + // or if you prefer you can use the Flatten function + // NOTE: I find this usefull when using a more hard static approach of checking field errors. + // The above, is best for passing to some generic code to say parse the errors. i.e. I pass errs + // to a routine which loops through the errors, creates and translates the error message into the + // users locale and returns a map of map[string]string // field and error which I then use + // within the HTML rendering. + + flat := errs.Flatten() + fmt.Println(flat) // output: map[Age:Field validation for "Age" failed on the "lte" tag Addresses[0].Address.City:Field validation for "City" failed on the "required" tag] + err = flat["Addresses[0].Address.City"] + fmt.Println(err.Field) // output: City + fmt.Println(err.Tag) // output: required + fmt.Println(err.Kind) // output: string + fmt.Println(err.Type) // output: string + fmt.Println(err.Param) // output: + fmt.Println(err.Value) // output: + + // from here you can create your own error messages in whatever language you wish + return + } + + // save user to database +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..c5dd351 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,95 @@ +package validator_test + +import ( + "fmt" + + "../validator" +) + +func ExampleValidate_new() { + validator.New("validate", validator.BakedInValidators) +} + +func ExampleValidate_addFunction() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + fn := func(top interface{}, current interface{}, field interface{}, param string) bool { + return field.(string) == "hello" + } + + validate.AddFunction("valueishello", fn) + + message := "hello" + err := validate.Field(message, "valueishello") + fmt.Println(err) + //Output: + // +} + +func ExampleValidate_field() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + i := 0 + err := validate.Field(i, "gt=1,lte=10") + fmt.Println(err.Field) + fmt.Println(err.Tag) + fmt.Println(err.Kind) // NOTE: Kind and Type can be different i.e. time Kind=struct and Type=time.Time + fmt.Println(err.Type) + fmt.Println(err.Param) + fmt.Println(err.Value) + //Output: + // + //gt + //int + //int + //1 + //0 +} + +func ExampleValidate_struct() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + type ContactInformation struct { + Phone string `validate:"required"` + Street string `validate:"required"` + City string `validate:"required"` + } + + type User struct { + Name string `validate:"required,excludesall=!@#$%^&*()_+-=:;?/0x2C"` // 0x2C = comma (,) + Age int8 `validate:"required,gt=0,lt=150"` + Email string `validate:"email"` + ContactInformation []*ContactInformation + } + + contactInfo := &ContactInformation{ + Street: "26 Here Blvd.", + City: "Paradeso", + } + + user := &User{ + Name: "Joey Bloggs", + Age: 31, + Email: "joeybloggs@gmail.com", + ContactInformation: []*ContactInformation{contactInfo}, + } + + structError := validate.Struct(user) + for _, fieldError := range structError.Errors { + fmt.Println(fieldError.Field) // Phone + fmt.Println(fieldError.Tag) // required + //... and so forth + //Output: + //Phone + //required + } +} diff --git a/regexes.go b/regexes.go index e5ed0fa..d3e8d80 100644 --- a/regexes.go +++ b/regexes.go @@ -3,33 +3,59 @@ package validator import "regexp" const ( - alphaRegexString = "^[a-zA-Z]+$" - alphaNumericRegexString = "^[a-zA-Z0-9]+$" - numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" - numberRegexString = "^[0-9]+$" - hexadecimalRegexString = "^[0-9a-fA-F]+$" - hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" - rgbRegexString = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$" - rgbaRegexString = "^rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" - hslRegexString = "^hsl\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*\\)$" - hslaRegexString = "^hsla\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" - emailRegexString = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" - base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" + alphaRegexString = "^[a-zA-Z]+$" + alphaNumericRegexString = "^[a-zA-Z0-9]+$" + numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" + numberRegexString = "^[0-9]+$" + hexadecimalRegexString = "^[0-9a-fA-F]+$" + hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" + rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" + hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" + iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" + iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" + uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" + uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + aSCIIRegexString = "^[\x00-\x7F]*$" + printableASCIIRegexString = "^[\x20-\x7E]*$" + multibyteRegexString = "[^\x00-\x7F]" + dataURIRegexString = "^data:.+\\/(.+);base64$" + latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$" + longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$" + sSNRegexString = `^\d{3}[- ]?\d{2}[- ]?\d{4}$` ) var ( - alphaRegex = regexp.MustCompile(alphaRegexString) - alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) - numericRegex = regexp.MustCompile(numericRegexString) - numberRegex = regexp.MustCompile(numberRegexString) - hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) - hexcolorRegex = regexp.MustCompile(hexcolorRegexString) - rgbRegex = regexp.MustCompile(rgbRegexString) - rgbaRegex = regexp.MustCompile(rgbaRegexString) - hslRegex = regexp.MustCompile(hslRegexString) - hslaRegex = regexp.MustCompile(hslaRegexString) - emailRegex = regexp.MustCompile(emailRegexString) - base64Regex = regexp.MustCompile(base64RegexString) + alphaRegex = regexp.MustCompile(alphaRegexString) + alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) + numericRegex = regexp.MustCompile(numericRegexString) + numberRegex = regexp.MustCompile(numberRegexString) + hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) + hexcolorRegex = regexp.MustCompile(hexcolorRegexString) + rgbRegex = regexp.MustCompile(rgbRegexString) + rgbaRegex = regexp.MustCompile(rgbaRegexString) + hslRegex = regexp.MustCompile(hslRegexString) + hslaRegex = regexp.MustCompile(hslaRegexString) + emailRegex = regexp.MustCompile(emailRegexString) + base64Regex = regexp.MustCompile(base64RegexString) + iSBN10Regex = regexp.MustCompile(iSBN10RegexString) + iSBN13Regex = regexp.MustCompile(iSBN13RegexString) + uUID3Regex = regexp.MustCompile(uUID3RegexString) + uUID4Regex = regexp.MustCompile(uUID4RegexString) + uUID5Regex = regexp.MustCompile(uUID5RegexString) + uUIDRegex = regexp.MustCompile(uUIDRegexString) + aSCIIRegex = regexp.MustCompile(aSCIIRegexString) + printableASCIIRegex = regexp.MustCompile(printableASCIIRegexString) + multibyteRegex = regexp.MustCompile(multibyteRegexString) + dataURIRegex = regexp.MustCompile(dataURIRegexString) + latitudeRegex = regexp.MustCompile(latitudeRegexString) + longitudeRegex = regexp.MustCompile(longitudeRegexString) + sSNRegex = regexp.MustCompile(sSNRegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator.go b/validator.go index 0ae7faf..ccd2a55 100644 --- a/validator.go +++ b/validator.go @@ -14,38 +14,230 @@ import ( "fmt" "reflect" "strings" + "sync" "time" "unicode" ) const ( - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - structErrMsg = "Struct:%s\n" + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" + arrayIndexFieldName = "%s[%d]" + mapIndexFieldName = "%s[%v]" ) +var structPool *sync.Pool + +// returns new *StructErrors to the pool +func newStructErrors() interface{} { + return &StructErrors{ + Errors: map[string]*FieldError{}, + StructErrors: map[string]*StructErrors{}, + } +} + +type cachedTags struct { + keyVals [][]string + isOrVal bool +} + +type cachedField struct { + index int + name string + tags []*cachedTags + tag string + kind reflect.Kind + typ reflect.Type + isTime bool + isSliceOrArray bool + isMap bool + isTimeSubtype bool + sliceSubtype reflect.Type + mapSubtype reflect.Type + sliceSubKind reflect.Kind + mapSubKind reflect.Kind + dive bool + diveTag string +} + +type cachedStruct struct { + children int + name string + kind reflect.Kind + fields []*cachedField +} + +type structsCacheMap struct { + lock sync.RWMutex + m map[reflect.Type]*cachedStruct +} + +func (s *structsCacheMap) Get(key reflect.Type) (*cachedStruct, bool) { + s.lock.RLock() + defer s.lock.RUnlock() + value, ok := s.m[key] + return value, ok +} + +func (s *structsCacheMap) Set(key reflect.Type, value *cachedStruct) { + s.lock.Lock() + defer s.lock.Unlock() + s.m[key] = value +} + +var structCache = &structsCacheMap{m: map[reflect.Type]*cachedStruct{}} + +type fieldsCacheMap struct { + lock sync.RWMutex + m map[string][]*cachedTags +} + +func (s *fieldsCacheMap) Get(key string) ([]*cachedTags, bool) { + s.lock.RLock() + defer s.lock.RUnlock() + value, ok := s.m[key] + return value, ok +} + +func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { + s.lock.Lock() + defer s.lock.Unlock() + s.m[key] = value +} + +var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} + // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { - Field string - Tag string - Kind reflect.Kind - Type reflect.Type - Param string - Value interface{} + Field string + Tag string + Kind reflect.Kind + Type reflect.Type + Param string + Value interface{} + IsPlaceholderErr bool + IsSliceOrArray bool + IsMap bool + SliceOrArrayErrs map[int]error // counld be FieldError, StructErrors + MapErrs map[interface{}]error // counld be FieldError, StructErrors } // This is intended for use in development + debugging and not intended to be a production error message. // it also allows FieldError to be used as an Error interface func (e *FieldError) Error() string { + + if e.IsPlaceholderErr { + + buff := bytes.NewBufferString("") + + if e.IsSliceOrArray { + + for j, err := range e.SliceOrArrayErrs { + buff.WriteString("\n") + buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, j, "\n"+err.Error())) + } + + } else if e.IsMap { + + for key, err := range e.MapErrs { + buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, "\n"+err.Error())) + } + } + + return strings.TrimSpace(buff.String()) + } + return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } +// Flatten flattens the FieldError hierarchical structure into a flat namespace style field name +// for those that want/need it. +// This is now needed because of the new dive functionality +func (e *FieldError) Flatten() map[string]*FieldError { + + errs := map[string]*FieldError{} + + if e.IsPlaceholderErr { + + if e.IsSliceOrArray { + for key, err := range e.SliceOrArrayErrs { + + fe, ok := err.(*FieldError) + + if ok { + + if flat := fe.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } + + } + } + } else { + + se := err.(*StructErrors) + + if flat := se.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v + } + } + } + } + } + + if e.IsMap { + for key, err := range e.MapErrs { + + fe, ok := err.(*FieldError) + + if ok { + + if flat := fe.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } + } + } + } else { + + se := err.(*StructErrors) + + if flat := se.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v + } + } + } + } + } + + return errs + } + + errs[e.Field] = e + + return errs +} + // StructErrors is hierarchical list of field and struct validation errors // for a non hierarchical representation please see the Flatten method for StructErrors type StructErrors struct { @@ -72,7 +264,7 @@ func (e *StructErrors) Error() string { buff.WriteString(err.Error()) } - return buff.String() + return strings.TrimSpace(buff.String()) } // Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name @@ -87,7 +279,17 @@ func (e *StructErrors) Flatten() map[string]*FieldError { for _, f := range e.Errors { - errs[f.Field] = f + if flat := f.Flatten(); flat != nil && len(flat) > 0 { + + for k, fe := range flat { + + if f.IsPlaceholderErr { + errs[f.Field+k] = fe + } else { + errs[k] = fe + } + } + } } for key, val := range e.StructErrors { @@ -124,6 +326,9 @@ type Validate struct { // New creates a new Validate instance for use. func New(tagName string, funcs map[string]Func) *Validate { + + structPool = &sync.Pool{New: newStructErrors} + return &Validate{ tagName: tagName, validationFuncs: funcs, @@ -132,12 +337,23 @@ func New(tagName string, funcs map[string]Func) *Validate { // SetTag sets tagName of the Validator to one of your choosing after creation // perhaps to dodge a tag name conflict in a specific section of code +// NOTE: this method is not thread-safe func (v *Validate) SetTag(tagName string) { v.tagName = tagName } +// SetMaxStructPoolSize sets the struct pools max size. this may be usefull for fine grained +// performance tuning towards your application, however, the default should be fine for +// nearly all cases. only increase if you have a deeply nested struct structure. +// NOTE: this method is not thread-safe +// NOTE: this is only here to keep compatibility with v5, in v6 the method will be removed +func (v *Validate) SetMaxStructPoolSize(max int) { + structPool = &sync.Pool{New: newStructErrors} +} + // AddFunction adds a validation Func to a Validate's map of validators denoted by the key // NOTE: if the key already exists, it will get replaced. +// NOTE: this method is not thread-safe func (v *Validate) AddFunction(key string, f Func) error { if len(key) == 0 { @@ -176,47 +392,84 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } structType := reflect.TypeOf(s) - structName := structType.Name() - validationErrors := &StructErrors{ - Struct: structName, - Errors: map[string]*FieldError{}, - StructErrors: map[string]*StructErrors{}, + var structName string + var numFields int + var cs *cachedStruct + var isCached bool + + cs, isCached = structCache.Get(structType) + + if isCached { + structName = cs.name + numFields = cs.children + } else { + structName = structType.Name() + numFields = structValue.NumField() + cs = &cachedStruct{name: structName, children: numFields} } - var numFields = structValue.NumField() + validationErrors := structPool.Get().(*StructErrors) + validationErrors.Struct = structName for i := 0; i < numFields; i++ { - valueField := structValue.Field(i) - typeField := structType.Field(i) + var valueField reflect.Value + var cField *cachedField + var typeField reflect.StructField - if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { - valueField = valueField.Elem() - } + if isCached { + cField = cs.fields[i] + valueField = structValue.Field(cField.index) + + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + valueField = valueField.Elem() + } + } else { + valueField = structValue.Field(i) - tag := typeField.Tag.Get(v.tagName) + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + valueField = valueField.Elem() + } + + typeField = structType.Field(i) + + cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: (valueField.Type() == reflect.TypeOf(time.Time{}) || valueField.Type() == reflect.TypeOf(&time.Time{}))} + + if cField.tag == noValidationTag { + cs.children-- + continue + } + + // if no validation and not a struct (which may containt fields for validation) + if cField.tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) { + cs.children-- + continue + } - if tag == noValidationTag { - continue + cField.name = typeField.Name + cField.kind = valueField.Kind() + cField.typ = valueField.Type() } - // if no validation and not a struct (which may containt fields for validation) - if tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) { - continue + // this can happen if the first cache value was nil + // but the second actually has a value + if cField.kind == reflect.Ptr { + cField.kind = valueField.Kind() } - switch valueField.Kind() { + switch cField.kind { case reflect.Struct, reflect.Interface: - if !unicode.IsUpper(rune(typeField.Name[0])) { + if !unicode.IsUpper(rune(cField.name[0])) { + cs.children-- continue } - if valueField.Type() == reflect.TypeOf(time.Time{}) { + if cField.isTime { - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), typeField.Name, tag); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference fieldError = nil @@ -224,28 +477,116 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } else { - if strings.Contains(tag, structOnlyTag) { + if strings.Contains(cField.tag, structOnlyTag) { + cs.children-- continue } + if (valueField.Kind() == reflect.Ptr || cField.kind == reflect.Interface) && valueField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + goto CACHEFIELD + } + + tags := strings.Split(cField.tag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } + + validationErrors.Errors[cField.name] = &FieldError{ + Field: cField.name, + Tag: vals[0], + Param: param, + Value: valueField.Interface(), + Kind: valueField.Kind(), + Type: valueField.Type(), + } + + goto CACHEFIELD + } + } + + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if cField.kind == reflect.Interface { + + valueField = valueField.Elem() + + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + valueField = valueField.Elem() + } + + if valueField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, nil); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + + goto CACHEFIELD + } + + VALIDATESTRUCT: if structErrors := v.structRecursive(top, valueField.Interface(), valueField.Interface()); structErrors != nil { - validationErrors.StructErrors[typeField.Name] = structErrors + validationErrors.StructErrors[cField.name] = structErrors // free up memory map no longer needed structErrors = nil } } - default: + case reflect.Slice, reflect.Array: + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) + cField.sliceSubKind = cField.sliceSubtype.Kind() - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), typeField.Name, tag); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + + case reflect.Map: + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) + cField.mapSubKind = cField.mapSubtype.Kind() + + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + + default: + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference fieldError = nil } } + + CACHEFIELD: + if !isCached { + cs.fields = append(cs.fields, cField) + } } + structCache.Set(structType, cs) + if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 { + structPool.Put(validationErrors) return nil } @@ -254,20 +595,22 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter // Field allows validation of a single field, still using tag style validation to check multiple errors func (v *Validate) Field(f interface{}, tag string) *FieldError { - return v.FieldWithValue(nil, f, tag) } // FieldWithValue allows validation of a single field, possibly even against another fields value, still using tag style validation to check multiple errors func (v *Validate) FieldWithValue(val interface{}, f interface{}, tag string) *FieldError { - - return v.fieldWithNameAndValue(nil, val, f, "", tag) + return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil) } -func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f interface{}, name string, tag string) *FieldError { +func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f interface{}, tag string, name string, isSingleField bool, cacheField *cachedField) *FieldError { + + var cField *cachedField + var isCached bool + var valueField reflect.Value // This is a double check if coming from validate.Struct but need to be here in case function is called directly - if tag == noValidationTag { + if tag == noValidationTag || tag == "" { return nil } @@ -275,87 +618,357 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } - valueField := reflect.ValueOf(f) - fieldKind := valueField.Kind() + valueField = reflect.ValueOf(f) - if fieldKind == reflect.Ptr && !valueField.IsNil() { - return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), name, tag) - } + if cacheField == nil { - fieldType := valueField.Type() + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + valueField = valueField.Elem() + f = valueField.Interface() + } - switch fieldKind { + cField = &cachedField{name: name, kind: valueField.Kind(), tag: tag, typ: valueField.Type()} + + switch cField.kind { + case reflect.Slice, reflect.Array: + isSingleField = false // cached tags mean nothing because it will be split up while diving + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) + cField.sliceSubKind = cField.sliceSubtype.Kind() + case reflect.Map: + isSingleField = false // cached tags mean nothing because it will be split up while diving + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) + cField.mapSubKind = cField.mapSubtype.Kind() + } + } else { + cField = cacheField + } + + switch cField.kind { case reflect.Struct, reflect.Interface, reflect.Invalid: - if fieldType != reflect.TypeOf(time.Time{}) { - panic("Invalid field passed to ValidateFieldWithTag") + if cField.typ != reflect.TypeOf(time.Time{}) { + panic("Invalid field passed to fieldWithNameAndValue") } } - var valErr *FieldError - var err error - valTags := strings.Split(tag, tagSeparator) + if len(cField.tags) == 0 { + + if isSingleField { + cField.tags, isCached = fieldsCache.Get(tag) + } + + if !isCached { + + for _, t := range strings.Split(tag, tagSeparator) { + + if t == diveTag { + + cField.dive = true + cField.diveTag = strings.TrimLeft(strings.SplitN(tag, diveTag, 2)[1], ",") + break + } + + orVals := strings.Split(t, orSeparator) + cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} + cField.tags = append(cField.tags, cTag) + + for i, val := range orVals { + vals := strings.SplitN(val, tagKeySeparator, 2) + + key := strings.TrimSpace(vals[0]) + + if len(key) == 0 { + panic(fmt.Sprintf("Invalid validation tag on field %s", name)) + } + + param := "" + if len(vals) > 1 { + param = strings.Replace(vals[1], utf8HexComma, ",", -1) + } + + cTag.keyVals[i] = []string{key, param} + } + } - for _, valTag := range valTags { + if isSingleField { + fieldsCache.Set(cField.tag, cField.tags) + } + } + } + + var fieldErr *FieldError + var err error - orVals := strings.Split(valTag, orSeparator) + for _, cTag := range cField.tags { - if len(orVals) > 1 { + if cTag.isOrVal { errTag := "" - for _, val := range orVals { + for _, val := range cTag.keyVals { - valErr, err = v.fieldWithNameAndSingleTag(val, current, f, name, val) + fieldErr, err = v.fieldWithNameAndSingleTag(val, current, f, val[0], val[1], name) if err == nil { return nil } - errTag += orSeparator + valErr.Tag - + errTag += orSeparator + fieldErr.Tag } errTag = strings.TrimLeft(errTag, orSeparator) - valErr.Tag = errTag - valErr.Kind = fieldKind + fieldErr.Tag = errTag + fieldErr.Kind = cField.kind + fieldErr.Type = cField.typ - return valErr + return fieldErr } - if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, name, valTag); err != nil { + if fieldErr, err = v.fieldWithNameAndSingleTag(val, current, f, cTag.keyVals[0][0], cTag.keyVals[0][1], name); err != nil { + + fieldErr.Kind = cField.kind + fieldErr.Type = cField.typ - valErr.Kind = valueField.Kind() - valErr.Type = fieldType + return fieldErr + } + } - return valErr + if cField.dive { + + if cField.isSliceOrArray { + + if errs := v.traverseSliceOrArray(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + IsPlaceholderErr: true, + IsSliceOrArray: true, + SliceOrArrayErrs: errs, + } + } + + } else if cField.isMap { + if errs := v.traverseMap(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + IsPlaceholderErr: true, + IsMap: true, + MapErrs: errs, + } + } + } else { + // throw error, if not a slice or map then should not have gotten here + panic("dive error! can't dive on a non slice or map") } } return nil } -func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, name string, valTag string) (*FieldError, error) { +func (v *Validate) traverseMap(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[interface{}]error { - vals := strings.Split(valTag, tagKeySeparator) - key := strings.TrimSpace(vals[0]) + errs := map[interface{}]error{} - if len(key) == 0 { - panic(fmt.Sprintf("Invalid validation tag on field %s", name)) + for _, key := range valueField.MapKeys() { + + idxField := valueField.MapIndex(key) + + if cField.mapSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.mapSubKind = idxField.Kind() + } + + switch cField.mapSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + + continue + } + + if (idxField.Kind() == reflect.Ptr || idxField.Kind() == reflect.Interface) && idxField.IsNil() { + + if strings.Contains(cField.diveTag, omitempty) { + continue + } + + tags := strings.Split(cField.diveTag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } + + errs[key.Interface()] = &FieldError{ + Field: fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), + Tag: vals[0], + Param: param, + Value: idxField.Interface(), + Kind: idxField.Kind(), + Type: cField.mapSubtype, + } + } + + continue + } + + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if idxField.Kind() == reflect.Interface { + + idxField = idxField.Elem() + + if idxField.Kind() == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + } + + if idxField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + + continue + } + + VALIDATESTRUCT: + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs[key.Interface()] = structErrors + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + } } - valErr := &FieldError{ - Field: name, - Tag: key, - Value: f, - Param: "", + return errs +} + +func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { + + errs := map[int]error{} + + for i := 0; i < valueField.Len(); i++ { + + idxField := valueField.Index(i) + + if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.sliceSubKind = idxField.Kind() + } + + switch cField.sliceSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { + errs[i] = fieldError + } + + continue + } + + if (idxField.Kind() == reflect.Ptr || idxField.Kind() == reflect.Interface) && idxField.IsNil() { + + if strings.Contains(cField.diveTag, omitempty) { + continue + } + + tags := strings.Split(cField.diveTag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } + + errs[i] = &FieldError{ + Field: fmt.Sprintf(arrayIndexFieldName, cField.name, i), + Tag: vals[0], + Param: param, + Value: idxField.Interface(), + Kind: idxField.Kind(), + Type: cField.sliceSubtype, + } + } + + continue + } + + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if idxField.Kind() == reflect.Interface { + + idxField = idxField.Elem() + + if idxField.Kind() == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + } + + if idxField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { + errs[i] = fieldError + } + + continue + } + + VALIDATESTRUCT: + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs[i] = structErrors + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { + errs[i] = fieldError + } + } } + return errs +} + +func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { + // OK to continue because we checked it's existance before getting into this loop if key == omitempty { - return valErr, nil + return nil, nil } valFunc, ok := v.validationFuncs[key] @@ -363,15 +976,14 @@ func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{ panic(fmt.Sprintf("Undefined validation function on field %s", name)) } - param := "" - if len(vals) > 1 { - param = strings.TrimSpace(vals[1]) - } - - if err := valFunc(val, current, f, param); !err { - valErr.Param = param - return valErr, errors.New(key) + if err := valFunc(val, current, f, param); err { + return nil, nil } - return valErr, nil + return &FieldError{ + Field: name, + Tag: key, + Value: f, + Param: param, + }, errors.New(key) } diff --git a/validator_test.go b/validator_test.go index ce7c546..8bfff9b 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14,6 +14,11 @@ import ( // - Run "gocov test | gocov report" to report on test converage by file // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called // +// or +// +// -- may be a good idea to change to output path to somewherelike /tmp +// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html +// // // go test -cpuprofile cpu.out // ./validator.test -test.bench=. -test.cpuprofile=cpu.prof @@ -103,127 +108,1568 @@ type TestSlice struct { OmitEmpty []int `validate:"omitempty,min=1,max=10"` } -var validate = New("validate", BakedInValidators) +var validate = New("validate", BakedInValidators) + +func IsEqual(t *testing.T, val1, val2 interface{}) bool { + v1 := reflect.ValueOf(val1) + v2 := reflect.ValueOf(val2) + + if v1.Kind() == reflect.Ptr { + v1 = v1.Elem() + } + + if v2.Kind() == reflect.Ptr { + v2 = v2.Elem() + } + + if !v1.IsValid() && !v2.IsValid() { + return true + } + + v1Underlying := reflect.Zero(reflect.TypeOf(v1)).Interface() + v2Underlying := reflect.Zero(reflect.TypeOf(v2)).Interface() + + if v1 == v1Underlying { + if v2 == v2Underlying { + goto CASE4 + } else { + goto CASE3 + } + } else { + if v2 == v2Underlying { + goto CASE2 + } else { + goto CASE1 + } + } + +CASE1: + return reflect.DeepEqual(v1.Interface(), v2.Interface()) + +CASE2: + return reflect.DeepEqual(v1.Interface(), v2) +CASE3: + return reflect.DeepEqual(v1, v2.Interface()) +CASE4: + return reflect.DeepEqual(v1, v2) +} + +func Equal(t *testing.T, val1, val2 interface{}) { + EqualSkip(t, 2, val1, val2) +} + +func EqualSkip(t *testing.T, skip int, val1, val2 interface{}) { + + if !IsEqual(t, val1, val2) { + + _, file, line, _ := runtime.Caller(skip) + fmt.Printf("%s:%d %v does not equal %v\n", path.Base(file), line, val1, val2) + t.FailNow() + } +} + +func NotEqual(t *testing.T, val1, val2 interface{}) { + NotEqualSkip(t, 2, val1, val2) +} + +func NotEqualSkip(t *testing.T, skip int, val1, val2 interface{}) { + + if IsEqual(t, val1, val2) { + _, file, line, _ := runtime.Caller(skip) + fmt.Printf("%s:%d %v should not be equal %v\n", path.Base(file), line, val1, val2) + t.FailNow() + } +} + +func PanicMatches(t *testing.T, fn func(), matches string) { + PanicMatchesSkip(t, 2, fn, matches) +} + +func PanicMatchesSkip(t *testing.T, skip int, fn func(), matches string) { + + _, file, line, _ := runtime.Caller(skip) + + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf("%s", r) + + if err != matches { + fmt.Printf("%s:%d Panic... expected [%s] received [%s]", path.Base(file), line, matches, err) + t.FailNow() + } + } + }() + + fn() +} + +func AssertStruct(t *testing.T, s *StructErrors, structFieldName string, expectedStructName string) *StructErrors { + + val, ok := s.StructErrors[structFieldName] + EqualSkip(t, 2, ok, true) + NotEqualSkip(t, 2, val, nil) + EqualSkip(t, 2, val.Struct, expectedStructName) + + return val +} + +func AssertFieldError(t *testing.T, s *StructErrors, field string, expectedTag string) { + + val, ok := s.Errors[field] + EqualSkip(t, 2, ok, true) + NotEqualSkip(t, 2, val, nil) + EqualSkip(t, 2, val.Field, field) + EqualSkip(t, 2, val.Tag, expectedTag) +} + +func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, expectedTag string) { + + val, ok := s[field] + EqualSkip(t, 2, ok, true) + NotEqualSkip(t, 2, val, nil) + EqualSkip(t, 2, val.Field, field) + EqualSkip(t, 2, val.Tag, expectedTag) +} + +func TestBadKeyValidation(t *testing.T) { + type Test struct { + Name string `validate:"required, "` + } + + tst := &Test{ + Name: "test", + } + + PanicMatches(t, func() { validate.Struct(tst) }, "Invalid validation tag on field Name") +} + +func TestFlattenValidation(t *testing.T) { + + type Inner struct { + Name string `validate:"required"` + } + + type TestMultiDimensionalStructsPtr struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,required"` + } + + var errStructPtrArray [][]*Inner + + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{"ok"}}) + + tmsp := &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs := validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldErr := errs.Errors["Errs"] + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, fieldErr.Field, "Errs") + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerSlice1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSlice1.IsPlaceholderErr, true) + Equal(t, innerSlice1.Field, "Errs[0]") + + flatFieldErr, ok := fieldErr.Flatten()["[0][1].Inner.Name"] + Equal(t, ok, true) + Equal(t, flatFieldErr.Field, "Name") + Equal(t, flatFieldErr.Tag, "required") + + structErrFlatten, ok := errs.Flatten()["Errs[0][1].Inner.Name"] + Equal(t, ok, true) + Equal(t, structErrFlatten.Field, "Name") + Equal(t, structErrFlatten.Tag, "required") + + errStructPtrArray = [][]*Inner{} + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, nil, &Inner{"ok"}}) + + tmsp = &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs = validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldErr = errs.Errors["Errs"] + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, fieldErr.Field, "Errs") + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerSlice1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSlice1.IsPlaceholderErr, true) + Equal(t, innerSlice1.Field, "Errs[0]") + + flatFieldErr, ok = fieldErr.Flatten()["[0][1]"] + Equal(t, ok, true) + Equal(t, flatFieldErr.Field, "Errs[0][1]") + Equal(t, flatFieldErr.Tag, "required") + + type TestMapStructPtr struct { + Errs map[int]*Inner `validate:"gt=0,dive,required"` + } + + mip := map[int]*Inner{0: &Inner{"ok"}, 3: &Inner{""}, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerStructError, ok := fieldError.MapErrs[3].(*StructErrors) + Equal(t, ok, true) + Equal(t, innerStructError.Struct, "Inner") + Equal(t, len(innerStructError.Errors), 1) + + innerInnerFieldError, ok := innerStructError.Errors["Name"] + Equal(t, ok, true) + Equal(t, innerInnerFieldError.IsPlaceholderErr, false) + Equal(t, innerInnerFieldError.IsSliceOrArray, false) + Equal(t, innerInnerFieldError.Field, "Name") + Equal(t, innerInnerFieldError.Tag, "required") + + flatErrs, ok := errs.Flatten()["Errs[3].Inner.Name"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Name") + Equal(t, flatErrs.Tag, "required") + + mip2 := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp2 := &TestMapStructPtr{ + Errs: mip2, + } + + errs = validate.Struct(msp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsSliceOrArray, false) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "required") + + flatErrs, ok = errs.Flatten()["Errs[3]"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Errs[3]") + Equal(t, flatErrs.Tag, "required") + + type TestMapInnerArrayStruct struct { + Errs map[int][]string `validate:"gt=0,dive,dive,required"` + } + + mias := map[int][]string{0: []string{"ok"}, 3: []string{"ok", ""}, 4: []string{"ok"}} + + mia := &TestMapInnerArrayStruct{ + Errs: mias, + } + + errs = validate.Struct(mia) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + flatErrs, ok = errs.Flatten()["Errs[3][1]"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Errs[3][1]") + Equal(t, flatErrs.Tag, "required") +} + +func TestInterfaceErrValidation(t *testing.T) { + + var v1 interface{} + var v2 interface{} + + v2 = 1 + v1 = v2 + + err := validate.Field(v1, "len=1") + Equal(t, err, nil) + err = validate.Field(v2, "len=1") + Equal(t, err, nil) + + type ExternalCMD struct { + Userid string `json:"userid"` + Action uint32 `json:"action"` + Data interface{} `json:"data,omitempty" validate:"required"` + } + + s := &ExternalCMD{ + Userid: "123456", + Action: 10000, + // Data: 1, + } + + errs := validate.Struct(s) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "required") + + type ExternalCMD2 struct { + Userid string `json:"userid"` + Action uint32 `json:"action"` + Data interface{} `json:"data,omitempty" validate:"len=1"` + } + + s2 := &ExternalCMD2{ + Userid: "123456", + Action: 10000, + // Data: 1, + } + + errs = validate.Struct(s2) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "len") + Equal(t, errs.Errors["Data"].Param, "1") + + s3 := &ExternalCMD2{ + Userid: "123456", + Action: 10000, + Data: 2, + } + + errs = validate.Struct(s3) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "len") + Equal(t, errs.Errors["Data"].Param, "1") + + type Inner struct { + Name string `validate:"required"` + } + + inner := &Inner{ + Name: "", + } + + s4 := &ExternalCMD{ + Userid: "123456", + Action: 10000, + Data: inner, + } + + errs = validate.Struct(s4) + NotEqual(t, errs, nil) + Equal(t, errs.StructErrors["Data"].Struct, "Inner") + Equal(t, errs.StructErrors["Data"].Errors["Name"].Field, "Name") + Equal(t, errs.StructErrors["Data"].Errors["Name"].Tag, "required") + + type TestMapStructPtr struct { + Errs map[int]interface{} `validate:"gt=0,dive,len=2"` + } + + mip := map[int]interface{}{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsMap, false) + Equal(t, len(innerFieldError.MapErrs), 0) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "len") + + type TestMultiDimensionalStructs struct { + Errs [][]interface{} `validate:"gt=0,dive,dive,len=2"` + } + + var errStructArray [][]interface{} + + errStructArray = append(errStructArray, []interface{}{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructArray = append(errStructArray, []interface{}{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + + tms := &TestMultiDimensionalStructs{ + Errs: errStructArray, + } + + errs = validate.Struct(tms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok := sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 := innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr2 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,len=2"` + } + + var errStructPtr2Array [][]*Inner + + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp2 := &TestMultiDimensionalStructsPtr2{ + Errs: errStructPtr2Array, + } + + errs = validate.Struct(tmsp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerSliceStructError2, ok := sliceError1.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceStructError2.IsPlaceholderErr, false) + Equal(t, innerSliceStructError2.IsSliceOrArray, false) + Equal(t, len(innerSliceStructError2.SliceOrArrayErrs), 0) + Equal(t, innerSliceStructError2.Field, "Errs[2][2]") + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + m := map[int]interface{}{0: "ok", 3: "", 4: "ok"} + + err = validate.Field(m, "len=3,dive,len=2") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsMap, true) + Equal(t, len(err.MapErrs), 1) + + err = validate.Field(m, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsMap, false) + Equal(t, len(err.MapErrs), 0) + + arr := []interface{}{"ok", "", "ok"} + + err = validate.Field(arr, "len=3,dive,len=2") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsSliceOrArray, true) + Equal(t, len(err.SliceOrArrayErrs), 1) + + err = validate.Field(arr, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsSliceOrArray, false) + Equal(t, len(err.SliceOrArrayErrs), 0) + + type MyStruct struct { + A, B string + C interface{} + } + + var a MyStruct + + a.A = "value" + a.C = "nu" + + errs = validate.Struct(a) + Equal(t, errs, nil) +} + +func TestMapDiveValidation(t *testing.T) { + + n := map[int]interface{}{0: nil} + err := validate.Field(n, "omitempty,required") + + m := map[int]string{0: "ok", 3: "", 4: "ok"} + + err = validate.Field(m, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsMap, true) + Equal(t, len(err.MapErrs), 1) + + err = validate.Field(m, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsMap, false) + Equal(t, len(err.MapErrs), 0) + + type Inner struct { + Name string `validate:"required"` + } + + type TestMapStruct struct { + Errs map[int]Inner `validate:"gt=0,dive"` + } + + mi := map[int]Inner{0: Inner{"ok"}, 3: Inner{""}, 4: Inner{"ok"}} + + ms := &TestMapStruct{ + Errs: mi, + } + + errs := validate.Struct(ms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + structErr, ok := fieldError.MapErrs[3].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(structErr.Errors), 1) + + innerErr := structErr.Errors["Name"] + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Name") + Equal(t, innerErr.Tag, "required") + + type TestMapTimeStruct struct { + Errs map[int]*time.Time `validate:"gt=0,dive,required"` + } + + t1 := time.Now().UTC() + + mta := map[int]*time.Time{0: &t1, 3: nil, 4: nil} + + mt := &TestMapTimeStruct{ + Errs: mta, + } + + errs = validate.Struct(mt) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 2) + + innerErr, ok = fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Errs[3]") + Equal(t, innerErr.Tag, "required") + + type TestMapStructPtr struct { + Errs map[int]*Inner `validate:"gt=0,dive,required"` + } + + mip := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsMap, false) + Equal(t, len(innerFieldError.MapErrs), 0) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "required") + + type TestMapStructPtr2 struct { + Errs map[int]*Inner `validate:"gt=0,dive,omitempty,required"` + } + + mip2 := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp2 := &TestMapStructPtr2{ + Errs: mip2, + } + + errs = validate.Struct(msp2) + Equal(t, errs, nil) +} + +func TestArrayDiveValidation(t *testing.T) { + + arr := []string{"ok", "", "ok"} + + err := validate.Field(arr, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsSliceOrArray, true) + Equal(t, len(err.SliceOrArrayErrs), 1) + + // flat := err.Flatten() + // fe, ok := flat["[1]"] + // Equal(t, ok, true) + // Equal(t, fe.Tag, "required") + + err = validate.Field(arr, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsSliceOrArray, false) + Equal(t, len(err.SliceOrArrayErrs), 0) + + type BadDive struct { + Name string `validate:"dive"` + } + + bd := &BadDive{ + Name: "TEST", + } + + PanicMatches(t, func() { validate.Struct(bd) }, "dive error! can't dive on a non slice or map") + + type Test struct { + Errs []string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: []string{"ok", "", "ok"}, + } + + errs := validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + // flat = errs.Flatten() + // me, ok := flat["Errs[1]"] + // Equal(t, ok, true) + // Equal(t, me.Field, "Errs[1]") + // Equal(t, me.Tag, "required") + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[1]") + + test = &Test{ + Errs: []string{"ok", "ok", ""}, + } + + errs = validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[2]") + + type TestMultiDimensional struct { + Errs [][]string `validate:"gt=0,dive,dive,required"` + } + + var errArray [][]string + + errArray = append(errArray, []string{"ok", "", ""}) + errArray = append(errArray, []string{"ok", "", ""}) + + tm := &TestMultiDimensional{ + Errs: errArray, + } + + errs = validate.Struct(tm) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + Equal(t, sliceError1.Field, "Errs[0]") + + innerSliceError1, ok := sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.Tag, required) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[0][1]") + + type Inner struct { + Name string `validate:"required"` + } + + type TestMultiDimensionalStructs struct { + Errs [][]Inner `validate:"gt=0,dive,dive"` + } + + var errStructArray [][]Inner + + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + + tms := &TestMultiDimensionalStructs{ + Errs: errStructArray, + } + + errs = validate.Struct(tms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok := sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 := innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr struct { + Errs [][]*Inner `validate:"gt=0,dive,dive"` + } + + var errStructPtrArray [][]*Inner + + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp := &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs = validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + // flat := errs.Flatten() + // // fmt.Println(errs) + // fmt.Println(flat) + // expect Errs[0][1].Inner.Name + // me, ok := flat["Errs[1]"] + // Equal(t, ok, true) + // Equal(t, me.Field, "Errs[1]") + // Equal(t, me.Tag, "required") + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + // flat := fieldErr.Flatten() + // fmt.Println(errs) + // fmt.Println(flat) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr2 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,required"` + } + + var errStructPtr2Array [][]*Inner + + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp2 := &TestMultiDimensionalStructsPtr2{ + Errs: errStructPtr2Array, + } + + errs = validate.Struct(tmsp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerSliceStructError2, ok := sliceError1.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceStructError2.IsPlaceholderErr, false) + Equal(t, innerSliceStructError2.IsSliceOrArray, false) + Equal(t, len(innerSliceStructError2.SliceOrArrayErrs), 0) + Equal(t, innerSliceStructError2.Field, "Errs[2][2]") + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr3 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,omitempty"` + } + + var errStructPtr3Array [][]*Inner + + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp3 := &TestMultiDimensionalStructsPtr3{ + Errs: errStructPtr3Array, + } + + errs = validate.Struct(tmsp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalTimeTime struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimePtr3Array [][]*time.Time + + t1 := time.Now().UTC() + t2 := time.Now().UTC() + t3 := time.Now().UTC().Add(time.Hour * 24) + + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, &t3}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, nil}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, nil, nil}) + + tmtp3 := &TestMultiDimensionalTimeTime{ + Errs: errTimePtr3Array, + } + + errs = validate.Struct(tmtp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2][1]") + Equal(t, innerSliceError1.Tag, required) + + type TestMultiDimensionalTimeTime2 struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimeArray [][]*time.Time + + t1 = time.Now().UTC() + t2 = time.Now().UTC() + t3 = time.Now().UTC().Add(time.Hour * 24) + + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, &t3}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, nil}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, nil, nil}) + + tmtp := &TestMultiDimensionalTimeTime2{ + Errs: errTimeArray, + } + + errs = validate.Struct(tmtp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2][1]") + Equal(t, innerSliceError1.Tag, required) +} + +func TestNilStructPointerValidation(t *testing.T) { + type Inner struct { + Data string + } + + type Outer struct { + Inner *Inner `validate:"omitempty"` + } + + inner := &Inner{ + Data: "test", + } + + outer := &Outer{ + Inner: inner, + } + + errs := validate.Struct(outer) + Equal(t, errs, nil) + + outer = &Outer{ + Inner: nil, + } + + errs = validate.Struct(outer) + Equal(t, errs, nil) + + type Inner2 struct { + Data string + } + + type Outer2 struct { + Inner2 *Inner2 `validate:"required"` + } + + inner2 := &Inner2{ + Data: "test", + } + + outer2 := &Outer2{ + Inner2: inner2, + } + + errs = validate.Struct(outer2) + Equal(t, errs, nil) + + outer2 = &Outer2{ + Inner2: nil, + } + + errs = validate.Struct(outer2) + NotEqual(t, errs, nil) + + type Inner3 struct { + Data string + } + + type Outer3 struct { + Inner3 *Inner3 + } + + inner3 := &Inner3{ + Data: "test", + } + + outer3 := &Outer3{ + Inner3: inner3, + } + + errs = validate.Struct(outer3) + Equal(t, errs, nil) + + type Inner4 struct { + Data string + } + + type Outer4 struct { + Inner4 *Inner4 `validate:"-"` + } + + inner4 := &Inner4{ + Data: "test", + } + + outer4 := &Outer4{ + Inner4: inner4, + } + + errs = validate.Struct(outer4) + Equal(t, errs, nil) +} + +func TestSSNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"00-90-8787", false}, + {"66690-76", false}, + {"191 60 2869", true}, + {"191-60-2869", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "ssn") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ssn") { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } + } +} + +func TestLongitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-180.000", true}, + {"180.1", false}, + {"+73.234", true}, + {"+382.3811", false}, + {"23.11111111", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "longitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "longitude") { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } + } +} + +func TestLatitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-90.000", true}, + {"+90", true}, + {"47.1231231", true}, + {"+99.9", false}, + {"108", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "latitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "latitude") { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } + } +} + +func TestDataURIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true}, + {"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true}, + {"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + {"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" + + "UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" + + "rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" + + "FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" + + "QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" + + "Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true}, + {"data:image/png;base64,12345", false}, + {"", false}, + {"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "datauri") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "datauri") { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } + } +} + +func TestMultibyteValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"abc", false}, + {"123", false}, + {"<>@;.-=", false}, + {"ひらがな・カタカナ、.漢字", true}, + {"あいうえお foobar", true}, + {"test@example.com", true}, + {"test@example.com", true}, + {"1234abcDExyz", true}, + {"カタカナ", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "multibyte") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "multibyte") { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } + } +} + +func TestPrintableASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"newline\n", false}, + {"\x19test\x7F", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "printascii") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "printascii") { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } + } +} + +func TestASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"", true}, + } -func IsEqual(t *testing.T, val1, val2 interface{}) bool { - v1 := reflect.ValueOf(val1) - v2 := reflect.ValueOf(val2) + for i, test := range tests { - if v1.Kind() == reflect.Ptr { - v1 = v1.Elem() - } + err := validate.Field(test.param, "ascii") - if v2.Kind() == reflect.Ptr { - v2 = v2.Elem() + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ascii") { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } } +} - if !v1.IsValid() && !v2.IsValid() { - return true +func TestUUID5Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"9c858901-8a57-4791-81fe-4c455b099bc9", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"987fbc97-4bed-5078-af07-9141ba07c9f3", true}, + {"987fbc97-4bed-5078-9f07-9141ba07c9f3", true}, } - v1Underlying := reflect.Zero(reflect.TypeOf(v1)).Interface() - v2Underlying := reflect.Zero(reflect.TypeOf(v2)).Interface() + for i, test := range tests { - if v1 == v1Underlying { - if v2 == v2Underlying { - goto CASE4 - } else { - goto CASE3 - } - } else { - if v2 == v2Underlying { - goto CASE2 + err := validate.Field(test.param, "uuid5") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } } else { - goto CASE1 + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid5") { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } } } - -CASE1: - return reflect.DeepEqual(v1.Interface(), v2.Interface()) - -CASE2: - return reflect.DeepEqual(v1.Interface(), v2) -CASE3: - return reflect.DeepEqual(v1, v2.Interface()) -CASE4: - return reflect.DeepEqual(v1, v2) } -func Equal(t *testing.T, val1, val2 interface{}) { - EqualSkip(t, 2, val1, val2) -} +func TestUUID4Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-5078-af07-9141ba07c9f3", false}, + {"934859", false}, + {"57b73598-8764-4ad0-a76a-679bb6640eb1", true}, + {"625e63f3-58f5-40b7-83a1-a72ad31acffb", true}, + } -func EqualSkip(t *testing.T, skip int, val1, val2 interface{}) { + for i, test := range tests { - if !IsEqual(t, val1, val2) { + err := validate.Field(test.param, "uuid4") - _, file, line, _ := runtime.Caller(skip) - fmt.Printf("%s:%d %v does not equal %v\n", path.Base(file), line, val1, val2) - t.FailNow() + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid4") { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } } } -func NotEqual(t *testing.T, val1, val2 interface{}) { - NotEqualSkip(t, 2, val1, val2) -} +func TestUUID3Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"412452646", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-4078-8f07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } -func NotEqualSkip(t *testing.T, skip int, val1, val2 interface{}) { + for i, test := range tests { - if IsEqual(t, val1, val2) { - _, file, line, _ := runtime.Caller(skip) - fmt.Printf("%s:%d %v should not be equal %v\n", path.Base(file), line, val1, val2) - t.FailNow() + err := validate.Field(test.param, "uuid3") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid3") { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } } } -func PanicMatches(t *testing.T, fn func(), matches string) { - PanicMatchesSkip(t, 2, fn, matches) +func TestUUIDValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3xxx", false}, + {"a987fbc94bed3078cf079141ba07c9f3", false}, + {"934859", false}, + {"987fbc9-4bed-3078-cf07a-9141ba07c9f3", false}, + {"aaaaaaaa-1111-1111-aaag-111111111111", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid") { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } + } } -func PanicMatchesSkip(t *testing.T, skip int, fn func(), matches string) { +func TestISBNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } - _, file, line, _ := runtime.Caller(skip) + for i, test := range tests { - defer func() { - if r := recover(); r != nil { - err := fmt.Sprintf("%s", r) + err := validate.Field(test.param, "isbn") - if err != matches { - fmt.Printf("%s:%d Panic... expected [%s] received [%s]", path.Base(file), line, matches, err) - t.FailNow() + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn") { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) } } - }() - - fn() + } } -func AssertStruct(t *testing.T, s *StructErrors, structFieldName string, expectedStructName string) *StructErrors { +func TestISBN13Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3-8362-2119-5", false}, + {"01234567890ab", false}, + {"978 3 8362 2119 0", false}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } - val, ok := s.StructErrors[structFieldName] - EqualSkip(t, 2, ok, true) - NotEqualSkip(t, 2, val, nil) - EqualSkip(t, 2, val.Struct, expectedStructName) + for i, test := range tests { - return val + err := validate.Field(test.param, "isbn13") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn13") { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } + } } -func AssertFieldError(t *testing.T, s *StructErrors, field string, expectedTag string) { +func TestISBN10Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3423214121", false}, + {"978-3836221191", false}, + {"3-423-21412-1", false}, + {"3 423 21412 1", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + } - val, ok := s.Errors[field] - EqualSkip(t, 2, ok, true) - NotEqualSkip(t, 2, val, nil) - EqualSkip(t, 2, val.Field, field) - EqualSkip(t, 2, val.Tag, expectedTag) -} + for i, test := range tests { -func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, expectedTag string) { + err := validate.Field(test.param, "isbn10") - val, ok := s[field] - EqualSkip(t, 2, ok, true) - NotEqualSkip(t, 2, val, nil) - EqualSkip(t, 2, val.Field, field) - EqualSkip(t, 2, val.Tag, expectedTag) + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn10") { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } + } } func TestExcludesRuneValidation(t *testing.T) { @@ -276,6 +1722,21 @@ func TestExcludesAllValidation(t *testing.T) { t.Fatalf("Index: %d failed Error: %s", i, errs) } } + + username := "joeybloggs " + + err := validate.Field(username, "excludesall=@ ") + NotEqual(t, err, nil) + + excluded := "," + + err = validate.Field(excluded, "excludesall=!@#$%^&*()_+.0x2C?") + NotEqual(t, err, nil) + + excluded = "=" + + err = validate.Field(excluded, "excludesall=!@#$%^&*()_+.0x2C=?") + NotEqual(t, err, nil) } func TestExcludesValidation(t *testing.T) { @@ -648,7 +2109,7 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: nil, } - errs := validate.Struct(outer).Flatten() + errs := validate.Struct(outer) NotEqual(t, errs, nil) inner := &Inner{ @@ -659,9 +2120,8 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: inner, } - errs = validate.Struct(outer).Flatten() - NotEqual(t, errs, nil) - Equal(t, len(errs), 0) + errs = validate.Struct(outer) + Equal(t, errs, nil) } func TestGtField(t *testing.T) { @@ -1721,10 +3181,19 @@ func TestRgba(t *testing.T) { err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55%,100%,0.12)" + err = validate.Field(s, "rgba") + Equal(t, err, nil) + s = "rgba( 0, 31, 255, 0.5)" err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55,100%,0.12)" + err = validate.Field(s, "rgba") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgba") + s = "rgb(0, 31, 255)" err = validate.Field(s, "rgba") NotEqual(t, err, nil) @@ -1754,6 +3223,15 @@ func TestRgb(t *testing.T) { err = validate.Field(s, "rgb") Equal(t, err, nil) + s = "rgb(10%, 50%, 100%)" + err = validate.Field(s, "rgb") + Equal(t, err, nil) + + s = "rgb(10%, 50%, 55)" + err = validate.Field(s, "rgb") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgb") + s = "rgb(1,349,275)" err = validate.Field(s, "rgb") NotEqual(t, err, nil) @@ -2029,6 +3507,8 @@ func TestFlattening(t *testing.T) { func TestStructStringValidation(t *testing.T) { + validate.SetMaxStructPoolSize(11) + tSuccess := &TestString{ Required: "Required", Len: "length==10", @@ -2300,7 +3780,7 @@ func TestInvalidField(t *testing.T) { Test: "1", } - PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to ValidateFieldWithTag") + PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to fieldWithNameAndValue") } func TestInvalidTagField(t *testing.T) { @@ -2319,53 +3799,22 @@ func TestInvalidValidatorFunction(t *testing.T) { PanicMatches(t, func() { validate.Field(s.Test, "zzxxBadFunction") }, fmt.Sprintf("Undefined validation function on field %s", "")) } -func BenchmarkValidateField(b *testing.B) { - for n := 0; n < b.N; n++ { - validate.Field("1", "len=1") - } -} - -func BenchmarkValidateStruct(b *testing.B) { - - // type Inner struct { +func TestPoolObjectMaxSizeValidation(t *testing.T) { + // this will ensure that the pool objects are let go + // when the pool is saturated + validate.SetMaxStructPoolSize(0) - // } - - // type Test struct { - // StringVal string `bson:"required,lt=10"` - // Int64Val int64 `bson:"gt=0,lt=10"` - // } - - tFail := &TestString{ - Required: "", - Len: "", - Min: "", - Max: "12345678901", - MinMax: "", - Lt: "0123456789", - Lte: "01234567890", - Gt: "1", - Gte: "1", - OmitEmpty: "12345678901", - Sub: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "", - }, - Iface: &Impl{ - F: "12", - }, + tSuccess := &TestSlice{ + Required: []int{1}, + Len: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, + Min: []int{1, 2}, + Max: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, + MinMax: []int{1, 2, 3, 4, 5}, + OmitEmpty: []int{}, } - // t := &Test{ - // StringVal: "test", - // Int64Val: 5, - // } - - for n := 0; n < b.N; n++ { - validate.Struct(tFail) + for i := 0; i < 2; i++ { + err := validate.Struct(tSuccess) + Equal(t, err, nil) } }