Add Struct Level Validation!

pull/203/head
joeybloggs 9 years ago
parent 1570c9b6b3
commit f1acffdfe0
  1. 157
      README.md
  2. 26
      benchmarks_test.go
  3. 99
      examples/struct-level/struct_level.go
  4. 89
      validator.go
  5. 106
      validator_test.go

@ -200,38 +200,143 @@ func ValidateValuer(field reflect.Value) interface{} {
} }
``` ```
Struct Level Validation
```go
package main
import (
"fmt"
"reflect"
"gopkg.in/go-playground/validator.v8"
)
// User contains user information
type User struct {
FirstName string `json:"fname"`
LastName string `json:"lname"`
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() {
config := &validator.Config{TagName: "validate"}
validate = validator.New(config)
validate.RegisterStructValidation(UserStructLevelValidation, User{})
validateStruct()
}
// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For Example this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
user := structLevel.CurrentStruct.Interface().(User)
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
structLevel.ReportError(reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname")
structLevel.ReportError(reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname")
}
// plus can to more, even with different tag than "fnameorlname"
}
func validateStruct() {
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
City: "Unknown",
}
user := &User{
FirstName: "",
LastName: "",
Age: 45,
Email: "Badger.Smith@gmail.com",
FavouriteColor: "#000",
Addresses: []*Address{address},
}
// returns nil or ValidationErrors ( map[string]*FieldError )
errs := validate.Struct(user)
if errs != nil {
fmt.Println(errs) // output: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag
// Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag
err := errs.(validator.ValidationErrors)["User.FirstName"]
fmt.Println(err.Field) // output: FirstName
fmt.Println(err.Tag) // output: fnameorlname
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 Benchmarks
------ ------
###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 using Go 1.5.1 ###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 using Go 1.5.1
```go ```go
$ go test -cpu=4 -bench=. -benchmem=true $ go test -cpu=4 -bench=. -benchmem=true
PASS PASS
BenchmarkFieldSuccess-4 5000000 291 ns/op 16 B/op 1 allocs/op BenchmarkFieldSuccess-4 5000000 305 ns/op 16 B/op 1 allocs/op
BenchmarkFieldFailure-4 5000000 294 ns/op 16 B/op 1 allocs/op BenchmarkFieldFailure-4 5000000 301 ns/op 16 B/op 1 allocs/op
BenchmarkFieldDiveSuccess-4 500000 3498 ns/op 528 B/op 28 allocs/op BenchmarkFieldDiveSuccess-4 500000 3544 ns/op 528 B/op 28 allocs/op
BenchmarkFieldDiveFailure-4 300000 4094 ns/op 928 B/op 32 allocs/op BenchmarkFieldDiveFailure-4 300000 4120 ns/op 928 B/op 32 allocs/op
BenchmarkFieldCustomTypeSuccess-4 3000000 460 ns/op 32 B/op 2 allocs/op BenchmarkFieldCustomTypeSuccess-4 3000000 465 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-4 2000000 758 ns/op 400 B/op 4 allocs/op BenchmarkFieldCustomTypeFailure-4 2000000 769 ns/op 400 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-4 1000000 1393 ns/op 32 B/op 2 allocs/op BenchmarkFieldOrTagSuccess-4 1000000 1372 ns/op 32 B/op 2 allocs/op
BenchmarkFieldOrTagFailure-4 1000000 1181 ns/op 432 B/op 6 allocs/op BenchmarkFieldOrTagFailure-4 1000000 1218 ns/op 432 B/op 6 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1218 ns/op 80 B/op 5 allocs/op BenchmarkStructLevelValidationSuccess-4 2000000 840 ns/op 160 B/op 6 allocs/op
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1748 ns/op 624 B/op 11 allocs/op BenchmarkStructLevelValidationFailure-4 1000000 1443 ns/op 592 B/op 11 allocs/op
BenchmarkStructPartialSuccess-4 1000000 1392 ns/op 400 B/op 11 allocs/op BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1262 ns/op 80 B/op 5 allocs/op
BenchmarkStructPartialFailure-4 1000000 1938 ns/op 816 B/op 16 allocs/op BenchmarkStructSimpleCustomTypeFailure-4 1000000 1812 ns/op 624 B/op 11 allocs/op
BenchmarkStructExceptSuccess-4 2000000 903 ns/op 368 B/op 9 allocs/op BenchmarkStructPartialSuccess-4 1000000 1419 ns/op 400 B/op 11 allocs/op
BenchmarkStructExceptFailure-4 1000000 1381 ns/op 400 B/op 11 allocs/op BenchmarkStructPartialFailure-4 1000000 1967 ns/op 816 B/op 16 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1215 ns/op 128 B/op 6 allocs/op BenchmarkStructExceptSuccess-4 2000000 954 ns/op 368 B/op 9 allocs/op
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1781 ns/op 560 B/op 11 allocs/op BenchmarkStructExceptFailure-4 1000000 1422 ns/op 400 B/op 11 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1801 ns/op 160 B/op 8 allocs/op BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1286 ns/op 128 B/op 6 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2357 ns/op 592 B/op 13 allocs/op BenchmarkStructSimpleCrossFieldFailure-4 1000000 1885 ns/op 560 B/op 11 allocs/op
BenchmarkStructSimpleSuccess-4 1000000 1161 ns/op 48 B/op 3 allocs/op BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1948 ns/op 176 B/op 9 allocs/op
BenchmarkStructSimpleFailure-4 1000000 1818 ns/op 624 B/op 11 allocs/op BenchmarkStructSimpleCrossStructCrossFieldFailure-4 500000 2491 ns/op 608 B/op 14 allocs/op
BenchmarkStructSimpleSuccessParallel-4 5000000 375 ns/op 48 B/op 3 allocs/op BenchmarkStructSimpleSuccess-4 1000000 1239 ns/op 48 B/op 3 allocs/op
BenchmarkStructSimpleFailureParallel-4 2000000 757 ns/op 624 B/op 11 allocs/op BenchmarkStructSimpleFailure-4 1000000 1891 ns/op 624 B/op 11 allocs/op
BenchmarkStructComplexSuccess-4 200000 8053 ns/op 432 B/op 27 allocs/op BenchmarkStructSimpleSuccessParallel-4 5000000 386 ns/op 48 B/op 3 allocs/op
BenchmarkStructComplexFailure-4 100000 12634 ns/op 3335 B/op 69 allocs/op BenchmarkStructSimpleFailureParallel-4 2000000 842 ns/op 624 B/op 11 allocs/op
BenchmarkStructComplexSuccessParallel-4 1000000 2718 ns/op 432 B/op 27 allocs/op BenchmarkStructComplexSuccess-4 200000 8604 ns/op 512 B/op 30 allocs/op
BenchmarkStructComplexFailureParallel-4 300000 5086 ns/op 3336 B/op 69 allocs/op BenchmarkStructComplexFailure-4 100000 13332 ns/op 3416 B/op 72 allocs/op
BenchmarkStructComplexSuccessParallel-4 1000000 2929 ns/op 512 B/op 30 allocs/op
BenchmarkStructComplexFailureParallel-4 300000 5220 ns/op 3416 B/op 72 allocs/op
``` ```
How to Contribute How to Contribute

@ -67,6 +67,32 @@ func BenchmarkFieldOrTagFailure(b *testing.B) {
} }
} }
func BenchmarkStructLevelValidationSuccess(b *testing.B) {
validate.RegisterStructValidation(StructValidationTestStructSuccess, TestStruct{})
tst := &TestStruct{
String: "good value",
}
for n := 0; n < b.N; n++ {
validate.Struct(tst)
}
}
func BenchmarkStructLevelValidationFailure(b *testing.B) {
validate.RegisterStructValidation(StructValidationTestStruct, TestStruct{})
tst := &TestStruct{
String: "good value",
}
for n := 0; n < b.N; n++ {
validate.Struct(tst)
}
}
func BenchmarkStructSimpleCustomTypeSuccess(b *testing.B) { func BenchmarkStructSimpleCustomTypeSuccess(b *testing.B) {
validate.RegisterCustomTypeFunc(ValidateValuerType, (*sql.Valuer)(nil), valuer{}) validate.RegisterCustomTypeFunc(ValidateValuerType, (*sql.Valuer)(nil), valuer{})

@ -0,0 +1,99 @@
package main
import (
"fmt"
"reflect"
"gopkg.in/go-playground/validator.v8"
)
// User contains user information
type User struct {
FirstName string `json:"fname"`
LastName string `json:"lname"`
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() {
config := &validator.Config{TagName: "validate"}
validate = validator.New(config)
validate.RegisterStructValidation(UserStructLevelValidation, User{})
validateStruct()
}
// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For Example this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
user := structLevel.CurrentStruct.Interface().(User)
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
structLevel.ReportError(reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname")
structLevel.ReportError(reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname")
}
// plus can to more, even with different tag than "fnameorlname"
}
func validateStruct() {
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
City: "Unknown",
}
user := &User{
FirstName: "",
LastName: "",
Age: 45,
Email: "Badger.Smith@gmail.com",
FavouriteColor: "#000",
Addresses: []*Address{address},
}
// returns nil or ValidationErrors ( map[string]*FieldError )
errs := validate.Struct(user)
if errs != nil {
fmt.Println(errs) // output: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag
// Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag
err := errs.(validator.ValidationErrors)["User.FirstName"]
fmt.Println(err.Field) // output: FirstName
fmt.Println(err.Tag) // output: fnameorlname
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
}

@ -36,6 +36,8 @@ const (
invalidValidation = "Invalid validation tag on field %s" invalidValidation = "Invalid validation tag on field %s"
undefinedValidation = "Undefined validation function on field %s" undefinedValidation = "Undefined validation function on field %s"
validatorNotInitialized = "Validator instance not initialized" validatorNotInitialized = "Validator instance not initialized"
fieldNameRequired = "Field Name Required"
tagRequired = "Tag Required"
) )
var ( var (
@ -75,15 +77,70 @@ func (s *tagCacheMap) Set(key string, value *cachedTag) {
s.m[key] = value s.m[key] = value
} }
// StructLevel contains all of the information and helper methods
// for reporting errors during struct level validation
type StructLevel struct {
TopStruct reflect.Value
CurrentStruct reflect.Value
errPrefix string
errs ValidationErrors
v *Validate
}
// ReportError reports an error just by passing the field and tag information
// NOTE: tag can be an existing validation tag or just something you make up
// and precess on the flip side it's up to you.
func (sl *StructLevel) ReportError(field reflect.Value, fieldName string, customName string, tag string) {
field, kind := sl.v.ExtractType(field)
if len(fieldName) == 0 {
panic(fieldNameRequired)
}
if len(customName) == 0 {
customName = fieldName
}
if len(tag) == 0 {
panic(tagRequired)
}
switch kind {
case reflect.Invalid:
sl.errs[sl.errPrefix+fieldName] = &FieldError{
Name: customName,
Field: fieldName,
Tag: tag,
ActualTag: tag,
Param: blank,
Kind: kind,
}
default:
sl.errs[sl.errPrefix+fieldName] = &FieldError{
Name: customName,
Field: fieldName,
Tag: tag,
ActualTag: tag,
Param: blank,
Value: field.Interface(),
Kind: kind,
Type: field.Type(),
}
}
}
// Validate contains the validator settings passed in using the Config struct // Validate contains the validator settings passed in using the Config struct
type Validate struct { type Validate struct {
tagName string tagName string
fieldNameTag string fieldNameTag string
validationFuncs map[string]Func validationFuncs map[string]Func
structLevelFuncs map[reflect.Type]StructLevelFunc
customTypeFuncs map[reflect.Type]CustomTypeFunc customTypeFuncs map[reflect.Type]CustomTypeFunc
aliasValidators map[string]string aliasValidators map[string]string
hasCustomFuncs bool hasCustomFuncs bool
hasAliasValidators bool hasAliasValidators bool
hasStructLevelFuncs bool
tagsCache *tagCacheMap tagsCache *tagCacheMap
errsPool *sync.Pool errsPool *sync.Pool
} }
@ -114,6 +171,9 @@ type CustomTypeFunc func(field reflect.Value) interface{}
// param = parameter used in validation i.e. gt=0 param would be 0 // param = parameter used in validation i.e. gt=0 param would be 0
type Func func(v *Validate, topStruct reflect.Value, currentStruct reflect.Value, field reflect.Value, fieldtype reflect.Type, fieldKind reflect.Kind, param string) bool type Func func(v *Validate, topStruct reflect.Value, currentStruct reflect.Value, field reflect.Value, fieldtype reflect.Type, fieldKind reflect.Kind, param string) bool
// StructLevelFunc accepts all values needed for struct level validation
type StructLevelFunc func(v *Validate, structLevel *StructLevel)
// ValidationErrors is a type of map[string]*FieldError // ValidationErrors is a type of map[string]*FieldError
// it exists to allow for multiple errors to be passed from this library // it exists to allow for multiple errors to be passed from this library
// and yet still subscribe to the error interface // and yet still subscribe to the error interface
@ -178,17 +238,33 @@ func New(config *Config) *Validate {
return v return v
} }
// RegisterStructValidation registers a StructLevelFunc against a number of types
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
func (v *Validate) RegisterStructValidation(fn StructLevelFunc, types ...interface{}) {
v.initCheck()
if v.structLevelFuncs == nil {
v.structLevelFuncs = map[reflect.Type]StructLevelFunc{}
}
for _, t := range types {
v.structLevelFuncs[reflect.TypeOf(t)] = fn
}
v.hasStructLevelFuncs = true
}
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key // RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
// NOTE: if the key already exists, the previous validation function will be replaced. // NOTE: if the key already exists, the previous validation function will be replaced.
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation // NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
func (v *Validate) RegisterValidation(key string, f Func) error { func (v *Validate) RegisterValidation(key string, fn Func) error {
v.initCheck() v.initCheck()
if len(key) == 0 { if len(key) == 0 {
return errors.New("Function Key cannot be empty") return errors.New("Function Key cannot be empty")
} }
if f == nil { if fn == nil {
return errors.New("Function cannot be empty") return errors.New("Function cannot be empty")
} }
@ -198,7 +274,7 @@ func (v *Validate) RegisterValidation(key string, f Func) error {
panic(fmt.Sprintf(restrictedTagErr, key)) panic(fmt.Sprintf(restrictedTagErr, key))
} }
v.validationFuncs[key] = f v.validationFuncs[key] = fn
return nil return nil
} }
@ -435,6 +511,13 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude) v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude)
} }
// check if any struct level validations, after all field validations already checked.
if v.hasStructLevelFuncs {
if fn, ok := v.structLevelFuncs[current.Type()]; ok {
fn(v, &StructLevel{v: v, TopStruct: topStruct, CurrentStruct: current, errPrefix: errPrefix, errs: errs})
}
}
} }
// traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options // traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options

@ -212,6 +212,112 @@ type TestPartial struct {
} }
} }
type TestStruct struct {
String string `validate:"required" json:"StringVal"`
}
func StructValidationTestStructSuccess(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "good value" {
structLevel.ReportError(reflect.ValueOf(st.String), "String", "StringVal", "badvalueteststruct")
}
}
func StructValidationTestStruct(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "bad value" {
structLevel.ReportError(reflect.ValueOf(st.String), "String", "StringVal", "badvalueteststruct")
}
}
func StructValidationBadTestStructFieldName(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "bad value" {
structLevel.ReportError(reflect.ValueOf(st.String), "", "StringVal", "badvalueteststruct")
}
}
func StructValidationBadTestStructTag(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "bad value" {
structLevel.ReportError(reflect.ValueOf(st.String), "String", "StringVal", "")
}
}
func StructValidationNoTestStructCustomName(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "bad value" {
structLevel.ReportError(reflect.ValueOf(st.String), "String", "", "badvalueteststruct")
}
}
func StructValidationTestStructInvalid(v *Validate, structLevel *StructLevel) {
st := structLevel.CurrentStruct.Interface().(TestStruct)
if st.String != "bad value" {
structLevel.ReportError(reflect.ValueOf(nil), "String", "StringVal", "badvalueteststruct")
}
}
func TestStructLevelValidations(t *testing.T) {
config := &Config{
TagName: "validate",
}
v1 := New(config)
v1.RegisterStructValidation(StructValidationTestStruct, TestStruct{})
tst := &TestStruct{
String: "good value",
}
errs := v1.Struct(tst)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestStruct.String", "String", "badvalueteststruct")
v2 := New(config)
v2.RegisterStructValidation(StructValidationBadTestStructFieldName, TestStruct{})
PanicMatches(t, func() { v2.Struct(tst) }, fieldNameRequired)
v3 := New(config)
v3.RegisterStructValidation(StructValidationBadTestStructTag, TestStruct{})
PanicMatches(t, func() { v3.Struct(tst) }, tagRequired)
v4 := New(config)
v4.RegisterStructValidation(StructValidationNoTestStructCustomName, TestStruct{})
errs = v4.Struct(tst)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestStruct.String", "String", "badvalueteststruct")
v5 := New(config)
v5.RegisterStructValidation(StructValidationTestStructInvalid, TestStruct{})
errs = v5.Struct(tst)
NotEqual(t, errs, nil)
AssertError(t, errs, "TestStruct.String", "String", "badvalueteststruct")
v6 := New(config)
v6.RegisterStructValidation(StructValidationTestStructSuccess, TestStruct{})
errs = v6.Struct(tst)
Equal(t, errs, nil)
}
func TestAliasTags(t *testing.T) { func TestAliasTags(t *testing.T) {
validate.RegisterAliasValidation("iscolor", "hexcolor|rgb|rgba|hsl|hsla") validate.RegisterAliasValidation("iscolor", "hexcolor|rgb|rgba|hsl|hsla")

Loading…
Cancel
Save