add struct caching, significant speedup even in a simple test

NOTE: validating a single fields speed however was reduced and more memory allocations, need to correct this before marking caching as complete.
pull/59/head
joeybloggs 9 years ago
parent 1d61bf3148
commit 22d031deb0
  1. 326
      validator.go
  2. 56
      validator_test.go

@ -162,6 +162,31 @@ func (v *Validate) Struct(s interface{}) *StructErrors {
return v.structRecursive(s, s, s) return v.structRecursive(s, s, s)
} }
type cacheTags struct {
keyVals [][]string
isOrVal bool
}
type cachedField struct {
index int
name string
tags []*cacheTags
// tags [][]string
tag string
kind reflect.Kind
typ reflect.Type
isTime bool
}
type cachedStruct struct {
children int
name string
kind reflect.Kind
fields []*cachedField
}
var cache = map[reflect.Type]*cachedStruct{}
// structRecursive validates a struct recursivly and passes the top level and current struct around for use in validator functions and returns a struct containing the errors // structRecursive validates a struct recursivly and passes the top level and current struct around for use in validator functions and returns a struct containing the errors
func (v *Validate) structRecursive(top interface{}, current interface{}, s interface{}) *StructErrors { func (v *Validate) structRecursive(top interface{}, current interface{}, s interface{}) *StructErrors {
@ -176,7 +201,23 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter
} }
structType := reflect.TypeOf(s) structType := reflect.TypeOf(s)
structName := structType.Name()
var structName string
var numFields int
// fmt.Println(structType)
cs, ok := cache[structType]
if ok {
structName = cs.name
numFields = cs.children
} else {
structName = structType.Name()
numFields = structValue.NumField()
cs = &cachedStruct{name: structName, children: numFields}
cache[structType] = cs
}
validationErrors := &StructErrors{ validationErrors := &StructErrors{
Struct: structName, Struct: structName,
@ -184,39 +225,88 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter
StructErrors: map[string]*StructErrors{}, StructErrors: map[string]*StructErrors{},
} }
var numFields = structValue.NumField()
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 fName string
// var tag string
if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { var typeField reflect.StructField
valueField = valueField.Elem()
} // if ok {
// cField = cs.fields[i]
// valueField = structValue.Field(cField.index)
// } else {
// valueField = structValue.Field(i)
// }
// if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
// valueField = valueField.Elem()
// }
if ok {
cField = cs.fields[i]
// fName = cField.name
// tag = cField.tag
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() {
valueField = valueField.Elem()
}
typeField = structType.Field(i)
cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName)}
// tag = typeField.Tag.Get(v.tagName)
if cField.tag == noValidationTag {
cs.children--
continue
}
tag := typeField.Tag.Get(v.tagName) // if no validation and not a struct (which may containt fields for validation)
if cField.tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) {
cs.children--
continue
}
if tag == noValidationTag { // fName = typeField.Name
continue cField.name = typeField.Name
cField.kind = valueField.Kind()
cField.typ = valueField.Type()
// cField = &cachedField{index: i, name: typeField.Name, tag: tag, kind: valueField.Kind()}
// cs.fields = append(cs.fields, cField)
} }
// if no validation and not a struct (which may containt fields for validation) // this can happen if the first cache value was nil
if tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) { // but the second actually has a value
continue if cField.kind == reflect.Ptr {
cField.kind = valueField.Kind()
} }
switch 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--
// cs.fields = cs.fields[:len(cs.fields)]
continue continue
} }
if valueField.Type() == reflect.TypeOf(time.Time{}) { if cField.isTime || valueField.Type() == reflect.TypeOf(time.Time{}) {
cField.isTime = true
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, 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,24 +314,34 @@ 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--
// cs.fields = cs.fields[:len(cs.fields)]
continue continue
} }
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
} }
} }
// cs.fields = append(cs.fields, cField)
default: default:
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, 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
} }
// cs.fields = append(cs.fields, cField)
}
if !ok {
cs.fields = append(cs.fields, cField)
} }
} }
@ -261,10 +361,14 @@ func (v *Validate) Field(f interface{}, tag string) *FieldError {
// 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) return v.fieldWithNameAndValue(nil, val, f, tag, "", nil)
} }
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, cacheField *cachedField) *FieldError {
// var fieldType reflect.Type
// var fieldKind reflect.Kind
var cField *cachedField
// 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 {
@ -275,39 +379,118 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f
return nil return nil
} }
valueField := reflect.ValueOf(f) if cacheField == nil {
fieldKind := valueField.Kind() valueField := reflect.ValueOf(f)
if fieldKind == reflect.Ptr && !valueField.IsNil() { cField = &cachedField{name: name, kind: valueField.Kind()}
return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), name, tag) // fieldKind = valueField.Kind()
}
fieldType := valueField.Type() if cField.kind == reflect.Ptr && !valueField.IsNil() {
return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, cacheField)
}
switch fieldKind { cField.typ = valueField.Type()
// cField.tags = make([][]string, 0)
// fieldType = valueField.Type()
// for _, t := range strings.Split(tag, tagSeparator) {
// vals := strings.Split(t, tagKeySeparator)
// 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.TrimSpace(vals[1])
// }
// // for vals := range strings.Split(t, tagKeySeparator) {
// cField.tags = append(cField.tags, []string{key, param})
// // }
// // vals := strings.Split(valTag, tagKeySeparator)
// // key := strings.TrimSpace(vals[0])
// }
} else {
cField = cacheField
// fieldType = cacheField.typ
// fieldKind = cacheField.kind
}
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 ValidateFieldWithTag")
} }
} }
if len(cField.tags) == 0 {
for _, t := range strings.Split(tag, tagSeparator) {
orVals := strings.Split(t, orSeparator)
// fmt.Println(len(orVals) - 1)
cTag := &cacheTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))}
cField.tags = append(cField.tags, cTag)
for i, val := range orVals {
vals := strings.Split(val, tagKeySeparator)
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.TrimSpace(vals[1])
}
// fmt.Println(cTag.keyVals)
cTag.keyVals[i] = []string{key, param}
// cTag.keyVals = append(cTag.keyVals, []string{key, param})
// for vals := range strings.Split(t, tagKeySeparator) {
// cField.tags = append(cField.tags, cacheTags{ isOrVal: len(orVals) > 1, []string{key, param})
}
// }
// vals := strings.Split(valTag, tagKeySeparator)
// key := strings.TrimSpace(vals[0])
}
}
// fmt.Println(fieldKind, cacheField.kind)
// switch cField.kind {
// case reflect.Struct, reflect.Interface, reflect.Invalid:
// if cField.typ != reflect.TypeOf(time.Time{}) {
// panic("Invalid field passed to ValidateFieldWithTag")
// }
// }
var valErr *FieldError var valErr *FieldError
var err error var err error
valTags := strings.Split(tag, tagSeparator) // valTags := strings.Split(tag, tagSeparator)
for _, valTag := range valTags { for _, cTag := range cField.tags {
orVals := strings.Split(valTag, orSeparator) if cTag.isOrVal {
if len(orVals) > 1 {
errTag := "" errTag := ""
for _, val := range orVals { for _, val := range cTag.keyVals {
// fmt.Println(cTag)
valErr, err = v.fieldWithNameAndSingleTag(val, current, f, name, val) valErr, err = v.fieldWithNameAndSingleTag(val, current, f, val[0], val[1], name, cacheField)
if err == nil { if err == nil {
return nil return nil
@ -320,31 +503,70 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f
errTag = strings.TrimLeft(errTag, orSeparator) errTag = strings.TrimLeft(errTag, orSeparator)
valErr.Tag = errTag valErr.Tag = errTag
valErr.Kind = fieldKind valErr.Kind = cField.kind
return valErr return valErr
} }
// else {
// fmt.Println(cTag.keyVals[0])
if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, name, valTag); err != nil { if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, cTag.keyVals[0][0], cTag.keyVals[0][1], name, cacheField); err != nil {
valErr.Kind = valueField.Kind() valErr.Kind = cField.kind
valErr.Type = fieldType valErr.Type = cField.typ
return valErr return valErr
} }
// }
// orVals := strings.Split(valTag, orSeparator)
// if len(orVals) > 1 {
// errTag := ""
// for _, val := range orVals {
// valErr, err = v.fieldWithNameAndSingleTag(val, current, f, val, name, cacheField)
// if err == nil {
// return nil
// }
// errTag += orSeparator + valErr.Tag
// }
// errTag = strings.TrimLeft(errTag, orSeparator)
// valErr.Tag = errTag
// valErr.Kind = cField.kind
// return valErr
// }
// if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, valTag, name, cacheField); err != nil {
// valErr.Kind = cField.kind
// valErr.Type = cField.typ
// return valErr
// }
} }
return nil return nil
} }
func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, name string, valTag string) (*FieldError, error) { func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string, cacheField *cachedField) (*FieldError, error) {
vals := strings.Split(valTag, tagKeySeparator) // vals := strings.Split(valTag, tagKeySeparator)
key := strings.TrimSpace(vals[0]) // key := strings.TrimSpace(vals[0])
if len(key) == 0 { // if len(key) == 0 {
panic(fmt.Sprintf("Invalid validation tag on field %s", name)) // panic(fmt.Sprintf("Invalid validation tag on field %s", name))
} // }
valErr := &FieldError{ valErr := &FieldError{
Field: name, Field: name,
@ -363,10 +585,10 @@ 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 := "" // param := ""
if len(vals) > 1 { // if len(vals) > 1 {
param = strings.TrimSpace(vals[1]) // param = strings.TrimSpace(vals[1])
} // }
if err := valFunc(val, current, f, param); !err { if err := valFunc(val, current, f, param); !err {
valErr.Param = param valErr.Param = param

@ -2336,36 +2336,44 @@ func BenchmarkValidateStruct(b *testing.B) {
// Int64Val int64 `bson:"gt=0,lt=10"` // Int64Val int64 `bson:"gt=0,lt=10"`
// } // }
tFail := &TestString{ // tFail := &TestString{
Required: "", // Required: "",
Len: "", // Len: "",
Min: "", // Min: "",
Max: "12345678901", // Max: "12345678901",
MinMax: "", // MinMax: "",
Lt: "0123456789", // Lt: "0123456789",
Lte: "01234567890", // Lte: "01234567890",
Gt: "1", // Gt: "1",
Gte: "1", // Gte: "1",
OmitEmpty: "12345678901", // OmitEmpty: "12345678901",
Sub: &SubTest{ // Sub: &SubTest{
Test: "", // Test: "",
}, // },
Anonymous: struct { // Anonymous: struct {
A string `validate:"required"` // A string `validate:"required"`
}{ // }{
A: "", // A: "",
}, // },
Iface: &Impl{ // Iface: &Impl{
F: "12", // F: "12",
}, // },
} // }
// t := &Test{ // t := &Test{
// StringVal: "test", // StringVal: "test",
// Int64Val: 5, // Int64Val: 5,
// } // }
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++ { for n := 0; n < b.N; n++ {
validate.Struct(tFail) validate.Struct(validFoo)
validate.Struct(invalidFoo)
} }
} }

Loading…
Cancel
Save