diff --git a/README.md b/README.md index b5a80c0..491e04d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ 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. +- Handles type interface by determining it's underlying type prior to validation. +- Handles custom field types such as sql driver Valuer see [Valuer](https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29) Installation ------------ @@ -35,6 +36,8 @@ Usage and documentation Please see http://godoc.org/gopkg.in/bluesuncorp/validator.v6 for detailed usage docs. ##### Examples: + +Struct & Field validation ```go package main @@ -130,6 +133,76 @@ func validateField() { } ``` +Custom Field Type +```go +package main + +import ( + "errors" + "fmt" + "reflect" + + sql "database/sql/driver" + + "gopkg.in/bluesuncorp/validator.v6" +) + +var validate *validator.Validate + +type valuer struct { + Name string +} + +func (v valuer) Value() (sql.Value, error) { + + if v.Name == "errorme" { + return nil, errors.New("some kind of error") + } + + if v.Name == "blankme" { + return "", nil + } + + if len(v.Name) == 0 { + return nil, nil + } + + return v.Name, nil +} + +func main() { + + customTypes := map[reflect.Type]validator.CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + config := validator.Config{ + TagName: "validate", + ValidationFuncs: validator.BakedInValidators, + CustomTypeFuncs: customTypes, + } + + validate = validator.New(config) + + validateCustomFieldType() +} + +func validateCustomFieldType() { + val := valuer{ + Name: "blankme", + } + + errs := validate.Field(val, "required") + if errs != nil { + fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "required" tag + return + } + + // all ok +} + +``` + Benchmarks ------ ###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 @@ -139,18 +212,22 @@ hurt parallel performance too much. ```go $ go test -cpu=4 -bench=. -benchmem=true PASS -BenchmarkFieldSuccess-4 5000000 326 ns/op 16 B/op 1 allocs/op -BenchmarkFieldFailure-4 5000000 327 ns/op 16 B/op 1 allocs/op -BenchmarkFieldOrTagSuccess-4 500000 2738 ns/op 20 B/op 2 allocs/op -BenchmarkFieldOrTagFailure-4 1000000 1341 ns/op 384 B/op 6 allocs/op -BenchmarkStructSimpleSuccess-4 1000000 1282 ns/op 24 B/op 3 allocs/op -BenchmarkStructSimpleFailure-4 1000000 1870 ns/op 529 B/op 11 allocs/op -BenchmarkStructSimpleSuccessParallel-4 5000000 348 ns/op 24 B/op 3 allocs/op -BenchmarkStructSimpleFailureParallel-4 2000000 807 ns/op 529 B/op 11 allocs/op -BenchmarkStructComplexSuccess-4 200000 8081 ns/op 368 B/op 30 allocs/op -BenchmarkStructComplexFailure-4 100000 12418 ns/op 2861 B/op 72 allocs/op -BenchmarkStructComplexSuccessParallel-4 500000 2249 ns/op 369 B/op 30 allocs/op -BenchmarkStructComplexFailureParallel-4 300000 5183 ns/op 2863 B/op 72 allocs/op +BenchmarkFieldSuccess-4 5000000 318 ns/op 16 B/op 1 allocs/op +BenchmarkFieldFailure-4 5000000 316 ns/op 16 B/op 1 allocs/op +BenchmarkFieldCustomTypeSuccess-4 3000000 492 ns/op 32 B/op 2 allocs/op +BenchmarkFieldCustomTypeFailure-4 2000000 843 ns/op 416 B/op 6 allocs/op +BenchmarkFieldOrTagSuccess-4 500000 2384 ns/op 20 B/op 2 allocs/op +BenchmarkFieldOrTagFailure-4 1000000 1295 ns/op 384 B/op 6 allocs/op +BenchmarkStructSimpleSuccess-4 1000000 1175 ns/op 24 B/op 3 allocs/op +BenchmarkStructSimpleFailure-4 1000000 1822 ns/op 529 B/op 11 allocs/op +BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1302 ns/op 56 B/op 5 allocs/op +BenchmarkStructSimpleCustomTypeFailure-4 1000000 1847 ns/op 577 B/op 13 allocs/op +BenchmarkStructSimpleSuccessParallel-4 5000000 339 ns/op 24 B/op 3 allocs/op +BenchmarkStructSimpleFailureParallel-4 2000000 733 ns/op 529 B/op 11 allocs/op +BenchmarkStructComplexSuccess-4 200000 7104 ns/op 368 B/op 30 allocs/op +BenchmarkStructComplexFailure-4 100000 11996 ns/op 2861 B/op 72 allocs/op +BenchmarkStructComplexSuccessParallel-4 1000000 2252 ns/op 368 B/op 30 allocs/op +BenchmarkStructComplexFailureParallel-4 300000 4691 ns/op 2862 B/op 72 allocs/op ``` How to Contribute diff --git a/benchmarks_test.go b/benchmarks_test.go index 9fcf85b..af201e6 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -1,6 +1,10 @@ package validator -import "testing" +import ( + sql "database/sql/driver" + "reflect" + "testing" +) func BenchmarkFieldSuccess(b *testing.B) { for n := 0; n < b.N; n++ { @@ -14,6 +18,38 @@ func BenchmarkFieldFailure(b *testing.B) { } } +func BenchmarkFieldCustomTypeSuccess(b *testing.B) { + + customTypes := map[reflect.Type]CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + validate := New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, CustomTypeFuncs: customTypes}) + + val := valuer{ + Name: "1", + } + + for n := 0; n < b.N; n++ { + validate.Field(val, "len=1") + } +} + +func BenchmarkFieldCustomTypeFailure(b *testing.B) { + + customTypes := map[reflect.Type]CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + validate := New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, CustomTypeFuncs: customTypes}) + + val := valuer{} + + for n := 0; n < b.N; n++ { + validate.Field(val, "len=1") + } +} + func BenchmarkFieldOrTagSuccess(b *testing.B) { for n := 0; n < b.N; n++ { validate.Field("rgba(0,0,0,1)", "rgb|rgba") @@ -54,6 +90,52 @@ func BenchmarkStructSimpleFailure(b *testing.B) { } } +func BenchmarkStructSimpleCustomTypeSuccess(b *testing.B) { + + customTypes := map[reflect.Type]CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + validate := New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, CustomTypeFuncs: customTypes}) + + val := valuer{ + Name: "1", + } + + type Foo struct { + Valuer valuer `validate:"len=1"` + IntValue int `validate:"min=5,max=10"` + } + + validFoo := &Foo{Valuer: val, IntValue: 7} + + for n := 0; n < b.N; n++ { + validate.Struct(validFoo) + } +} + +func BenchmarkStructSimpleCustomTypeFailure(b *testing.B) { + + customTypes := map[reflect.Type]CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + validate := New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, CustomTypeFuncs: customTypes}) + + val := valuer{} + + type Foo struct { + Valuer valuer `validate:"len=1"` + IntValue int `validate:"min=5,max=10"` + } + + validFoo := &Foo{Valuer: val, IntValue: 3} + + for n := 0; n < b.N; n++ { + validate.Struct(validFoo) + } +} + func BenchmarkStructSimpleSuccessParallel(b *testing.B) { type Foo struct { diff --git a/examples/simple.go b/examples/simple.go index 46e9295..fd503c4 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -1,7 +1,11 @@ package main import ( + "errors" "fmt" + "reflect" + + sql "database/sql/driver" "gopkg.in/bluesuncorp/validator.v6" ) @@ -90,3 +94,57 @@ func validateField() { // email ok, move on } + +var validate2 *validator.Validate + +type valuer struct { + Name string +} + +func (v valuer) Value() (sql.Value, error) { + + if v.Name == "errorme" { + return nil, errors.New("some kind of error") + } + + if v.Name == "blankme" { + return "", nil + } + + if len(v.Name) == 0 { + return nil, nil + } + + return v.Name, nil +} + +func main2() { + + customTypes := map[reflect.Type]validator.CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + + config := validator.Config{ + TagName: "validate", + ValidationFuncs: validator.BakedInValidators, + CustomTypeFuncs: customTypes, + } + + validate2 = validator.New(config) + + validateCustomFieldType() +} + +func validateCustomFieldType() { + val := valuer{ + Name: "blankme", + } + + errs := validate2.Field(val, "required") + if errs != nil { + fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "required" tag + return + } + + // all ok +} diff --git a/validator.go b/validator.go index 19f86a7..e557d28 100644 --- a/validator.go +++ b/validator.go @@ -45,7 +45,7 @@ var ( // returns new ValidationErrors to the pool func newValidationErrors() interface{} { - return map[string]*FieldError{} + return ValidationErrors{} } type tagCache struct { @@ -81,8 +81,15 @@ type Validate struct { type Config struct { TagName string ValidationFuncs map[string]Func + CustomTypeFuncs map[reflect.Type]CustomTypeFunc + hasCustomFuncs bool } +// CustomTypeFunc allows for overriding or adding custom field type handler functions +// field = field value of the type to return a value to be validated +// example Valuer from sql drive see https://golang.org/src/database/sql/driver/types.go?s=1210:1293#L29 +type CustomTypeFunc func(field reflect.Value) interface{} + // Func accepts all values needed for file and cross field validation // topStruct = top level struct when validating by struct otherwise nil // currentStruct = current level struct when validating by struct otherwise optional comparison value @@ -124,6 +131,11 @@ type FieldError struct { // New creates a new Validate instance for use. func New(config Config) *Validate { + + if config.CustomTypeFuncs != nil && len(config.CustomTypeFuncs) > 0 { + config.hasCustomFuncs = true + } + return &Validate{config: config} } @@ -150,7 +162,7 @@ func (v *Validate) RegisterValidation(key string, f Func) error { // validate Array, Slice and maps fields which may contain more than one error func (v *Validate) Field(field interface{}, tag string) ValidationErrors { - errs := errsPool.Get().(map[string]*FieldError) + errs := errsPool.Get().(ValidationErrors) fieldVal := reflect.ValueOf(field) v.traverseField(fieldVal, fieldVal, fieldVal, "", errs, false, tag, "") @@ -168,7 +180,7 @@ func (v *Validate) Field(field interface{}, tag string) ValidationErrors { // validate Array, Slice and maps fields which may contain more than one error func (v *Validate) FieldWithValue(val interface{}, field interface{}, tag string) ValidationErrors { - errs := errsPool.Get().(map[string]*FieldError) + errs := errsPool.Get().(ValidationErrors) topVal := reflect.ValueOf(val) v.traverseField(topVal, topVal, reflect.ValueOf(field), "", errs, false, tag, "") @@ -184,7 +196,7 @@ func (v *Validate) FieldWithValue(val interface{}, field interface{}, tag string // Struct validates a structs exposed fields, and automatically validates nested structs, unless otherwise specified. func (v *Validate) Struct(current interface{}) ValidationErrors { - errs := errsPool.Get().(map[string]*FieldError) + errs := errsPool.Get().(ValidationErrors) sv := reflect.ValueOf(current) v.tranverseStruct(sv, sv, sv, "", errs, true) @@ -316,6 +328,13 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect. if kind == reflect.Struct { + if v.config.hasCustomFuncs { + if fn, ok := v.config.CustomTypeFuncs[typ]; ok { + v.traverseField(topStruct, currentStruct, reflect.ValueOf(fn(current)), errPrefix, errs, isStructField, tag, name) + return + } + } + // required passed validation above so stop here // if only validating the structs existance. if strings.Contains(tag, structOnlyTag) { @@ -334,6 +353,13 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect. } } + if v.config.hasCustomFuncs { + if fn, ok := v.config.CustomTypeFuncs[typ]; ok { + v.traverseField(topStruct, currentStruct, reflect.ValueOf(fn(current)), errPrefix, errs, isStructField, tag, name) + return + } + } + tags, isCached := tagsCache.Get(tag) if !isCached { diff --git a/validator_test.go b/validator_test.go index d69c1a2..37b6dee 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1,11 +1,14 @@ package validator import ( + "errors" "fmt" "reflect" "testing" "time" + sql "database/sql/driver" + . "gopkg.in/bluesuncorp/assert.v1" ) @@ -119,6 +122,131 @@ func AssertError(t *testing.T, errs ValidationErrors, key, field, expectedTag st EqualSkip(t, 2, val.Tag, expectedTag) } +type valuer struct { + Name string +} + +func (v valuer) Value() (sql.Value, error) { + + if v.Name == "errorme" { + return nil, errors.New("some kind of error") + } + + if len(v.Name) == 0 { + return nil, nil + } + + return v.Name, nil +} + +type MadeUpCustomType struct { + FirstName string + LastName string +} + +func ValidateCustomType(field reflect.Value) interface{} { + if cust, ok := field.Interface().(MadeUpCustomType); ok { + + if len(cust.FirstName) == 0 || len(cust.LastName) == 0 { + return "" + } + + return cust.FirstName + " " + cust.LastName + } + + return "" +} + +func OverrideIntTypeForSomeReason(field reflect.Value) interface{} { + + if i, ok := field.Interface().(int); ok { + if i == 1 { + return "1" + } + + if i == 2 { + return "12" + } + } + + return "" +} + +type CustomMadeUpStruct struct { + MadeUp MadeUpCustomType `validate:"required"` + OverriddenInt int `validate:"gt=1"` +} + +func ValidateValuerType(field reflect.Value) interface{} { + if valuer, ok := field.Interface().(sql.Valuer); ok { + val, err := valuer.Value() + if err != nil { + // handle the error how you want + return nil + } + + return val + } + + return nil +} + +func TestSQLValueValidation(t *testing.T) { + + customTypes := map[reflect.Type]CustomTypeFunc{} + customTypes[reflect.TypeOf((*sql.Valuer)(nil))] = ValidateValuerType + customTypes[reflect.TypeOf(valuer{})] = ValidateValuerType + customTypes[reflect.TypeOf(MadeUpCustomType{})] = ValidateCustomType + customTypes[reflect.TypeOf(1)] = OverrideIntTypeForSomeReason + + validate := New(Config{TagName: "validate", ValidationFuncs: BakedInValidators, CustomTypeFuncs: customTypes}) + + val := valuer{ + Name: "", + } + + errs := validate.Field(val, "required") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "required") + + val.Name = "Valid Name" + errs = validate.Field(val, "required") + Equal(t, errs, nil) + + val.Name = "errorme" + + PanicMatches(t, func() { errs = validate.Field(val, "required") }, "SQL Driver Valuer error: some kind of error") + + type myValuer valuer + + myVal := valuer{ + Name: "", + } + + errs = validate.Field(myVal, "required") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "required") + + cust := MadeUpCustomType{ + FirstName: "Joey", + LastName: "Bloggs", + } + + c := CustomMadeUpStruct{MadeUp: cust, OverriddenInt: 2} + + errs = validate.Struct(c) + Equal(t, errs, nil) + + c.MadeUp.FirstName = "" + c.OverriddenInt = 1 + + errs = validate.Struct(c) + NotEqual(t, errs, nil) + Equal(t, len(errs), 2) + AssertError(t, errs, "CustomMadeUpStruct.MadeUp", "MadeUp", "required") + AssertError(t, errs, "CustomMadeUpStruct.OverriddenInt", "OverriddenInt", "gt") +} + func TestMACValidation(t *testing.T) { tests := []struct { param string