diff --git a/README.md b/README.md index b93b0b0..74b5700 100644 --- a/README.md +++ b/README.md @@ -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 ------ ###### 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 test -cpu=4 -bench=. -benchmem=true PASS -BenchmarkFieldSuccess-4 5000000 291 ns/op 16 B/op 1 allocs/op -BenchmarkFieldFailure-4 5000000 294 ns/op 16 B/op 1 allocs/op -BenchmarkFieldDiveSuccess-4 500000 3498 ns/op 528 B/op 28 allocs/op -BenchmarkFieldDiveFailure-4 300000 4094 ns/op 928 B/op 32 allocs/op -BenchmarkFieldCustomTypeSuccess-4 3000000 460 ns/op 32 B/op 2 allocs/op -BenchmarkFieldCustomTypeFailure-4 2000000 758 ns/op 400 B/op 4 allocs/op -BenchmarkFieldOrTagSuccess-4 1000000 1393 ns/op 32 B/op 2 allocs/op -BenchmarkFieldOrTagFailure-4 1000000 1181 ns/op 432 B/op 6 allocs/op -BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1218 ns/op 80 B/op 5 allocs/op -BenchmarkStructSimpleCustomTypeFailure-4 1000000 1748 ns/op 624 B/op 11 allocs/op -BenchmarkStructPartialSuccess-4 1000000 1392 ns/op 400 B/op 11 allocs/op -BenchmarkStructPartialFailure-4 1000000 1938 ns/op 816 B/op 16 allocs/op -BenchmarkStructExceptSuccess-4 2000000 903 ns/op 368 B/op 9 allocs/op -BenchmarkStructExceptFailure-4 1000000 1381 ns/op 400 B/op 11 allocs/op -BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1215 ns/op 128 B/op 6 allocs/op -BenchmarkStructSimpleCrossFieldFailure-4 1000000 1781 ns/op 560 B/op 11 allocs/op -BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1801 ns/op 160 B/op 8 allocs/op -BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2357 ns/op 592 B/op 13 allocs/op -BenchmarkStructSimpleSuccess-4 1000000 1161 ns/op 48 B/op 3 allocs/op -BenchmarkStructSimpleFailure-4 1000000 1818 ns/op 624 B/op 11 allocs/op -BenchmarkStructSimpleSuccessParallel-4 5000000 375 ns/op 48 B/op 3 allocs/op -BenchmarkStructSimpleFailureParallel-4 2000000 757 ns/op 624 B/op 11 allocs/op -BenchmarkStructComplexSuccess-4 200000 8053 ns/op 432 B/op 27 allocs/op -BenchmarkStructComplexFailure-4 100000 12634 ns/op 3335 B/op 69 allocs/op -BenchmarkStructComplexSuccessParallel-4 1000000 2718 ns/op 432 B/op 27 allocs/op -BenchmarkStructComplexFailureParallel-4 300000 5086 ns/op 3336 B/op 69 allocs/op +BenchmarkFieldSuccess-4 5000000 305 ns/op 16 B/op 1 allocs/op +BenchmarkFieldFailure-4 5000000 301 ns/op 16 B/op 1 allocs/op +BenchmarkFieldDiveSuccess-4 500000 3544 ns/op 528 B/op 28 allocs/op +BenchmarkFieldDiveFailure-4 300000 4120 ns/op 928 B/op 32 allocs/op +BenchmarkFieldCustomTypeSuccess-4 3000000 465 ns/op 32 B/op 2 allocs/op +BenchmarkFieldCustomTypeFailure-4 2000000 769 ns/op 400 B/op 4 allocs/op +BenchmarkFieldOrTagSuccess-4 1000000 1372 ns/op 32 B/op 2 allocs/op +BenchmarkFieldOrTagFailure-4 1000000 1218 ns/op 432 B/op 6 allocs/op +BenchmarkStructLevelValidationSuccess-4 2000000 840 ns/op 160 B/op 6 allocs/op +BenchmarkStructLevelValidationFailure-4 1000000 1443 ns/op 592 B/op 11 allocs/op +BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1262 ns/op 80 B/op 5 allocs/op +BenchmarkStructSimpleCustomTypeFailure-4 1000000 1812 ns/op 624 B/op 11 allocs/op +BenchmarkStructPartialSuccess-4 1000000 1419 ns/op 400 B/op 11 allocs/op +BenchmarkStructPartialFailure-4 1000000 1967 ns/op 816 B/op 16 allocs/op +BenchmarkStructExceptSuccess-4 2000000 954 ns/op 368 B/op 9 allocs/op +BenchmarkStructExceptFailure-4 1000000 1422 ns/op 400 B/op 11 allocs/op +BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1286 ns/op 128 B/op 6 allocs/op +BenchmarkStructSimpleCrossFieldFailure-4 1000000 1885 ns/op 560 B/op 11 allocs/op +BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1948 ns/op 176 B/op 9 allocs/op +BenchmarkStructSimpleCrossStructCrossFieldFailure-4 500000 2491 ns/op 608 B/op 14 allocs/op +BenchmarkStructSimpleSuccess-4 1000000 1239 ns/op 48 B/op 3 allocs/op +BenchmarkStructSimpleFailure-4 1000000 1891 ns/op 624 B/op 11 allocs/op +BenchmarkStructSimpleSuccessParallel-4 5000000 386 ns/op 48 B/op 3 allocs/op +BenchmarkStructSimpleFailureParallel-4 2000000 842 ns/op 624 B/op 11 allocs/op +BenchmarkStructComplexSuccess-4 200000 8604 ns/op 512 B/op 30 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 diff --git a/benchmarks_test.go b/benchmarks_test.go index 3a2a1fc..2c9af0a 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -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) { validate.RegisterCustomTypeFunc(ValidateValuerType, (*sql.Valuer)(nil), valuer{}) diff --git a/examples/struct-level/struct_level.go b/examples/struct-level/struct_level.go new file mode 100644 index 0000000..92526c9 --- /dev/null +++ b/examples/struct-level/struct_level.go @@ -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 +} diff --git a/validator.go b/validator.go index 6329986..4fbaca0 100644 --- a/validator.go +++ b/validator.go @@ -36,6 +36,8 @@ const ( invalidValidation = "Invalid validation tag on field %s" undefinedValidation = "Undefined validation function on field %s" validatorNotInitialized = "Validator instance not initialized" + fieldNameRequired = "Field Name Required" + tagRequired = "Tag Required" ) var ( @@ -75,17 +77,72 @@ func (s *tagCacheMap) Set(key string, value *cachedTag) { 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 type Validate struct { - tagName string - fieldNameTag string - validationFuncs map[string]Func - customTypeFuncs map[reflect.Type]CustomTypeFunc - aliasValidators map[string]string - hasCustomFuncs bool - hasAliasValidators bool - tagsCache *tagCacheMap - errsPool *sync.Pool + tagName string + fieldNameTag string + validationFuncs map[string]Func + structLevelFuncs map[reflect.Type]StructLevelFunc + customTypeFuncs map[reflect.Type]CustomTypeFunc + aliasValidators map[string]string + hasCustomFuncs bool + hasAliasValidators bool + hasStructLevelFuncs bool + tagsCache *tagCacheMap + errsPool *sync.Pool } func (v *Validate) initCheck() { @@ -114,6 +171,9 @@ type CustomTypeFunc func(field reflect.Value) interface{} // 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 +// StructLevelFunc accepts all values needed for struct level validation +type StructLevelFunc func(v *Validate, structLevel *StructLevel) + // ValidationErrors is a type of map[string]*FieldError // it exists to allow for multiple errors to be passed from this library // and yet still subscribe to the error interface @@ -178,17 +238,33 @@ func New(config *Config) *Validate { 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 // 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 -func (v *Validate) RegisterValidation(key string, f Func) error { +func (v *Validate) RegisterValidation(key string, fn Func) error { v.initCheck() if len(key) == 0 { return errors.New("Function Key cannot be empty") } - if f == nil { + if fn == nil { 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)) } - v.validationFuncs[key] = f + v.validationFuncs[key] = fn 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) } + + // 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 diff --git a/validator_test.go b/validator_test.go index bac8d46..50df0bf 100644 --- a/validator_test.go +++ b/validator_test.go @@ -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) { validate.RegisterAliasValidation("iscolor", "hexcolor|rgb|rgba|hsl|hsla")