Add handling of custom types

can add custom type handling, example: sql driver Valuer
can override any base data type like string or int ( I'd be carefull about that, but you can )
pull/135/head
joeybloggs 9 years ago
parent a7e8a12f07
commit d363ed316c
  1. 103
      README.md
  2. 84
      benchmarks_test.go
  3. 58
      examples/simple.go
  4. 34
      validator.go
  5. 128
      validator_test.go

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

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

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

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

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

Loading…
Cancel
Save