Merge branch 'v5-development' of https://gopkg.in/bluesuncorp/validator.v5 into v5-development

pull/111/head
zhing 10 years ago
commit 3bece518fa
  1. 2
      .gitignore
  2. 9
      .travis.yml
  3. 122
      README.md
  4. 135
      baked_in.go
  5. 163
      benchmarks_test.go
  6. 88
      doc.go
  7. 85
      examples/simple.go
  8. 95
      examples_test.go
  9. 38
      regexes.go
  10. 754
      validator.go
  11. 1715
      validator_test.go

2
.gitignore vendored

@ -24,3 +24,5 @@ _testmain.go
*.prof *.prof
*.test *.test
*.out *.out
cover.html
README.html

@ -7,7 +7,14 @@ notificaitons:
on_failure: always on_failure: always
go: go:
- 1.2
- 1.3 - 1.3
- 1.4 - 1.4
- tip - 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

@ -1,17 +1,25 @@
Package validator 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) [![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) [![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. 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 Installation
============ ------------
Use go get. Use go get.
go get -u gopkg.in/bluesuncorp/validator.v5 go get gopkg.in/bluesuncorp/validator.v5
or to update or to update
@ -22,12 +30,114 @@ Then import the validator package into your own code.
import "gopkg.in/bluesuncorp/validator.v5" import "gopkg.in/bluesuncorp/validator.v5"
Usage and documentation Usage and documentation
======================= ------
Please see http://godoc.org/gopkg.in/bluesuncorp/validator.v5 for detailed usage docs. 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 How to Contribute
================= ------
There will always be a development branch for each version i.e. `v1-development`. In order 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. 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. help make this package even better.
License License
======= ------
Distributed under MIT License, please see license file in code for more details. Distributed under MIT License, please see license file in code for more details.

@ -50,6 +50,141 @@ var BakedInValidators = map[string]Func{
"excludes": excludes, "excludes": excludes,
"excludesall": excludesAll, "excludesall": excludesAll,
"excludesrune": excludesRune, "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 { func excludesRune(top interface{}, current interface{}, field interface{}, param string) bool {

@ -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)
}
})
}

@ -10,7 +10,7 @@ Validate
A simple example usage: A simple example usage:
type UserDetail { type UserDetail struct {
Details string `validate:"-"` 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 if cross field + cross struct validation is needed your own custom validator
should be implemented. 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: 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. verify it has been assigned.
omitempty 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 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. such as min or max won't run, but if a value is set validation will run.
(Usage: omitempty) (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 required
This validates that the value is not the data types default value. This validates that the value is not the data types default value.
For numbers ensures value is not zero. For strings ensures value is 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. This validates that a string value does not contain the supplied rune value.
(Usage: excludesrune=@) (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: Validator notes:
regex regex

@ -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
}

@ -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:
//<nil>
}
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
}
}

@ -9,12 +9,25 @@ const (
numberRegexString = "^[0-9]+$" numberRegexString = "^[0-9]+$"
hexadecimalRegexString = "^[0-9a-fA-F]+$" hexadecimalRegexString = "^[0-9a-fA-F]+$"
hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" 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*\\)$" 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])\\s*,\\s*((0.[1-9]*)|[01])\\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*\\)$" 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*\\)$" 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}])))\\.?$" 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}=)$)" 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 ( var (
@ -30,6 +43,19 @@ var (
hslaRegex = regexp.MustCompile(hslaRegexString) hslaRegex = regexp.MustCompile(hslaRegexString)
emailRegex = regexp.MustCompile(emailRegexString) emailRegex = regexp.MustCompile(emailRegexString)
base64Regex = regexp.MustCompile(base64RegexString) 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 { func matchesRegex(regex *regexp.Regexp, field interface{}) bool {

@ -14,21 +14,110 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"sync"
"time" "time"
"unicode" "unicode"
) )
const ( const (
utf8HexComma = "0x2C"
tagSeparator = "," tagSeparator = ","
orSeparator = "|" orSeparator = "|"
noValidationTag = "-" noValidationTag = "-"
tagKeySeparator = "=" tagKeySeparator = "="
structOnlyTag = "structonly" structOnlyTag = "structonly"
omitempty = "omitempty" omitempty = "omitempty"
required = "required"
fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" 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" 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 // FieldError contains a single field's validation error along
// with other properties that may be needed for error message creation // with other properties that may be needed for error message creation
type FieldError struct { type FieldError struct {
@ -38,14 +127,117 @@ type FieldError struct {
Type reflect.Type Type reflect.Type
Param string Param string
Value interface{} 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. // 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 // it also allows FieldError to be used as an Error interface
func (e *FieldError) Error() string { 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) 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 // StructErrors is hierarchical list of field and struct validation errors
// for a non hierarchical representation please see the Flatten method for StructErrors // for a non hierarchical representation please see the Flatten method for StructErrors
type StructErrors struct { type StructErrors struct {
@ -72,7 +264,7 @@ func (e *StructErrors) Error() string {
buff.WriteString(err.Error()) 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 // 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 { 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 { for key, val := range e.StructErrors {
@ -124,6 +326,9 @@ type Validate struct {
// New creates a new Validate instance for use. // New creates a new Validate instance for use.
func New(tagName string, funcs map[string]Func) *Validate { func New(tagName string, funcs map[string]Func) *Validate {
structPool = &sync.Pool{New: newStructErrors}
return &Validate{ return &Validate{
tagName: tagName, tagName: tagName,
validationFuncs: funcs, 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 // 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 // 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) { func (v *Validate) SetTag(tagName string) {
v.tagName = tagName 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 // 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: 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 { func (v *Validate) AddFunction(key string, f Func) error {
if len(key) == 0 { if len(key) == 0 {
@ -176,47 +392,84 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter
} }
structType := reflect.TypeOf(s) structType := reflect.TypeOf(s)
structName := structType.Name()
validationErrors := &StructErrors{ var structName string
Struct: structName, var numFields int
Errors: map[string]*FieldError{}, var cs *cachedStruct
StructErrors: map[string]*StructErrors{}, 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++ { for i := 0; i < numFields; i++ {
valueField := structValue.Field(i) var valueField reflect.Value
typeField := structType.Field(i) var cField *cachedField
var typeField reflect.StructField
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)
if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
valueField = valueField.Elem() valueField = valueField.Elem()
} }
tag := typeField.Tag.Get(v.tagName) 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 tag == noValidationTag { if cField.tag == noValidationTag {
cs.children--
continue continue
} }
// if no validation and not a struct (which may containt fields for validation) // 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{})) { if cField.tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) {
cs.children--
continue continue
} }
switch valueField.Kind() { cField.name = typeField.Name
cField.kind = valueField.Kind()
cField.typ = valueField.Type()
}
// 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 cField.kind {
case reflect.Struct, reflect.Interface: case reflect.Struct, reflect.Interface:
if !unicode.IsUpper(rune(typeField.Name[0])) { if !unicode.IsUpper(rune(cField.name[0])) {
cs.children--
continue 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 validationErrors.Errors[fieldError.Field] = fieldError
// free up memory reference // free up memory reference
fieldError = nil fieldError = nil
@ -224,28 +477,116 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter
} else { } else {
if strings.Contains(tag, structOnlyTag) { if strings.Contains(cField.tag, structOnlyTag) {
cs.children--
continue 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 { 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 // free up memory map no longer needed
structErrors = nil 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 validationErrors.Errors[fieldError.Field] = fieldError
// free up memory reference // free up memory reference
fieldError = nil fieldError = nil
} }
} }
CACHEFIELD:
if !isCached {
cs.fields = append(cs.fields, cField)
} }
}
structCache.Set(structType, cs)
if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 { if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 {
structPool.Put(validationErrors)
return nil 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 // 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 { func (v *Validate) Field(f interface{}, tag string) *FieldError {
return v.FieldWithValue(nil, f, tag) 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 // 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 { func (v *Validate) FieldWithValue(val interface{}, f interface{}, tag string) *FieldError {
return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil)
return v.fieldWithNameAndValue(nil, val, f, "", tag)
} }
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 // 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 return nil
} }
@ -275,87 +618,357 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f
return nil return nil
} }
valueField := reflect.ValueOf(f) valueField = reflect.ValueOf(f)
fieldKind := valueField.Kind()
if fieldKind == reflect.Ptr && !valueField.IsNil() { if cacheField == nil {
return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), name, tag)
if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
valueField = valueField.Elem()
f = valueField.Interface()
} }
fieldType := valueField.Type() 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 fieldKind { switch cField.kind {
case reflect.Struct, reflect.Interface, reflect.Invalid: case reflect.Struct, reflect.Interface, reflect.Invalid:
if fieldType != reflect.TypeOf(time.Time{}) { if cField.typ != reflect.TypeOf(time.Time{}) {
panic("Invalid field passed to ValidateFieldWithTag") panic("Invalid field passed to fieldWithNameAndValue")
} }
} }
var valErr *FieldError if len(cField.tags) == 0 {
var err error
valTags := strings.Split(tag, tagSeparator) 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}
}
}
if isSingleField {
fieldsCache.Set(cField.tag, cField.tags)
}
}
}
for _, valTag := range valTags { var fieldErr *FieldError
var err error
orVals := strings.Split(valTag, orSeparator) for _, cTag := range cField.tags {
if len(orVals) > 1 { if cTag.isOrVal {
errTag := "" 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 { if err == nil {
return nil return nil
} }
errTag += orSeparator + valErr.Tag errTag += orSeparator + fieldErr.Tag
} }
errTag = strings.TrimLeft(errTag, orSeparator) errTag = strings.TrimLeft(errTag, orSeparator)
valErr.Tag = errTag fieldErr.Tag = errTag
valErr.Kind = fieldKind fieldErr.Kind = cField.kind
fieldErr.Type = cField.typ
return fieldErr
}
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
return valErr return fieldErr
} }
}
if cField.dive {
if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, name, valTag); err != nil { if cField.isSliceOrArray {
valErr.Kind = valueField.Kind() if errs := v.traverseSliceOrArray(val, current, valueField, cField); errs != nil && len(errs) > 0 {
valErr.Type = fieldType
return valErr 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 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) errs := map[interface{}]error{}
key := strings.TrimSpace(vals[0])
if len(key) == 0 { for _, key := range valueField.MapKeys() {
panic(fmt.Sprintf("Invalid validation tag on field %s", name))
idxField := valueField.MapIndex(key)
if cField.mapSubKind == reflect.Ptr && !idxField.IsNil() {
idxField = idxField.Elem()
cField.mapSubKind = idxField.Kind()
} }
valErr := &FieldError{ switch cField.mapSubKind {
Field: name, case reflect.Struct, reflect.Interface:
Tag: key,
Value: f, if cField.isTimeSubtype {
Param: "",
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
}
}
}
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 // OK to continue because we checked it's existance before getting into this loop
if key == omitempty { if key == omitempty {
return valErr, nil return nil, nil
} }
valFunc, ok := v.validationFuncs[key] 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)) panic(fmt.Sprintf("Undefined validation function on field %s", name))
} }
param := "" if err := valFunc(val, current, f, param); err {
if len(vals) > 1 { return nil, nil
param = strings.TrimSpace(vals[1])
} }
if err := valFunc(val, current, f, param); !err { return &FieldError{
valErr.Param = param Field: name,
return valErr, errors.New(key) Tag: key,
} Value: f,
Param: param,
return valErr, nil }, errors.New(key)
} }

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save