From 22d031deb0c479cecb84c020acf4bc48406376e0 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 21:39:21 -0400 Subject: [PATCH 01/52] 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. --- validator.go | 326 ++++++++++++++++++++++++++++++++++++++-------- validator_test.go | 56 ++++---- 2 files changed, 306 insertions(+), 76 deletions(-) diff --git a/validator.go b/validator.go index 0ae7faf..2bf65b5 100644 --- a/validator.go +++ b/validator.go @@ -162,6 +162,31 @@ func (v *Validate) Struct(s interface{}) *StructErrors { 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 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) - 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{ Struct: structName, @@ -184,39 +225,88 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter StructErrors: map[string]*StructErrors{}, } - var numFields = structValue.NumField() - for i := 0; i < numFields; i++ { - valueField := structValue.Field(i) - typeField := structType.Field(i) + var valueField reflect.Value + var cField *cachedField + // var fName string + // var tag string - if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { - valueField = valueField.Elem() - } + var typeField reflect.StructField + + // 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 { - continue + // fName = typeField.Name + 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) - if tag == "" && ((valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface) || valueField.Type() == reflect.TypeOf(time.Time{})) { - continue + // this can happen if the first cache value was nil + // but the second actually has a value + if cField.kind == reflect.Ptr { + cField.kind = valueField.Kind() } - switch valueField.Kind() { + switch cField.kind { 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 } - 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 // free up memory reference fieldError = nil @@ -224,24 +314,34 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } else { - if strings.Contains(tag, structOnlyTag) { + if strings.Contains(cField.tag, structOnlyTag) { + cs.children-- + // cs.fields = cs.fields[:len(cs.fields)] continue } 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 structErrors = nil } } + // cs.fields = append(cs.fields, cField) + 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 // free up memory reference 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 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 if tag == noValidationTag { @@ -275,39 +379,118 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } - valueField := reflect.ValueOf(f) - fieldKind := valueField.Kind() + if cacheField == nil { + valueField := reflect.ValueOf(f) - if fieldKind == reflect.Ptr && !valueField.IsNil() { - return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), name, tag) - } + cField = &cachedField{name: name, kind: valueField.Kind()} + // 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: - if fieldType != reflect.TypeOf(time.Time{}) { + if cField.typ != reflect.TypeOf(time.Time{}) { 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 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 len(orVals) > 1 { + if cTag.isOrVal { 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 { return nil @@ -320,31 +503,70 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f errTag = strings.TrimLeft(errTag, orSeparator) valErr.Tag = errTag - valErr.Kind = fieldKind + valErr.Kind = cField.kind 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.Type = fieldType + valErr.Kind = cField.kind + valErr.Type = cField.typ 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 } -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) - key := strings.TrimSpace(vals[0]) + // vals := strings.Split(valTag, tagKeySeparator) + // key := strings.TrimSpace(vals[0]) - if len(key) == 0 { - panic(fmt.Sprintf("Invalid validation tag on field %s", name)) - } + // if len(key) == 0 { + // panic(fmt.Sprintf("Invalid validation tag on field %s", name)) + // } valErr := &FieldError{ Field: name, @@ -363,10 +585,10 @@ func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{ panic(fmt.Sprintf("Undefined validation function on field %s", name)) } - param := "" - if len(vals) > 1 { - param = strings.TrimSpace(vals[1]) - } + // param := "" + // if len(vals) > 1 { + // param = strings.TrimSpace(vals[1]) + // } if err := valFunc(val, current, f, param); !err { valErr.Param = param diff --git a/validator_test.go b/validator_test.go index ce7c546..9a71573 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2336,36 +2336,44 @@ func BenchmarkValidateStruct(b *testing.B) { // Int64Val int64 `bson:"gt=0,lt=10"` // } - tFail := &TestString{ - Required: "", - Len: "", - Min: "", - Max: "12345678901", - MinMax: "", - Lt: "0123456789", - Lte: "01234567890", - Gt: "1", - Gte: "1", - OmitEmpty: "12345678901", - Sub: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "", - }, - Iface: &Impl{ - F: "12", - }, - } + // tFail := &TestString{ + // Required: "", + // Len: "", + // Min: "", + // Max: "12345678901", + // MinMax: "", + // Lt: "0123456789", + // Lte: "01234567890", + // Gt: "1", + // Gte: "1", + // OmitEmpty: "12345678901", + // Sub: &SubTest{ + // Test: "", + // }, + // Anonymous: struct { + // A string `validate:"required"` + // }{ + // A: "", + // }, + // Iface: &Impl{ + // F: "12", + // }, + // } // t := &Test{ // StringVal: "test", // 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++ { - validate.Struct(tFail) + validate.Struct(validFoo) + validate.Struct(invalidFoo) } } From e4f2ff67bd457930b9d224e9e05f5d7194bae7d2 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 22:24:26 -0400 Subject: [PATCH 02/52] add caching of field only tags, now less time, memory and only 2 allocations vs 9 --- validator.go | 97 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/validator.go b/validator.go index 2bf65b5..bb27feb 100644 --- a/validator.go +++ b/validator.go @@ -306,7 +306,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter cField.isTime = true - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, cField); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference fieldError = nil @@ -331,7 +331,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter default: - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, cField); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference fieldError = nil @@ -361,14 +361,17 @@ 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 func (v *Validate) FieldWithValue(val interface{}, f interface{}, tag string) *FieldError { - return v.fieldWithNameAndValue(nil, val, f, tag, "", nil) + return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil) } -func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f interface{}, tag string, name string, cacheField *cachedField) *FieldError { +var cacheFields = map[string][]*cacheTags{} + +func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f interface{}, tag string, name string, isSingleField bool, cacheField *cachedField) *FieldError { // var fieldType reflect.Type // var fieldKind reflect.Kind var cField *cachedField + var ok bool // This is a double check if coming from validate.Struct but need to be here in case function is called directly if tag == noValidationTag { @@ -382,14 +385,30 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if cacheField == nil { valueField := reflect.ValueOf(f) - cField = &cachedField{name: name, kind: valueField.Kind()} + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + // valueField = valueField.Elem() + // f = valueField.Interface() + return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, isSingleField, cacheField) + } + + // if !ok { + // cacheFields[cField.tag] = cField + + // valueField = valueField.Elem() + + cField = &cachedField{name: name, kind: valueField.Kind(), tag: tag, typ: valueField.Type()} // fieldKind = valueField.Kind() - if cField.kind == reflect.Ptr && !valueField.IsNil() { - return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, cacheField) - } + // if cField.kind == reflect.Ptr && !valueField.IsNil() { + // return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, isSingleField, cacheField) + // } + + // cField.typ = valueField.Type() + // cField.tag = tag - cField.typ = valueField.Type() + // if isSingleField { + // cacheFields[cField.tag] = cField + // } // cField.tags = make([][]string, 0) // fieldType = valueField.Type() // for _, t := range strings.Split(tag, tagSeparator) { @@ -413,6 +432,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f // // vals := strings.Split(valTag, tagKeySeparator) // // key := strings.TrimSpace(vals[0]) // } + // } } else { cField = cacheField // fieldType = cacheField.typ @@ -429,39 +449,52 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } 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) + if isSingleField { + cField.tags, ok = cacheFields[tag] + } - for i, val := range orVals { - vals := strings.Split(val, tagKeySeparator) + if !ok { - key := strings.TrimSpace(vals[0]) + for _, t := range strings.Split(tag, tagSeparator) { - if len(key) == 0 { - panic(fmt.Sprintf("Invalid validation tag on field %s", name)) - } + 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) - param := "" - if len(vals) > 1 { - param = strings.TrimSpace(vals[1]) - } + for i, val := range orVals { + vals := strings.Split(val, tagKeySeparator) + + key := strings.TrimSpace(vals[0]) - // fmt.Println(cTag.keyVals) - cTag.keyVals[i] = []string{key, param} - // cTag.keyVals = append(cTag.keyVals, []string{key, param}) + if len(key) == 0 { + panic(fmt.Sprintf("Invalid validation tag on field %s", name)) + } - // for vals := range strings.Split(t, tagKeySeparator) { - // cField.tags = append(cField.tags, cacheTags{ isOrVal: len(orVals) > 1, []string{key, param}) + 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]) } - // } - // vals := strings.Split(valTag, tagKeySeparator) - // key := strings.TrimSpace(vals[0]) + if isSingleField && !ok { + // fmt.Println(cField.tag) + cacheFields[cField.tag] = cField.tags + } } } From 0c5fbee316f4b8a20156fb1740726615e35dd8fb Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 22:59:36 -0400 Subject: [PATCH 03/52] code cleanup + variable renaming restructure fieldWithNameAndSingleTag for speed + less allocations --- validator.go | 277 +++++++++++---------------------------------------- 1 file changed, 60 insertions(+), 217 deletions(-) diff --git a/validator.go b/validator.go index bb27feb..2471e2a 100644 --- a/validator.go +++ b/validator.go @@ -29,6 +29,31 @@ const ( structErrMsg = "Struct:%s\n" ) +type cachedTags struct { + keyVals [][]string + isOrVal bool +} + +type cachedField struct { + index int + name string + tags []*cachedTags + tag string + kind reflect.Kind + typ reflect.Type + isTime bool +} + +type cachedStruct struct { + children int + name string + kind reflect.Kind + fields []*cachedField +} + +var structCache = map[reflect.Type]*cachedStruct{} +var fieldsCache = map[string][]*cachedTags{} + // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { @@ -162,31 +187,6 @@ func (v *Validate) Struct(s interface{}) *StructErrors { 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 func (v *Validate) structRecursive(top interface{}, current interface{}, s interface{}) *StructErrors { @@ -205,18 +205,16 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter var structName string var numFields int - // fmt.Println(structType) + cs, isCached := structCache[structType] - cs, ok := cache[structType] - - if ok { + if isCached { structName = cs.name numFields = cs.children } else { structName = structType.Name() numFields = structValue.NumField() cs = &cachedStruct{name: structName, children: numFields} - cache[structType] = cs + structCache[structType] = cs } validationErrors := &StructErrors{ @@ -229,26 +227,10 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter var valueField reflect.Value var cField *cachedField - // var fName string - // var tag string - var typeField reflect.StructField - // 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 { + if isCached { cField = cs.fields[i] - // fName = cField.name - // tag = cField.tag valueField = structValue.Field(cField.index) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { @@ -265,8 +247,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName)} - // tag = typeField.Tag.Get(v.tagName) - if cField.tag == noValidationTag { cs.children-- continue @@ -278,12 +258,9 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } - // fName = typeField.Name 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) } // this can happen if the first cache value was nil @@ -298,7 +275,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter if !unicode.IsUpper(rune(cField.name[0])) { cs.children-- - // cs.fields = cs.fields[:len(cs.fields)] continue } @@ -316,7 +292,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter if strings.Contains(cField.tag, structOnlyTag) { cs.children-- - // cs.fields = cs.fields[:len(cs.fields)] continue } @@ -327,8 +302,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } } - // cs.fields = append(cs.fields, cField) - default: if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { @@ -336,11 +309,9 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter // free up memory reference fieldError = nil } - - // cs.fields = append(cs.fields, cField) } - if !ok { + if !isCached { cs.fields = append(cs.fields, cField) } } @@ -364,14 +335,10 @@ func (v *Validate) FieldWithValue(val interface{}, f interface{}, tag string) *F return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil) } -var cacheFields = map[string][]*cacheTags{} - func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f interface{}, tag string, name string, isSingleField bool, cacheField *cachedField) *FieldError { - // var fieldType reflect.Type - // var fieldKind reflect.Kind var cField *cachedField - var ok bool + var isCached bool // This is a double check if coming from validate.Struct but need to be here in case function is called directly if tag == noValidationTag { @@ -386,57 +353,13 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f valueField := reflect.ValueOf(f) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { - // valueField = valueField.Elem() - // f = valueField.Interface() - return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, isSingleField, cacheField) + valueField = valueField.Elem() + f = valueField.Interface() } - // if !ok { - // cacheFields[cField.tag] = cField - - // valueField = valueField.Elem() - cField = &cachedField{name: name, kind: valueField.Kind(), tag: tag, typ: valueField.Type()} - // fieldKind = valueField.Kind() - - // if cField.kind == reflect.Ptr && !valueField.IsNil() { - // return v.fieldWithNameAndValue(val, current, valueField.Elem().Interface(), tag, name, isSingleField, cacheField) - // } - - // cField.typ = valueField.Type() - // cField.tag = tag - - // if isSingleField { - // cacheFields[cField.tag] = cField - // } - // 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 { @@ -451,16 +374,15 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if len(cField.tags) == 0 { if isSingleField { - cField.tags, ok = cacheFields[tag] + cField.tags, isCached = fieldsCache[tag] } - if !ok { + if !isCached { 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))} + cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} cField.tags = append(cField.tags, cTag) for i, val := range orVals { @@ -477,41 +399,18 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f 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]) } - if isSingleField && !ok { - // fmt.Println(cField.tag) - cacheFields[cField.tag] = cField.tags + if isSingleField { + fieldsCache[cField.tag] = cField.tags } } } - // 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 fieldErr *FieldError var err error - // valTags := strings.Split(tag, tagSeparator) for _, cTag := range cField.tags { @@ -521,96 +420,41 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f for _, val := range cTag.keyVals { - // fmt.Println(cTag) - - valErr, err = v.fieldWithNameAndSingleTag(val, current, f, val[0], val[1], name, cacheField) + fieldErr, err = v.fieldWithNameAndSingleTag(val, current, f, val[0], val[1], name) if err == nil { return nil } - errTag += orSeparator + valErr.Tag - + errTag += orSeparator + fieldErr.Tag } errTag = strings.TrimLeft(errTag, orSeparator) - valErr.Tag = errTag - valErr.Kind = cField.kind - - return valErr + fieldErr.Tag = errTag + fieldErr.Kind = cField.kind + fieldErr.Type = cField.typ + return fieldErr } - // else { - // fmt.Println(cTag.keyVals[0]) + if fieldErr, err = v.fieldWithNameAndSingleTag(val, current, f, cTag.keyVals[0][0], cTag.keyVals[0][1], name); err != nil { - if valErr, err = v.fieldWithNameAndSingleTag(val, current, f, cTag.keyVals[0][0], cTag.keyVals[0][1], name, cacheField); err != nil { + fieldErr.Kind = cField.kind + fieldErr.Type = cField.typ - valErr.Kind = cField.kind - valErr.Type = cField.typ - - return valErr + return fieldErr } - // } - - // 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 } -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) - // key := strings.TrimSpace(vals[0]) - - // if len(key) == 0 { - // panic(fmt.Sprintf("Invalid validation tag on field %s", name)) - // } - - valErr := &FieldError{ - Field: name, - Tag: key, - Value: f, - Param: "", - } +func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { // OK to continue because we checked it's existance before getting into this loop if key == omitempty { - return valErr, nil + return nil, nil } valFunc, ok := v.validationFuncs[key] @@ -618,15 +462,14 @@ func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{ panic(fmt.Sprintf("Undefined validation function on field %s", name)) } - // param := "" - // if len(vals) > 1 { - // param = strings.TrimSpace(vals[1]) - // } - - if err := valFunc(val, current, f, param); !err { - valErr.Param = param - return valErr, errors.New(key) + if err := valFunc(val, current, f, param); err { + return nil, nil + } else { + return &FieldError{ + Field: name, + Tag: key, + Value: f, + Param: param, + }, errors.New(key) } - - return valErr, nil } From 8c2248a5cee46f54ec089f7890f6fe885b305225 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 23:22:13 -0400 Subject: [PATCH 04/52] add map thread safety --- validator.go | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/validator.go b/validator.go index 2471e2a..8a85c69 100644 --- a/validator.go +++ b/validator.go @@ -14,6 +14,7 @@ import ( "fmt" "reflect" "strings" + "sync" "time" "unicode" ) @@ -51,8 +52,45 @@ type cachedStruct struct { fields []*cachedField } -var structCache = map[reflect.Type]*cachedStruct{} -var fieldsCache = map[string][]*cachedTags{} +type structsCacheMap struct { + lock sync.RWMutex + m map[reflect.Type]*cachedStruct +} + +func (s *structsCacheMap) Get(key reflect.Type) (*cachedStruct, bool) { + s.lock.RLock() + defer s.lock.RUnlock() + value, ok := s.m[key] + return value, ok +} + +func (s *structsCacheMap) Set(key reflect.Type, value *cachedStruct) { + s.lock.Lock() + defer s.lock.Unlock() + s.m[key] = value +} + +var structCache = &structsCacheMap{m: map[reflect.Type]*cachedStruct{}} + +type fieldsCacheMap struct { + lock sync.RWMutex + m map[string][]*cachedTags +} + +func (s *fieldsCacheMap) Get(key string) ([]*cachedTags, bool) { + s.lock.RLock() + defer s.lock.RUnlock() + value, ok := s.m[key] + return value, ok +} + +func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { + s.lock.Lock() + defer s.lock.Unlock() + s.m[key] = value +} + +var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation @@ -205,7 +243,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter var structName string var numFields int - cs, isCached := structCache[structType] + cs, isCached := structCache.Get(structType) if isCached { structName = cs.name @@ -214,7 +252,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter structName = structType.Name() numFields = structValue.NumField() cs = &cachedStruct{name: structName, children: numFields} - structCache[structType] = cs + structCache.Set(structType, cs) } validationErrors := &StructErrors{ @@ -374,7 +412,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if len(cField.tags) == 0 { if isSingleField { - cField.tags, isCached = fieldsCache[tag] + cField.tags, isCached = fieldsCache.Get(tag) } if !isCached { @@ -404,7 +442,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } if isSingleField { - fieldsCache[cField.tag] = cField.tags + fieldsCache.Set(cField.tag, cField.tags) } } } From 6ffa5d14556c84d53c5b7e3dfe630c3ee1a81ce6 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 23:25:14 -0400 Subject: [PATCH 05/52] add thread safety note to AddFunction and SetTag functions --- validator.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator.go b/validator.go index 8a85c69..020d3c8 100644 --- a/validator.go +++ b/validator.go @@ -195,12 +195,14 @@ func New(tagName string, funcs map[string]Func) *Validate { // SetTag sets tagName of the Validator to one of your choosing after creation // perhaps to dodge a tag name conflict in a specific section of code +// NOTE: this method is not thread-safe func (v *Validate) SetTag(tagName string) { v.tagName = tagName } // AddFunction adds a validation Func to a Validate's map of validators denoted by the key // NOTE: if the key already exists, it will get replaced. +// NOTE: this method is not thread-safe func (v *Validate) AddFunction(key string, f Func) error { if len(key) == 0 { From ad39bb92ddf41a195605ba4710a41401be078bf4 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 23:40:23 -0400 Subject: [PATCH 06/52] code cleanup --- validator.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/validator.go b/validator.go index 020d3c8..186111e 100644 --- a/validator.go +++ b/validator.go @@ -244,8 +244,10 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter var structName string var numFields int + var cs *cachedStruct + var isCached bool - cs, isCached := structCache.Get(structType) + cs, isCached = structCache.Get(structType) if isCached { structName = cs.name From 81ce302975642be612f20717304827de38bebb55 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 23:54:06 -0400 Subject: [PATCH 07/52] add parallel benchmark --- validator_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/validator_test.go b/validator_test.go index 9a71573..aed06a9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2377,3 +2377,21 @@ func BenchmarkValidateStruct(b *testing.B) { validate.Struct(invalidFoo) } } + +func BenchmarkTemplateParallel(b *testing.B) { + + 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} + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } + }) +} From 56b4ce18117f195c19bc6cf94fabb3552e7207c6 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 7 Jun 2015 23:57:32 -0400 Subject: [PATCH 08/52] add larger tests for benchmarks --- validator_test.go | 163 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 40 deletions(-) diff --git a/validator_test.go b/validator_test.go index aed06a9..268ddd6 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2325,45 +2325,8 @@ func BenchmarkValidateField(b *testing.B) { } } -func BenchmarkValidateStruct(b *testing.B) { - - // type Inner struct { - - // } - - // type Test struct { - // StringVal string `bson:"required,lt=10"` - // Int64Val int64 `bson:"gt=0,lt=10"` - // } - - // tFail := &TestString{ - // Required: "", - // Len: "", - // Min: "", - // Max: "12345678901", - // MinMax: "", - // Lt: "0123456789", - // Lte: "01234567890", - // Gt: "1", - // Gte: "1", - // OmitEmpty: "12345678901", - // Sub: &SubTest{ - // Test: "", - // }, - // Anonymous: struct { - // A string `validate:"required"` - // }{ - // A: "", - // }, - // Iface: &Impl{ - // F: "12", - // }, - // } - - // t := &Test{ - // StringVal: "test", - // Int64Val: 5, - // } +func BenchmarkValidateStructSimple(b *testing.B) { + type Foo struct { StringValue string `validate:"min=5,max=10"` IntValue int `validate:"min=5,max=10"` @@ -2378,7 +2341,7 @@ func BenchmarkValidateStruct(b *testing.B) { } } -func BenchmarkTemplateParallel(b *testing.B) { +func BenchmarkTemplateParallelSimple(b *testing.B) { type Foo struct { StringValue string `validate:"min=5,max=10"` @@ -2395,3 +2358,123 @@ func BenchmarkTemplateParallel(b *testing.B) { } }) } + +func BenchmarkValidateStructLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + for n := 0; n < b.N; n++ { + validate.Struct(tSuccess) + validate.Struct(tFail) + } +} + +func BenchmarkTemplateParallelLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(tSuccess) + validate.Struct(tFail) + } + }) +} From aa275c658d5ca985ec1a2019342815f935dce02d Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 08:27:07 -0400 Subject: [PATCH 09/52] merge pul request changes from @xboston --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 1b02c0f..a4cf673 100644 --- a/doc.go +++ b/doc.go @@ -10,7 +10,7 @@ Validate A simple example usage: - type UserDetail { + type UserDetail struct { Details string `validate:"-"` } From 1fda4930f6077581e10362d69c34a7f0bf85dbe9 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 08:37:53 -0400 Subject: [PATCH 10/52] comment out parallel benchmarks to maintain go 1.2 compatibility --- validator_test.go | 148 +++++++++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/validator_test.go b/validator_test.go index 268ddd6..67a34b0 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2341,23 +2341,23 @@ func BenchmarkValidateStructSimple(b *testing.B) { } } -func BenchmarkTemplateParallelSimple(b *testing.B) { +// func BenchmarkTemplateParallelSimple(b *testing.B) { - type Foo struct { - StringValue string `validate:"min=5,max=10"` - IntValue int `validate:"min=5,max=10"` - } +// 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} +// validFoo := &Foo{StringValue: "Foobar", IntValue: 7} +// invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - validate.Struct(validFoo) - validate.Struct(invalidFoo) - } - }) -} +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(validFoo) +// validate.Struct(invalidFoo) +// } +// }) +// } func BenchmarkValidateStructLarge(b *testing.B) { @@ -2418,63 +2418,63 @@ func BenchmarkValidateStructLarge(b *testing.B) { } } -func BenchmarkTemplateParallelLarge(b *testing.B) { - - tFail := &TestString{ - Required: "", - Len: "", - Min: "", - Max: "12345678901", - MinMax: "", - Lt: "0123456789", - Lte: "01234567890", - Gt: "1", - Gte: "1", - OmitEmpty: "12345678901", - Sub: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "", - }, - Iface: &Impl{ - F: "12", - }, - } - - tSuccess := &TestString{ - Required: "Required", - Len: "length==10", - Min: "min=1", - Max: "1234567890", - MinMax: "12345", - Lt: "012345678", - Lte: "0123456789", - Gt: "01234567890", - Gte: "0123456789", - OmitEmpty: "", - Sub: &SubTest{ - Test: "1", - }, - SubIgnore: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "1", - }, - Iface: &Impl{ - F: "123", - }, - } - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - validate.Struct(tSuccess) - validate.Struct(tFail) - } - }) -} +// func BenchmarkTemplateParallelLarge(b *testing.B) { + +// tFail := &TestString{ +// Required: "", +// Len: "", +// Min: "", +// Max: "12345678901", +// MinMax: "", +// Lt: "0123456789", +// Lte: "01234567890", +// Gt: "1", +// Gte: "1", +// OmitEmpty: "12345678901", +// Sub: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "", +// }, +// Iface: &Impl{ +// F: "12", +// }, +// } + +// tSuccess := &TestString{ +// Required: "Required", +// Len: "length==10", +// Min: "min=1", +// Max: "1234567890", +// MinMax: "12345", +// Lt: "012345678", +// Lte: "0123456789", +// Gt: "01234567890", +// Gte: "0123456789", +// OmitEmpty: "", +// Sub: &SubTest{ +// Test: "1", +// }, +// SubIgnore: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "1", +// }, +// Iface: &Impl{ +// F: "123", +// }, +// } + +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(tSuccess) +// validate.Struct(tFail) +// } +// }) +// } From 6ae296cb93a559e918a6ec130f42399f1b87d2d9 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Mon, 8 Jun 2015 09:06:40 -0400 Subject: [PATCH 11/52] Update README.md fix go get documentation, remove the -u option --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c1991a..c1e9d00 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Installation Use go get. - go get -u gopkg.in/bluesuncorp/validator.v5 + go get gopkg.in/bluesuncorp/validator.v5 or to update From 793099be40469358832d394c1a214fdb205d8e7b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 16:45:51 -0400 Subject: [PATCH 12/52] remove trimming of param option as space could be a valid param option or char for #63 --- validator.go | 16 ++++++++-------- validator_test.go | 5 +++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/validator.go b/validator.go index 186111e..c44ad1a 100644 --- a/validator.go +++ b/validator.go @@ -438,7 +438,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f param := "" if len(vals) > 1 { - param = strings.TrimSpace(vals[1]) + param = vals[1] } cTag.keyVals[i] = []string{key, param} @@ -506,12 +506,12 @@ func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{ if err := valFunc(val, current, f, param); err { return nil, nil - } else { - return &FieldError{ - Field: name, - Tag: key, - Value: f, - Param: param, - }, errors.New(key) } + + return &FieldError{ + Field: name, + Tag: key, + Value: f, + Param: param, + }, errors.New(key) } diff --git a/validator_test.go b/validator_test.go index 67a34b0..30adc0a 100644 --- a/validator_test.go +++ b/validator_test.go @@ -276,6 +276,11 @@ func TestExcludesAllValidation(t *testing.T) { t.Fatalf("Index: %d failed Error: %s", i, errs) } } + + username := "joeybloggs " + + err := validate.Field(username, "excludesall=@ ") + NotEqual(t, err, nil) } func TestExcludesValidation(t *testing.T) { From 0fbc3228e88f1433babdd9a3101df370ef705247 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 20:42:16 -0400 Subject: [PATCH 13/52] rework code to allow handling of comma (,) and = within the params i.e. excludesall=,= add test cases for comma and = validation within params add documentation stating how to include a comma within the parameters for #67 --- doc.go | 5 +++++ validator.go | 5 +++-- validator_test.go | 10 ++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index a4cf673..5282cf5 100644 --- a/doc.go +++ b/doc.go @@ -143,6 +143,11 @@ NOTE: Baked In Cross field validation only compares fields on the same struct, if cross field + cross struct validation is needed your own custom validator should be implemented. +NOTE2: comma is the default separator of validation tags, if you wish to have a comma +included within the parameter i.e. excludesall=, you will need to use the UTF-8 hex +representation 0x2C, which is replaced in the code as a comma, so the above will +become excludesall=0x2C + Here is a list of the current built in validators: - diff --git a/validator.go b/validator.go index c44ad1a..8dd373c 100644 --- a/validator.go +++ b/validator.go @@ -20,6 +20,7 @@ import ( ) const ( + utf8HexComma = "0x2C" tagSeparator = "," orSeparator = "|" noValidationTag = "-" @@ -428,7 +429,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f cField.tags = append(cField.tags, cTag) for i, val := range orVals { - vals := strings.Split(val, tagKeySeparator) + vals := strings.SplitN(val, tagKeySeparator, 2) key := strings.TrimSpace(vals[0]) @@ -438,7 +439,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f param := "" if len(vals) > 1 { - param = vals[1] + param = strings.Replace(vals[1], utf8HexComma, ",", -1) } cTag.keyVals[i] = []string{key, param} diff --git a/validator_test.go b/validator_test.go index 30adc0a..b9ff595 100644 --- a/validator_test.go +++ b/validator_test.go @@ -281,6 +281,16 @@ func TestExcludesAllValidation(t *testing.T) { err := validate.Field(username, "excludesall=@ ") NotEqual(t, err, nil) + + excluded := "," + + err = validate.Field(excluded, "excludesall=!@#$%^&*()_+.0x2C?") + NotEqual(t, err, nil) + + excluded = "=" + + err = validate.Field(excluded, "excludesall=!@#$%^&*()_+.0x2C=?") + NotEqual(t, err, nil) } func TestExcludesValidation(t *testing.T) { From fcbf6b65e44e7b9d7fc785e8f21709316b39c534 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 21:27:00 -0400 Subject: [PATCH 14/52] add caching pool of StructErrors to reuse objects, reduce garbage collection and reduce memory allocations for #56 --- validator.go | 63 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/validator.go b/validator.go index 8dd373c..33110f6 100644 --- a/validator.go +++ b/validator.go @@ -31,6 +31,48 @@ const ( structErrMsg = "Struct:%s\n" ) +var structPool *pool + +// Pool holds a channelStructErrors. +type pool struct { + pool chan *StructErrors +} + +// NewPool creates a new pool of Clients. +func newPool(max int) *pool { + return &pool{ + pool: make(chan *StructErrors, max), + } +} + +// Borrow a StructErrors from the pool. +func (p *pool) Borrow() *StructErrors { + var c *StructErrors + + select { + case c = <-p.pool: + default: + c = &StructErrors{ + Errors: map[string]*FieldError{}, + StructErrors: map[string]*StructErrors{}, + } + } + + return c +} + +// Return returns a StructErrors to the pool. +func (p *pool) Return(c *StructErrors) { + + // c.Struct = "" + + select { + case p.pool <- c: + default: + // let it go, let it go... + } +} + type cachedTags struct { keyVals [][]string isOrVal bool @@ -188,6 +230,9 @@ type Validate struct { // New creates a new Validate instance for use. func New(tagName string, funcs map[string]Func) *Validate { + + structPool = newPool(10) + return &Validate{ tagName: tagName, validationFuncs: funcs, @@ -201,6 +246,16 @@ func (v *Validate) SetTag(tagName string) { v.tagName = tagName } +// SetStructPoolMax sets the struct pools max size. this may be usefull for fine grained +// performance tuning towards your application, however, the default should be fine for +// nearly all cases. only increase if you have a deeply nested struct structure. +// NOTE: this method is not thread-safe +// NOTE: this is only here to keep compatibility with v5, in v6 the method will be removed +// and the max pool size will be passed into the New function +func (v *Validate) SetMaxStructPoolSize(max int) { + structPool = newPool(max) +} + // AddFunction adds a validation Func to a Validate's map of validators denoted by the key // NOTE: if the key already exists, it will get replaced. // NOTE: this method is not thread-safe @@ -260,11 +315,8 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter structCache.Set(structType, cs) } - validationErrors := &StructErrors{ - Struct: structName, - Errors: map[string]*FieldError{}, - StructErrors: map[string]*StructErrors{}, - } + validationErrors := structPool.Borrow() + validationErrors.Struct = structName for i := 0; i < numFields; i++ { @@ -360,6 +412,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 { + structPool.Return(validationErrors) return nil } From f4837a073b044868f79c4320b753ca2781b95f29 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 8 Jun 2015 21:46:08 -0400 Subject: [PATCH 15/52] add call to SetMaxStructPoolSize in test to ensure continued 100% test coverage --- validator_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validator_test.go b/validator_test.go index b9ff595..aa8c749 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2044,6 +2044,8 @@ func TestFlattening(t *testing.T) { func TestStructStringValidation(t *testing.T) { + validate.SetMaxStructPoolSize(11) + tSuccess := &TestString{ Required: "Required", Len: "length==10", From 9ff11ae808be91c24d3f10d7219c813dbdb73f10 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 10 Jun 2015 22:03:57 -0400 Subject: [PATCH 16/52] break out benchmarks into separate file create examples file for better godocs --- benchmarks_test.go | 163 +++++++++++++++++++++++++++++++++++++++++++++ examples_test.go | 95 ++++++++++++++++++++++++++ validator.go | 2 +- validator_test.go | 160 -------------------------------------------- 4 files changed, 259 insertions(+), 161 deletions(-) create mode 100644 benchmarks_test.go create mode 100644 examples_test.go diff --git a/benchmarks_test.go b/benchmarks_test.go new file mode 100644 index 0000000..2517209 --- /dev/null +++ b/benchmarks_test.go @@ -0,0 +1,163 @@ +package validator + +import "testing" + +func BenchmarkValidateField(b *testing.B) { + for n := 0; n < b.N; n++ { + validate.Field("1", "len=1") + } +} + +func BenchmarkValidateStructSimple(b *testing.B) { + + 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++ { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } +} + +// func BenchmarkTemplateParallelSimple(b *testing.B) { + +// 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} + +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(validFoo) +// validate.Struct(invalidFoo) +// } +// }) +// } + +func BenchmarkValidateStructLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + for n := 0; n < b.N; n++ { + validate.Struct(tSuccess) + validate.Struct(tFail) + } +} + +// func BenchmarkTemplateParallelLarge(b *testing.B) { + +// tFail := &TestString{ +// Required: "", +// Len: "", +// Min: "", +// Max: "12345678901", +// MinMax: "", +// Lt: "0123456789", +// Lte: "01234567890", +// Gt: "1", +// Gte: "1", +// OmitEmpty: "12345678901", +// Sub: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "", +// }, +// Iface: &Impl{ +// F: "12", +// }, +// } + +// tSuccess := &TestString{ +// Required: "Required", +// Len: "length==10", +// Min: "min=1", +// Max: "1234567890", +// MinMax: "12345", +// Lt: "012345678", +// Lte: "0123456789", +// Gt: "01234567890", +// Gte: "0123456789", +// OmitEmpty: "", +// Sub: &SubTest{ +// Test: "1", +// }, +// SubIgnore: &SubTest{ +// Test: "", +// }, +// Anonymous: struct { +// A string `validate:"required"` +// }{ +// A: "1", +// }, +// Iface: &Impl{ +// F: "123", +// }, +// } + +// b.RunParallel(func(pb *testing.PB) { +// for pb.Next() { +// validate.Struct(tSuccess) +// validate.Struct(tFail) +// } +// }) +// } diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..c5dd351 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,95 @@ +package validator_test + +import ( + "fmt" + + "../validator" +) + +func ExampleValidate_new() { + validator.New("validate", validator.BakedInValidators) +} + +func ExampleValidate_addFunction() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + fn := func(top interface{}, current interface{}, field interface{}, param string) bool { + return field.(string) == "hello" + } + + validate.AddFunction("valueishello", fn) + + message := "hello" + err := validate.Field(message, "valueishello") + fmt.Println(err) + //Output: + // +} + +func ExampleValidate_field() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + i := 0 + err := validate.Field(i, "gt=1,lte=10") + fmt.Println(err.Field) + fmt.Println(err.Tag) + fmt.Println(err.Kind) // NOTE: Kind and Type can be different i.e. time Kind=struct and Type=time.Time + fmt.Println(err.Type) + fmt.Println(err.Param) + fmt.Println(err.Value) + //Output: + // + //gt + //int + //int + //1 + //0 +} + +func ExampleValidate_struct() { + // This should be stored somewhere globally + var validate *validator.Validate + + validate = validator.New("validate", validator.BakedInValidators) + + type ContactInformation struct { + Phone string `validate:"required"` + Street string `validate:"required"` + City string `validate:"required"` + } + + type User struct { + Name string `validate:"required,excludesall=!@#$%^&*()_+-=:;?/0x2C"` // 0x2C = comma (,) + Age int8 `validate:"required,gt=0,lt=150"` + Email string `validate:"email"` + ContactInformation []*ContactInformation + } + + contactInfo := &ContactInformation{ + Street: "26 Here Blvd.", + City: "Paradeso", + } + + user := &User{ + Name: "Joey Bloggs", + Age: 31, + Email: "joeybloggs@gmail.com", + ContactInformation: []*ContactInformation{contactInfo}, + } + + structError := validate.Struct(user) + for _, fieldError := range structError.Errors { + fmt.Println(fieldError.Field) // Phone + fmt.Println(fieldError.Tag) // required + //... and so forth + //Output: + //Phone + //required + } +} diff --git a/validator.go b/validator.go index 33110f6..5f0c18a 100644 --- a/validator.go +++ b/validator.go @@ -246,7 +246,7 @@ func (v *Validate) SetTag(tagName string) { v.tagName = tagName } -// SetStructPoolMax sets the struct pools max size. this may be usefull for fine grained +// SetMaxStructPoolSize sets the struct pools max size. this may be usefull for fine grained // performance tuning towards your application, however, the default should be fine for // nearly all cases. only increase if you have a deeply nested struct structure. // NOTE: this method is not thread-safe diff --git a/validator_test.go b/validator_test.go index aa8c749..5828c2e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -2335,163 +2335,3 @@ func TestInvalidValidatorFunction(t *testing.T) { PanicMatches(t, func() { validate.Field(s.Test, "zzxxBadFunction") }, fmt.Sprintf("Undefined validation function on field %s", "")) } - -func BenchmarkValidateField(b *testing.B) { - for n := 0; n < b.N; n++ { - validate.Field("1", "len=1") - } -} - -func BenchmarkValidateStructSimple(b *testing.B) { - - 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++ { - validate.Struct(validFoo) - validate.Struct(invalidFoo) - } -} - -// func BenchmarkTemplateParallelSimple(b *testing.B) { - -// 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} - -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(validFoo) -// validate.Struct(invalidFoo) -// } -// }) -// } - -func BenchmarkValidateStructLarge(b *testing.B) { - - tFail := &TestString{ - Required: "", - Len: "", - Min: "", - Max: "12345678901", - MinMax: "", - Lt: "0123456789", - Lte: "01234567890", - Gt: "1", - Gte: "1", - OmitEmpty: "12345678901", - Sub: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "", - }, - Iface: &Impl{ - F: "12", - }, - } - - tSuccess := &TestString{ - Required: "Required", - Len: "length==10", - Min: "min=1", - Max: "1234567890", - MinMax: "12345", - Lt: "012345678", - Lte: "0123456789", - Gt: "01234567890", - Gte: "0123456789", - OmitEmpty: "", - Sub: &SubTest{ - Test: "1", - }, - SubIgnore: &SubTest{ - Test: "", - }, - Anonymous: struct { - A string `validate:"required"` - }{ - A: "1", - }, - Iface: &Impl{ - F: "123", - }, - } - - for n := 0; n < b.N; n++ { - validate.Struct(tSuccess) - validate.Struct(tFail) - } -} - -// func BenchmarkTemplateParallelLarge(b *testing.B) { - -// tFail := &TestString{ -// Required: "", -// Len: "", -// Min: "", -// Max: "12345678901", -// MinMax: "", -// Lt: "0123456789", -// Lte: "01234567890", -// Gt: "1", -// Gte: "1", -// OmitEmpty: "12345678901", -// Sub: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "", -// }, -// Iface: &Impl{ -// F: "12", -// }, -// } - -// tSuccess := &TestString{ -// Required: "Required", -// Len: "length==10", -// Min: "min=1", -// Max: "1234567890", -// MinMax: "12345", -// Lt: "012345678", -// Lte: "0123456789", -// Gt: "01234567890", -// Gte: "0123456789", -// OmitEmpty: "", -// Sub: &SubTest{ -// Test: "1", -// }, -// SubIgnore: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "1", -// }, -// Iface: &Impl{ -// F: "123", -// }, -// } - -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(tSuccess) -// validate.Struct(tFail) -// } -// }) -// } From 5db5165e2c8b9b564f41ae9b55b853b9c8c2f5c4 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 22:07:30 -0400 Subject: [PATCH 17/52] update rub and reba regex rejects now properly test for RGB 255 and RGB using percentages, before it allowed mixing of percentages or numbers between 0-255 but it's either or now. --- regexes.go | 4 ++-- validator_test.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/regexes.go b/regexes.go index e5ed0fa..5ca45e9 100644 --- a/regexes.go +++ b/regexes.go @@ -9,8 +9,8 @@ const ( numberRegexString = "^[0-9]+$" hexadecimalRegexString = "^[0-9a-fA-F]+$" hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" - rgbRegexString = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$" - rgbaRegexString = "^rgba\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" + rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" + rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" hslRegexString = "^hsl\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*\\)$" hslaRegexString = "^hsla\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" emailRegexString = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" diff --git a/validator_test.go b/validator_test.go index 5828c2e..7536527 100644 --- a/validator_test.go +++ b/validator_test.go @@ -1736,10 +1736,19 @@ func TestRgba(t *testing.T) { err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55%,100%,0.12)" + err = validate.Field(s, "rgba") + Equal(t, err, nil) + s = "rgba( 0, 31, 255, 0.5)" err = validate.Field(s, "rgba") Equal(t, err, nil) + s = "rgba(12%,55,100%,0.12)" + err = validate.Field(s, "rgba") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgba") + s = "rgb(0, 31, 255)" err = validate.Field(s, "rgba") NotEqual(t, err, nil) @@ -1769,6 +1778,15 @@ func TestRgb(t *testing.T) { err = validate.Field(s, "rgb") Equal(t, err, nil) + s = "rgb(10%, 50%, 100%)" + err = validate.Field(s, "rgb") + Equal(t, err, nil) + + s = "rgb(10%, 50%, 55)" + err = validate.Field(s, "rgb") + NotEqual(t, err, nil) + Equal(t, err.Tag, "rgb") + s = "rgb(1,349,275)" err = validate.Field(s, "rgb") NotEqual(t, err, nil) From 05e0fe1f85065bbfbee4af1d0a8f966328246882 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 22:12:36 -0400 Subject: [PATCH 18/52] updated various regex's to remove any capturing groups --- regexes.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/regexes.go b/regexes.go index 5ca45e9..31c1c68 100644 --- a/regexes.go +++ b/regexes.go @@ -11,9 +11,9 @@ const ( hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" - hslRegexString = "^hsl\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*\\)$" - hslaRegexString = "^hsla\\(\\s*(0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0|[1-9]\\d?|100)%)\\s*,\\s*((0.[1-9]*)|[01])\\s*\\)$" - emailRegexString = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" + hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" ) From 20d9b7909fbb2b144b958358b55d768492f38a14 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Tue, 16 Jun 2015 23:22:36 -0400 Subject: [PATCH 19/52] add isbn, isbn10 and isbn validators + tests + documentation --- baked_in.go | 59 ++++++++++++++++++++++++++++ doc.go | 12 ++++++ regexes.go | 4 ++ validator_test.go | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) diff --git a/baked_in.go b/baked_in.go index ed69eb3..50c0c19 100644 --- a/baked_in.go +++ b/baked_in.go @@ -50,6 +50,65 @@ var BakedInValidators = map[string]Func{ "excludes": excludes, "excludesall": excludesAll, "excludesrune": excludesRune, + "isbn": isISBN, + "isbn10": isISBN10, + "isbn13": isISBN13, +} + +func isISBN(top interface{}, current interface{}, field interface{}, param string) bool { + return isISBN10(top, current, field, param) || isISBN13(top, current, field, param) +} + +func isISBN13(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 4), " ", "", 4) + + if !matchesRegex(iSBN13Regex, s) { + return false + } + + var checksum int32 + var i int32 + + factor := []int32{1, 3} + + for i = 0; i < 12; i++ { + checksum += factor[i%2] * int32(s[i]-'0') + } + + if (int32(s[12]-'0'))-((10-(checksum%10))%10) == 0 { + return true + } + + return false +} + +func isISBN10(top interface{}, current interface{}, field interface{}, param string) bool { + + s := strings.Replace(strings.Replace(field.(string), "-", "", 3), " ", "", 3) + + if !matchesRegex(iSBN10Regex, s) { + return false + } + + var checksum int32 + var i int32 + + for i = 0; i < 9; i++ { + checksum += (i + 1) * int32(s[i]-'0') + } + + if s[9] == 'X' { + checksum += 10 * 10 + } else { + checksum += 10 * int32(s[9]-'0') + } + + if checksum%11 == 0 { + return true + } + + return false } func excludesRune(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 5282cf5..55d325d 100644 --- a/doc.go +++ b/doc.go @@ -362,6 +362,18 @@ Here is a list of the current built in validators: This validates that a string value does not contain the supplied rune value. (Usage: excludesrune=@) + isbn + This validates that a string value contains a valid isbn10 or isbn13 value. + (Usage: isbn) + + isbn10 + This validates that a string value contains a valid isbn10 value. + (Usage: isbn10) + + isbn13 + This validates that a string value contains a valid isbn13 value. + (Usage: isbn13) + Validator notes: regex diff --git a/regexes.go b/regexes.go index 31c1c68..984ffbd 100644 --- a/regexes.go +++ b/regexes.go @@ -15,6 +15,8 @@ const ( hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" + iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" + iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" ) var ( @@ -30,6 +32,8 @@ var ( hslaRegex = regexp.MustCompile(hslaRegexString) emailRegex = regexp.MustCompile(emailRegexString) base64Regex = regexp.MustCompile(base64RegexString) + iSBN10Regex = regexp.MustCompile(iSBN10RegexString) + iSBN13Regex = regexp.MustCompile(iSBN13RegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index 7536527..dc0a1e3 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,104 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestISBNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn") { + t.Fatalf("Index: %d ISBN failed Error: %s", i, err) + } + } + } +} + +func TestISBN13Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3-8362-2119-5", false}, + {"01234567890ab", false}, + {"978 3 8362 2119 0", false}, + {"9784873113685", true}, + {"978-4-87311-368-5", true}, + {"978 3401013190", true}, + {"978-3-8362-2119-1", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn13") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn13") { + t.Fatalf("Index: %d ISBN13 failed Error: %s", i, err) + } + } + } +} + +func TestISBN10Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"foo", false}, + {"3423214121", false}, + {"978-3836221191", false}, + {"3-423-21412-1", false}, + {"3 423 21412 1", false}, + {"3836221195", true}, + {"1-61729-085-8", true}, + {"3 423 21412 0", true}, + {"3 401 01319 X", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "isbn10") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "isbn10") { + t.Fatalf("Index: %d ISBN10 failed Error: %s", i, err) + } + } + } +} + func TestExcludesRuneValidation(t *testing.T) { tests := []struct { From 7aa70841bcfc0a4b9ba8f0a7c182c72bef89878e Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 08:03:31 -0400 Subject: [PATCH 20/52] add uuid, uuid3, uuid4 and uuid5 validators + tests + documentation --- baked_in.go | 20 ++++++++ doc.go | 16 +++++++ regexes.go | 8 ++++ validator_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+) diff --git a/baked_in.go b/baked_in.go index 50c0c19..da5933e 100644 --- a/baked_in.go +++ b/baked_in.go @@ -53,6 +53,26 @@ var BakedInValidators = map[string]Func{ "isbn": isISBN, "isbn10": isISBN10, "isbn13": isISBN13, + "uuid": isUUID, + "uuid3": isUUID3, + "uuid4": isUUID4, + "uuid5": isUUID5, +} + +func isUUID5(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID5Regex, field) +} + +func isUUID4(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID4Regex, field) +} + +func isUUID3(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUID3Regex, field) +} + +func isUUID(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(uUIDRegex, field) } func isISBN(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 55d325d..5c8b44b 100644 --- a/doc.go +++ b/doc.go @@ -374,6 +374,22 @@ Here is a list of the current built in validators: This validates that a string value contains a valid isbn13 value. (Usage: isbn13) + uuid + This validates that a string value contains a valid UUID. + (Usage: uuid) + + uuid3 + This validates that a string value contains a valid version 3 UUID. + (Usage: uuid3) + + uuid4 + This validates that a string value contains a valid version 4 UUID. + (Usage: uuid4) + + uuid5 + This validates that a string value contains a valid version 5 UUID. + (Usage: uuid5) + Validator notes: regex diff --git a/regexes.go b/regexes.go index 984ffbd..d8c22cc 100644 --- a/regexes.go +++ b/regexes.go @@ -17,6 +17,10 @@ const ( base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" + uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" + uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" ) var ( @@ -34,6 +38,10 @@ var ( base64Regex = regexp.MustCompile(base64RegexString) iSBN10Regex = regexp.MustCompile(iSBN10RegexString) iSBN13Regex = regexp.MustCompile(iSBN13RegexString) + uUID3Regex = regexp.MustCompile(uUID3RegexString) + uUID4Regex = regexp.MustCompile(uUID4RegexString) + uUID5Regex = regexp.MustCompile(uUID5RegexString) + uUIDRegex = regexp.MustCompile(uUIDRegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index dc0a1e3..ba7eb5c 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,124 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestUUID5Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"9c858901-8a57-4791-81fe-4c455b099bc9", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"987fbc97-4bed-5078-af07-9141ba07c9f3", true}, + {"987fbc97-4bed-5078-9f07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid5") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid5") { + t.Fatalf("Index: %d UUID5 failed Error: %s", i, err) + } + } + } +} + +func TestUUID4Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-5078-af07-9141ba07c9f3", false}, + {"934859", false}, + {"57b73598-8764-4ad0-a76a-679bb6640eb1", true}, + {"625e63f3-58f5-40b7-83a1-a72ad31acffb", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid4") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid4") { + t.Fatalf("Index: %d UUID4 failed Error: %s", i, err) + } + } + } +} + +func TestUUID3Validation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"412452646", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-4078-8f07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid3") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid3") { + t.Fatalf("Index: %d UUID3 failed Error: %s", i, err) + } + } + } +} + +func TestUUIDValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"xxxa987fbc9-4bed-3078-cf07-9141ba07c9f3", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3xxx", false}, + {"a987fbc94bed3078cf079141ba07c9f3", false}, + {"934859", false}, + {"987fbc9-4bed-3078-cf07a-9141ba07c9f3", false}, + {"aaaaaaaa-1111-1111-aaag-111111111111", false}, + {"a987fbc9-4bed-3078-cf07-9141ba07c9f3", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "uuid") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "uuid") { + t.Fatalf("Index: %d UUID failed Error: %s", i, err) + } + } + } +} + func TestISBNValidation(t *testing.T) { tests := []struct { param string From 35aff710e438d75e5667968d892818f0bae165db Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 09:02:26 -0400 Subject: [PATCH 21/52] added many new validator + tests + documentation: ascii printascii multibyte datauri latitude longitude ssn --- baked_in.go | 56 ++++++++++++ doc.go | 34 ++++++- regexes.go | 86 ++++++++++-------- validator_test.go | 220 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 37 deletions(-) diff --git a/baked_in.go b/baked_in.go index da5933e..22746ad 100644 --- a/baked_in.go +++ b/baked_in.go @@ -57,6 +57,62 @@ var BakedInValidators = map[string]Func{ "uuid3": isUUID3, "uuid4": isUUID4, "uuid5": isUUID5, + "ascii": isASCII, + "printascii": isPrintableASCII, + "multibyte": hasMultiByteCharacter, + "datauri": isDataURI, + "latitude": isLatitude, + "longitude": isLongitude, + "ssn": isSSN, +} + +func isSSN(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) != 11 { + return false + } + + return matchesRegex(sSNRegex, field) +} + +func isLongitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(longitudeRegex, field) +} + +func isLatitude(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(latitudeRegex, field) +} + +func isDataURI(top interface{}, current interface{}, field interface{}, param string) bool { + + uri := strings.SplitN(field.(string), ",", 2) + + if len(uri) != 2 { + return false + } + + if !matchesRegex(dataURIRegex, uri[0]) { + return false + } + + return isBase64(top, current, uri[1], param) +} + +func hasMultiByteCharacter(top interface{}, current interface{}, field interface{}, param string) bool { + + if len(field.(string)) == 0 { + return true + } + + return matchesRegex(multibyteRegex, field) +} + +func isPrintableASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(printableASCIIRegex, field) +} + +func isASCII(top interface{}, current interface{}, field interface{}, param string) bool { + return matchesRegex(aSCIIRegex, field) } func isUUID5(top interface{}, current interface{}, field interface{}, param string) bool { diff --git a/doc.go b/doc.go index 5c8b44b..89142e0 100644 --- a/doc.go +++ b/doc.go @@ -168,7 +168,7 @@ Here is a list of the current built in validators: verify it has been assigned. omitempty - Allows conitional validation, for example if a field is not set with + Allows conditional validation, for example if a field is not set with a value (Determined by the required validator) then other validation such as min or max won't run, but if a value is set validation will run. (Usage: omitempty) @@ -390,6 +390,38 @@ Here is a list of the current built in validators: This validates that a string value contains a valid version 5 UUID. (Usage: uuid5) + ascii + This validates that a string value contains only ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: ascii) + + asciiprint + This validates that a string value contains only printable ASCII characters. + NOTE: if the string is blank, this validates as true. + (Usage: asciiprint) + + multibyte + This validates that a string value contains one or more multibyte characters. + NOTE: if the string is blank, this validates as true. + (Usage: multibyte) + + datauri + This validates that a string value contains a valid DataURI. + NOTE: this will also validate that the data portion is valid base64 + (Usage: datauri) + + latitude + This validates that a string value contains a valid latitude. + (Usage: latitude) + + longitude + This validates that a string value contains a valid longitude. + (Usage: longitude) + + ssn + This validates that a string value contains a valid U.S. Social Security Number. + (Usage: ssn) + Validator notes: regex diff --git a/regexes.go b/regexes.go index d8c22cc..d3e8d80 100644 --- a/regexes.go +++ b/regexes.go @@ -3,45 +3,59 @@ package validator import "regexp" const ( - alphaRegexString = "^[a-zA-Z]+$" - alphaNumericRegexString = "^[a-zA-Z0-9]+$" - numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" - numberRegexString = "^[0-9]+$" - hexadecimalRegexString = "^[0-9a-fA-F]+$" - hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" - rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" - rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" - hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" - hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" - emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" - base64RegexString = "(?:^(?:[A-Za-z0-9+\\/]{4}\\n?)*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)$)" - iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" - iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" - uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" - uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" - uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + alphaRegexString = "^[a-zA-Z]+$" + alphaNumericRegexString = "^[a-zA-Z0-9]+$" + numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$" + numberRegexString = "^[0-9]+$" + hexadecimalRegexString = "^[0-9a-fA-F]+$" + hexcolorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$" + rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$" + rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$" + hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$" + emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:\\(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22)))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$" + iSBN10RegexString = "^(?:[0-9]{9}X|[0-9]{10})$" + iSBN13RegexString = "^(?:(?:97(?:8|9))[0-9]{10})$" + uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" + uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + aSCIIRegexString = "^[\x00-\x7F]*$" + printableASCIIRegexString = "^[\x20-\x7E]*$" + multibyteRegexString = "[^\x00-\x7F]" + dataURIRegexString = "^data:.+\\/(.+);base64$" + latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$" + longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$" + sSNRegexString = `^\d{3}[- ]?\d{2}[- ]?\d{4}$` ) var ( - alphaRegex = regexp.MustCompile(alphaRegexString) - alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) - numericRegex = regexp.MustCompile(numericRegexString) - numberRegex = regexp.MustCompile(numberRegexString) - hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) - hexcolorRegex = regexp.MustCompile(hexcolorRegexString) - rgbRegex = regexp.MustCompile(rgbRegexString) - rgbaRegex = regexp.MustCompile(rgbaRegexString) - hslRegex = regexp.MustCompile(hslRegexString) - hslaRegex = regexp.MustCompile(hslaRegexString) - emailRegex = regexp.MustCompile(emailRegexString) - base64Regex = regexp.MustCompile(base64RegexString) - iSBN10Regex = regexp.MustCompile(iSBN10RegexString) - iSBN13Regex = regexp.MustCompile(iSBN13RegexString) - uUID3Regex = regexp.MustCompile(uUID3RegexString) - uUID4Regex = regexp.MustCompile(uUID4RegexString) - uUID5Regex = regexp.MustCompile(uUID5RegexString) - uUIDRegex = regexp.MustCompile(uUIDRegexString) + alphaRegex = regexp.MustCompile(alphaRegexString) + alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString) + numericRegex = regexp.MustCompile(numericRegexString) + numberRegex = regexp.MustCompile(numberRegexString) + hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString) + hexcolorRegex = regexp.MustCompile(hexcolorRegexString) + rgbRegex = regexp.MustCompile(rgbRegexString) + rgbaRegex = regexp.MustCompile(rgbaRegexString) + hslRegex = regexp.MustCompile(hslRegexString) + hslaRegex = regexp.MustCompile(hslaRegexString) + emailRegex = regexp.MustCompile(emailRegexString) + base64Regex = regexp.MustCompile(base64RegexString) + iSBN10Regex = regexp.MustCompile(iSBN10RegexString) + iSBN13Regex = regexp.MustCompile(iSBN13RegexString) + uUID3Regex = regexp.MustCompile(uUID3RegexString) + uUID4Regex = regexp.MustCompile(uUID4RegexString) + uUID5Regex = regexp.MustCompile(uUID5RegexString) + uUIDRegex = regexp.MustCompile(uUIDRegexString) + aSCIIRegex = regexp.MustCompile(aSCIIRegexString) + printableASCIIRegex = regexp.MustCompile(printableASCIIRegexString) + multibyteRegex = regexp.MustCompile(multibyteRegexString) + dataURIRegex = regexp.MustCompile(dataURIRegexString) + latitudeRegex = regexp.MustCompile(latitudeRegexString) + longitudeRegex = regexp.MustCompile(longitudeRegexString) + sSNRegex = regexp.MustCompile(sSNRegexString) ) func matchesRegex(regex *regexp.Regexp, field interface{}) bool { diff --git a/validator_test.go b/validator_test.go index ba7eb5c..c9da1e4 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,226 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestSSNValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"00-90-8787", false}, + {"66690-76", false}, + {"191 60 2869", true}, + {"191-60-2869", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "ssn") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ssn") { + t.Fatalf("Index: %d SSN failed Error: %s", i, err) + } + } + } +} + +func TestLongitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-180.000", true}, + {"180.1", false}, + {"+73.234", true}, + {"+382.3811", false}, + {"23.11111111", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "longitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "longitude") { + t.Fatalf("Index: %d Longitude failed Error: %s", i, err) + } + } + } +} + +func TestLatitudeValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", false}, + {"-90.000", true}, + {"+90", true}, + {"47.1231231", true}, + {"+99.9", false}, + {"108", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "latitude") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "latitude") { + t.Fatalf("Index: %d Latitude failed Error: %s", i, err) + } + } + } +} + +func TestDataURIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"data:image/png;base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdC4=", true}, + {"data:text/plain;base64,Vml2YW11cyBmZXJtZW50dW0gc2VtcGVyIHBvcnRhLg==", true}, + {"image/gif;base64,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + {"data:image/gif;base64,MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuMPNS1Ufof9EW/M98FNw" + + "UAKrwflsqVxaxQjBQnHQmiI7Vac40t8x7pIb8gLGV6wL7sBTJiPovJ0V7y7oc0Ye" + + "rhKh0Rm4skP2z/jHwwZICgGzBvA0rH8xlhUiTvcwDCJ0kc+fh35hNt8srZQM4619" + + "FTgB66Xmp4EtVyhpQV+t02g6NzK72oZI0vnAvqhpkxLeLiMCyrI416wHm5Tkukhx" + + "QmcL2a6hNOyu0ixX/x2kSFXApEnVrJ+/IxGyfyw8kf4N2IZpW5nEP847lpfj0SZZ" + + "Fwrd1mnfnDbYohX2zRptLy2ZUn06Qo9pkG5ntvFEPo9bfZeULtjYzIl6K8gJ2uGZ" + "HQIDAQAB", true}, + {"data:image/png;base64,12345", false}, + {"", false}, + {"data:text,:;base85,U3VzcGVuZGlzc2UgbGVjdHVzIGxlbw==", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "datauri") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "datauri") { + t.Fatalf("Index: %d DataURI failed Error: %s", i, err) + } + } + } +} + +func TestMultibyteValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"abc", false}, + {"123", false}, + {"<>@;.-=", false}, + {"ひらがな・カタカナ、.漢字", true}, + {"あいうえお foobar", true}, + {"test@example.com", true}, + {"test@example.com", true}, + {"1234abcDExyz", true}, + {"カタカナ", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "multibyte") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "multibyte") { + t.Fatalf("Index: %d Multibyte failed Error: %s", i, err) + } + } + } +} + +func TestPrintableASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"newline\n", false}, + {"\x19test\x7F", false}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "printascii") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "printascii") { + t.Fatalf("Index: %d Printable ASCII failed Error: %s", i, err) + } + } + } +} + +func TestASCIIValidation(t *testing.T) { + tests := []struct { + param string + expected bool + }{ + {"", true}, + {"foobar", false}, + {"xyz098", false}, + {"123456", false}, + {"カタカナ", false}, + {"foobar", true}, + {"0987654321", true}, + {"test@example.com", true}, + {"1234abcDEF", true}, + {"", true}, + } + + for i, test := range tests { + + err := validate.Field(test.param, "ascii") + + if test.expected == true { + if !IsEqual(t, err, nil) { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } else { + if IsEqual(t, err, nil) || !IsEqual(t, err.Tag, "ascii") { + t.Fatalf("Index: %d ASCII failed Error: %s", i, err) + } + } + } +} + func TestUUID5Validation(t *testing.T) { tests := []struct { param string From 63a3b7e6ad8e6057986caebbc78f6a7439a9d9cf Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Wed, 17 Jun 2015 09:38:48 -0400 Subject: [PATCH 22/52] add multibyte test case for blank string to maintain 100% test coverage --- validator_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/validator_test.go b/validator_test.go index c9da1e4..a2d5394 100644 --- a/validator_test.go +++ b/validator_test.go @@ -352,6 +352,7 @@ func TestMultibyteValidation(t *testing.T) { param string expected bool }{ + {"", true}, {"abc", false}, {"123", false}, {"<>@;.-=", false}, From e0bfa17b226425fcca3d08709cf3f4ab6059de60 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 20 Jun 2015 10:04:55 -0400 Subject: [PATCH 23/52] add initial dive logic --- validator.go | 68 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/validator.go b/validator.go index 5f0c18a..5e2196c 100644 --- a/validator.go +++ b/validator.go @@ -28,6 +28,8 @@ const ( structOnlyTag = "structonly" omitempty = "omitempty" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" + mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" ) @@ -64,8 +66,6 @@ func (p *pool) Borrow() *StructErrors { // Return returns a StructErrors to the pool. func (p *pool) Return(c *StructErrors) { - // c.Struct = "" - select { case p.pool <- c: default: @@ -135,15 +135,62 @@ func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} +// // SliceError contains a fields error for a single index within an array or slice +// // NOTE: library only checks the first dimension of the array so if you have a multidimensional +// // array [][]string that validations after the "dive" tag are applied to []string not each +// // string within it. It is not a dificulty with traversing the chain, but how to add validations +// // to what dimension of an array and even how to report on them in any meaningful fashion. +// type SliceError struct { +// Index uint64 +// Field string +// Tag string +// Kind reflect.Kind +// Type reflect.Type +// Param string +// Value interface{} +// } + +// // This is intended for use in development + debugging and not intended to be a production error message. +// // it also allows SliceError to be used as an Error interface +// func (e *SliceError) Error() string { +// return fmt.Sprintf(sliceErrMsg, e.Field, e.Index, e.Tag) +// } + +// // MapError contains a fields error for a single key within a map +// // NOTE: library only checks the first dimension of the array so if you have a multidimensional +// // array [][]string that validations after the "dive" tag are applied to []string not each +// // string within it. It is not a dificulty with traversing the chain, but how to add validations +// // to what dimension of an array and even how to report on them in any meaningful fashion. +// type MapError struct { +// Key interface{} +// Field string +// Tag string +// Kind reflect.Kind +// Type reflect.Type +// Param string +// Value interface{} +// } + +// // This is intended for use in development + debugging and not intended to be a production error message. +// // it also allows MapError to be used as an Error interface +// func (e *MapError) Error() string { +// return fmt.Sprintf(mapErrMsg, e.Field, e.Key, e.Tag) +// } + // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { - Field string - Tag string - Kind reflect.Kind - Type reflect.Type - Param string - Value interface{} + Field string + Tag string + Kind reflect.Kind + Type reflect.Type + Param string + Value interface{} + IsSliceOrArrayError bool + IsMapError bool + Key interface{} + Index uint64 + DiveErrors []*error // counld be FieldError, StructErrors } // This is intended for use in development + debugging and not intended to be a production error message. @@ -162,6 +209,11 @@ type StructErrors struct { // Struct Fields of type struct and their errors // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors + + IsSliceOrArrayError bool + IsMapError bool + Key interface{} + Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. From e0e86490bf9755a3870d5b80f19707471017bf3b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 20 Jun 2015 11:56:16 -0400 Subject: [PATCH 24/52] Fix Issue with nested struct as pointer being nil for #79 --- validator.go | 19 +++++++++ validator_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/validator.go b/validator.go index 5f0c18a..278dda9 100644 --- a/validator.go +++ b/validator.go @@ -27,6 +27,7 @@ const ( tagKeySeparator = "=" structOnlyTag = "structonly" omitempty = "omitempty" + required = "required" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" ) @@ -390,6 +391,24 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } + if valueField.Kind() == reflect.Ptr && valueField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + validationErrors.Errors[cField.name] = &FieldError{ + Field: cField.name, + Tag: required, + Value: valueField.Interface(), + } + + continue + } + } + if structErrors := v.structRecursive(top, valueField.Interface(), valueField.Interface()); structErrors != nil { validationErrors.StructErrors[cField.name] = structErrors // free up memory map no longer needed diff --git a/validator_test.go b/validator_test.go index a2d5394..84c4785 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,98 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestNilStructPointerValidation(t *testing.T) { + type Inner struct { + Data string + } + + type Outer struct { + Inner *Inner `validate:"omitempty"` + } + + inner := &Inner{ + Data: "test", + } + + outer := &Outer{ + Inner: inner, + } + + errs := validate.Struct(outer) + Equal(t, errs, nil) + + outer = &Outer{ + Inner: nil, + } + + errs = validate.Struct(outer) + Equal(t, errs, nil) + + type Inner2 struct { + Data string + } + + type Outer2 struct { + Inner2 *Inner2 `validate:"required"` + } + + inner2 := &Inner2{ + Data: "test", + } + + outer2 := &Outer2{ + Inner2: inner2, + } + + errs = validate.Struct(outer2) + Equal(t, errs, nil) + + outer2 = &Outer2{ + Inner2: nil, + } + + errs = validate.Struct(outer2) + NotEqual(t, errs, nil) + + type Inner3 struct { + Data string + } + + type Outer3 struct { + Inner3 *Inner3 + } + + inner3 := &Inner3{ + Data: "test", + } + + outer3 := &Outer3{ + Inner3: inner3, + } + + errs = validate.Struct(outer3) + Equal(t, errs, nil) + + type Inner4 struct { + Data string + } + + type Outer4 struct { + Inner4 *Inner4 `validate:"-"` + } + + inner4 := &Inner4{ + Data: "test", + } + + outer4 := &Outer4{ + Inner4: inner4, + } + + errs = validate.Struct(outer4) + Equal(t, errs, nil) +} + func TestSSNValidation(t *testing.T) { tests := []struct { param string @@ -1100,7 +1192,7 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: nil, } - errs := validate.Struct(outer).Flatten() + errs := validate.Struct(outer) NotEqual(t, errs, nil) inner := &Inner{ @@ -1111,9 +1203,8 @@ func TestStructOnlyValidation(t *testing.T) { InnerStruct: inner, } - errs = validate.Struct(outer).Flatten() - NotEqual(t, errs, nil) - Equal(t, len(errs), 0) + errs = validate.Struct(outer) + Equal(t, errs, nil) } func TestGtField(t *testing.T) { From 4afdc19aef655b5c36a3089d548ac25e9a9b6b64 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 25 Jun 2015 09:15:46 -0400 Subject: [PATCH 25/52] Finish initial array traversal logic for #78 --- validator.go | 309 +++++++++++++++++++++++++++++++++++++++++----- validator_test.go | 32 +++++ 2 files changed, 313 insertions(+), 28 deletions(-) diff --git a/validator.go b/validator.go index ba293f0..049be60 100644 --- a/validator.go +++ b/validator.go @@ -32,6 +32,8 @@ const ( sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" structErrMsg = "Struct:%s\n" + diveTag = "dive" + diveSplit = "," + diveTag ) var structPool *pool @@ -80,13 +82,23 @@ type cachedTags struct { } type cachedField struct { - index int - name string - tags []*cachedTags - tag string - kind reflect.Kind - typ reflect.Type - isTime bool + index int + name string + tags []*cachedTags + tag string + kind reflect.Kind + typ reflect.Type + isTime bool + isSliceOrArray bool + isMap bool + isTimeSubtype bool + sliceSubtype reflect.Type + mapSubtype reflect.Type + sliceSubKind reflect.Kind + mapSubKind reflect.Kind + // DiveMaxDepth uint64 // zero means no depth + // DiveTags map[uint64]string // map of dive depth and associated tag as string] + diveTag string } type cachedStruct struct { @@ -181,17 +193,19 @@ var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { - Field string - Tag string - Kind reflect.Kind - Type reflect.Type - Param string - Value interface{} - IsSliceOrArrayError bool - IsMapError bool - Key interface{} - Index uint64 - DiveErrors []*error // counld be FieldError, StructErrors + Field string + Tag string + Kind reflect.Kind + Type reflect.Type + Param string + Value interface{} + isPlaceholderErr bool + IsSliceOrArray bool + IsMap bool + // Key interface{} + // Index int + SliceOrArrayErrs []error // counld be FieldError, StructErrors + MapErrs map[interface{}]error // counld be FieldError, StructErrors } // This is intended for use in development + debugging and not intended to be a production error message. @@ -211,10 +225,12 @@ type StructErrors struct { // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors - IsSliceOrArrayError bool - IsMapError bool - Key interface{} - Index uint64 + // Index int + // Key interface{} + // IsSliceOrArrayError bool + // IsMapError bool + // Key interface{} + // Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. @@ -393,7 +409,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter typeField = structType.Field(i) - cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName)} + cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: valueField.Type() == reflect.TypeOf(time.Time{})} if cField.tag == noValidationTag { cs.children-- @@ -426,9 +442,9 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } - if cField.isTime || valueField.Type() == reflect.TypeOf(time.Time{}) { + if cField.isTime { - cField.isTime = true + // cField.isTime = true if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError @@ -468,8 +484,31 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } } - default: + case reflect.Slice, reflect.Array: + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.sliceSubKind = cField.sliceSubtype.Kind() + + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + + case reflect.Map: + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.mapSubKind = cField.mapSubtype.Kind() + + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil + } + default: if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference @@ -506,6 +545,8 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f var cField *cachedField var isCached bool + // var isInDive bool + var valueField reflect.Value // This is a double check if coming from validate.Struct but need to be here in case function is called directly if tag == noValidationTag { @@ -516,8 +557,10 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } + valueField = reflect.ValueOf(f) + if cacheField == nil { - valueField := reflect.ValueOf(f) + // valueField = reflect.ValueOf(f) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { valueField = valueField.Elem() @@ -525,6 +568,19 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } cField = &cachedField{name: name, kind: valueField.Kind(), tag: tag, typ: valueField.Type()} + + switch cField.kind { + case reflect.Slice, reflect.Array: + cField.isSliceOrArray = true + cField.sliceSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.sliceSubKind = cField.sliceSubtype.Kind() + case reflect.Map: + cField.isMap = true + cField.mapSubtype = cField.typ.Elem() + cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.mapSubKind = cField.mapSubtype.Kind() + } } else { cField = cacheField } @@ -546,11 +602,37 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if !isCached { - for _, t := range strings.Split(tag, tagSeparator) { + for k, t := range strings.Split(tag, tagSeparator) { + + if t == diveTag { + + if k == 0 { + cField.diveTag = tag[4:] + } else { + cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] + } + + break + } orVals := strings.Split(t, orSeparator) cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} + + // if isInDive { + + // s, ok := cField.DiveTags[cField.DiveMaxDepth] + + // if ok { + // cField.DiveTags[cField.DiveMaxDepth] = cField.DiveTags[cField.DiveMaxDepth] + tagSeparator + tag + // } else { + // cField.DiveTags[cField.DiveMaxDepth] = tag + // } + + // continue + + // } else { cField.tags = append(cField.tags, cTag) + // } for i, val := range orVals { vals := strings.SplitN(val, tagKeySeparator, 2) @@ -614,9 +696,180 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } + if len(cField.diveTag) > 0 { + + if cField.isSliceOrArray { + + if errs := v.traverseSliceOrArray(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + isPlaceholderErr: true, + IsSliceOrArray: true, + // Index: i, + SliceOrArrayErrs: errs, + } + } + // return if error here + } else if cField.isMap { + // return if error here + } else { + // throw error, if not a slice or map then should not have gotten here + } + + // dive tags need to be passed to traverse + // traverse needs to call a SliceOrArray recursive function to meet depth requirements + + // for depth, diveTag := range cField.DiveTags { + + // // error returned should be added to SliceOrArrayErrs + // if errs := v.traverseSliceOrArrayField(val, current, depth, currentDepth+1, diveTag, cField, valueField); len(errs) > 0 { + // // result := &FieldError{ + // // Field: cField.name, + // // Kind: cField.kind, + // // Type: cField.typ, + // // Value: valueField.Index(i).Interface(), + // // isPlaceholderErr: true, + // // IsSliceOrArray:true, + // // Index:i, + // // SliceOrArrayErrs: + // // } + // } + + // for _, tag := range diveTag { + + // fmt.Println("Depth:", depth, " Tag:", tag, " SliceType:", cField.SliceSubtype, " MapType:", cField.MapSubtype, " Kind:", cField.kind) + + // } + // } + } + return nil } +func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) []error { + + errs := make([]error, 0) + + for i := 0; i < valueField.Len(); i++ { + + idxField := valueField.Index(i) + + switch cField.sliceSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + errs = append(errs, fieldError) + } + + continue + } + + if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + errs = append(errs, &FieldError{ + Field: cField.name, + Tag: required, + Value: idxField.Interface(), + Kind: reflect.Ptr, + Type: cField.sliceSubtype, + }) + + continue + } + } + + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs = append(errs, structErrors) + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + errs = append(errs, fieldError) + } + } + } + + return errs +} + +// func (v *Validate) traverseSliceOrArrayField(val interface{}, current interface{}, depth uint64, currentDepth uint64, diveTags []*cachedTags, cField *cachedField, valueField reflect.Value) []error { + +// for i := 0; i < valueField.Len(); i++ { + +// if depth != currentDepth { + +// switch cField.SliceSubKind { +// case reflect.Slice, reflect.Array: +// return v.fieldWithNameAndValue(val, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField, currentDepth) + +// // type FieldError struct { +// // Field string +// // Tag string +// // Kind reflect.Kind +// // Type reflect.Type +// // Param string +// // Value interface{} +// // HasErr bool +// // IsSliceOrArray bool +// // IsMap bool +// // Key interface{} +// // Index uint64 +// // SliceOrArrayErrs []*error // counld be FieldError, StructErrors +// // MapErrs map[interface{}]*error // counld be FieldError, StructErrors +// // } + +// // result := &FieldError{ +// // Field: cField.name, +// // Kind: cField.kind, +// // Type: cField.typ, +// // Value: valueField.Index(i).Interface(), +// // isPlaceholderErr: true, +// // IsSliceOrArray:true, +// // Index:i, +// // SliceOrArrayErrs: +// // } +// // validationErrors.Errors[fieldError.Field] = fieldError +// // // free up memory reference +// // fieldError = nil +// // } +// default: +// panic("attempting to dive deeper, but Kind is not a Slice nor Array") +// } +// } + +// // switch cField.SliceSubKind { +// // case reflect.Struct, reflect.Interface: +// // // need to check if required tag and or omitempty just like in struct recirsive +// // if cField.isTimeSubtype || valueField.Type() == reflect.TypeOf(time.Time{}) { + +// // if fieldError := v.fieldWithNameAndValue(top, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField); fieldError != nil { +// // validationErrors.Errors[fieldError.Field] = fieldError +// // // free up memory reference +// // fieldError = nil +// // } +// // } +// // } +// fmt.Println(valueField.Index(i)) +// } +// // fmt.Println(v) +// // for _, item := range arr { + +// // } +// return nil +// } + func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { // OK to continue because we checked it's existance before getting into this loop diff --git a/validator_test.go b/validator_test.go index 84c4785..804babf 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,38 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestArrayDiveValidation(t *testing.T) { + + type Test struct { + Errs []string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: []string{"ok", "", "ok"}, + } + + errs := validate.Struct(test) + + fmt.Println(errs) + + // type TestMap struct { + // Errs *map[int]string `validate:"gt=0,dive,required"` + // } + + // m := map[int]string{} + // m[1] = "ok" + // m[2] = "" + // m[3] = "ok" + + // testMap := &TestMap{ + // Errs: &m, + // } + + // errs = validate.Struct(testMap) + + // fmt.Println(errs) +} + func TestNilStructPointerValidation(t *testing.T) { type Inner struct { Data string From d019d02290b692285b4104a5f43daead30c57f8b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 25 Jun 2015 17:24:25 -0400 Subject: [PATCH 26/52] Add some initial validation change slice errors variable type to map[int]error to allow tracking of index of the error i the array for #78 --- validator.go | 21 +++++++++++---------- validator_test.go | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/validator.go b/validator.go index 049be60..e6e0cb9 100644 --- a/validator.go +++ b/validator.go @@ -199,12 +199,13 @@ type FieldError struct { Type reflect.Type Param string Value interface{} - isPlaceholderErr bool + IsPlaceholderErr bool IsSliceOrArray bool IsMap bool // Key interface{} // Index int - SliceOrArrayErrs []error // counld be FieldError, StructErrors + // SliceOrArrayErrs []error // counld be FieldError, StructErrors + SliceOrArrayErrs map[int]error // counld be FieldError, StructErrors MapErrs map[interface{}]error // counld be FieldError, StructErrors } @@ -707,7 +708,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f Kind: cField.kind, Type: cField.typ, Value: f, - isPlaceholderErr: true, + IsPlaceholderErr: true, IsSliceOrArray: true, // Index: i, SliceOrArrayErrs: errs, @@ -750,9 +751,9 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } -func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) []error { +func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { - errs := make([]error, 0) + errs := make(map[int]error, 0) for i := 0; i < valueField.Len(); i++ { @@ -764,7 +765,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { - errs = append(errs, fieldError) + errs[i] = fieldError } continue @@ -778,25 +779,25 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if strings.Contains(cField.tag, required) { - errs = append(errs, &FieldError{ + errs[i] = &FieldError{ Field: cField.name, Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, Type: cField.sliceSubtype, - }) + } continue } } if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { - errs = append(errs, structErrors) + errs[i] = structErrors } default: if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { - errs = append(errs, fieldError) + errs[i] = fieldError } } } diff --git a/validator_test.go b/validator_test.go index 804babf..a32d99e 100644 --- a/validator_test.go +++ b/validator_test.go @@ -237,8 +237,42 @@ func TestArrayDiveValidation(t *testing.T) { } errs := validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs") + + test = &Test{ + Errs: []string{"ok", "ok", ""}, + } + + errs = validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs") - fmt.Println(errs) + fmt.Println(errs.Errors["Errs"].IsPlaceholderErr) // type TestMap struct { // Errs *map[int]string `validate:"gt=0,dive,required"` From 6eded1f81732b10f5aa8c1249e7561925b1df2f1 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:28:15 -0400 Subject: [PATCH 27/52] correct error output and index out of order error for #78 --- validator.go | 228 +++++++++++----------------------------------- validator_test.go | 129 ++++++++++++++++---------- 2 files changed, 135 insertions(+), 222 deletions(-) diff --git a/validator.go b/validator.go index e6e0cb9..3f32f4d 100644 --- a/validator.go +++ b/validator.go @@ -29,11 +29,12 @@ const ( omitempty = "omitempty" required = "required" fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" index \"%d\" failed on the \"%s\" tag" - mapErrMsg = "Field validation for \"%s\" key \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" failed with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed with key \"%v\" failed with error(s): %s" structErrMsg = "Struct:%s\n" diveTag = "dive" diveSplit = "," + diveTag + indexFieldName = "%s[%d]" ) var structPool *pool @@ -96,9 +97,7 @@ type cachedField struct { mapSubtype reflect.Type sliceSubKind reflect.Kind mapSubKind reflect.Kind - // DiveMaxDepth uint64 // zero means no depth - // DiveTags map[uint64]string // map of dive depth and associated tag as string] - diveTag string + diveTag string } type cachedStruct struct { @@ -148,48 +147,6 @@ func (s *fieldsCacheMap) Set(key string, value []*cachedTags) { var fieldsCache = &fieldsCacheMap{m: map[string][]*cachedTags{}} -// // SliceError contains a fields error for a single index within an array or slice -// // NOTE: library only checks the first dimension of the array so if you have a multidimensional -// // array [][]string that validations after the "dive" tag are applied to []string not each -// // string within it. It is not a dificulty with traversing the chain, but how to add validations -// // to what dimension of an array and even how to report on them in any meaningful fashion. -// type SliceError struct { -// Index uint64 -// Field string -// Tag string -// Kind reflect.Kind -// Type reflect.Type -// Param string -// Value interface{} -// } - -// // This is intended for use in development + debugging and not intended to be a production error message. -// // it also allows SliceError to be used as an Error interface -// func (e *SliceError) Error() string { -// return fmt.Sprintf(sliceErrMsg, e.Field, e.Index, e.Tag) -// } - -// // MapError contains a fields error for a single key within a map -// // NOTE: library only checks the first dimension of the array so if you have a multidimensional -// // array [][]string that validations after the "dive" tag are applied to []string not each -// // string within it. It is not a dificulty with traversing the chain, but how to add validations -// // to what dimension of an array and even how to report on them in any meaningful fashion. -// type MapError struct { -// Key interface{} -// Field string -// Tag string -// Kind reflect.Kind -// Type reflect.Type -// Param string -// Value interface{} -// } - -// // This is intended for use in development + debugging and not intended to be a production error message. -// // it also allows MapError to be used as an Error interface -// func (e *MapError) Error() string { -// return fmt.Sprintf(mapErrMsg, e.Field, e.Key, e.Tag) -// } - // FieldError contains a single field's validation error along // with other properties that may be needed for error message creation type FieldError struct { @@ -202,9 +159,6 @@ type FieldError struct { IsPlaceholderErr bool IsSliceOrArray bool IsMap bool - // Key interface{} - // Index int - // SliceOrArrayErrs []error // counld be FieldError, StructErrors SliceOrArrayErrs map[int]error // counld be FieldError, StructErrors MapErrs map[interface{}]error // counld be FieldError, StructErrors } @@ -212,6 +166,40 @@ type FieldError struct { // This is intended for use in development + debugging and not intended to be a production error message. // it also allows FieldError to be used as an Error interface func (e *FieldError) Error() string { + + if e.IsPlaceholderErr { + + buff := bytes.NewBufferString("") + + if e.IsSliceOrArray { + + for i, err := range e.SliceOrArrayErrs { + + if i != 0 { + buff.WriteString("\n") + } + + buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, i, err)) + } + + } else if e.IsMap { + + var i uint64 + + for key, err := range e.MapErrs { + + if i != 0 { + buff.WriteString("\n") + } + + buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, err)) + i++ + } + } + + return buff.String() + } + return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } @@ -225,13 +213,6 @@ type StructErrors struct { // Struct Fields of type struct and their errors // key = Field Name of current struct, but internally Struct will be the actual struct name unless anonymous struct, it will be blank StructErrors map[string]*StructErrors - - // Index int - // Key interface{} - // IsSliceOrArrayError bool - // IsMapError bool - // Key interface{} - // Index uint64 } // This is intended for use in development + debugging and not intended to be a production error message. @@ -241,11 +222,19 @@ func (e *StructErrors) Error() string { for _, err := range e.Errors { buff.WriteString(err.Error()) - buff.WriteString("\n") } + var i uint64 + for _, err := range e.StructErrors { + + if i != 0 { + buff.WriteString("\n") + } + buff.WriteString(err.Error()) + + i++ } return buff.String() @@ -445,8 +434,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter if cField.isTime { - // cField.isTime = true - if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { validationErrors.Errors[fieldError.Field] = fieldError // free up memory reference @@ -532,13 +519,11 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter // Field allows validation of a single field, still using tag style validation to check multiple errors func (v *Validate) Field(f interface{}, tag string) *FieldError { - return v.FieldWithValue(nil, f, tag) } // 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 { - return v.fieldWithNameAndValue(nil, val, f, tag, "", true, nil) } @@ -546,7 +531,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f var cField *cachedField var isCached bool - // var isInDive bool var valueField reflect.Value // This is a double check if coming from validate.Struct but need to be here in case function is called directly @@ -561,7 +545,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f valueField = reflect.ValueOf(f) if cacheField == nil { - // valueField = reflect.ValueOf(f) if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { valueField = valueField.Elem() @@ -608,7 +591,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if t == diveTag { if k == 0 { - cField.diveTag = tag[4:] + cField.diveTag = tag[5:] } else { cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] } @@ -618,22 +601,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f orVals := strings.Split(t, orSeparator) cTag := &cachedTags{isOrVal: len(orVals) > 1, keyVals: make([][]string, len(orVals))} - - // if isInDive { - - // s, ok := cField.DiveTags[cField.DiveMaxDepth] - - // if ok { - // cField.DiveTags[cField.DiveMaxDepth] = cField.DiveTags[cField.DiveMaxDepth] + tagSeparator + tag - // } else { - // cField.DiveTags[cField.DiveMaxDepth] = tag - // } - - // continue - - // } else { cField.tags = append(cField.tags, cTag) - // } for i, val := range orVals { vals := strings.SplitN(val, tagKeySeparator, 2) @@ -710,42 +678,16 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f Value: f, IsPlaceholderErr: true, IsSliceOrArray: true, - // Index: i, SliceOrArrayErrs: errs, } } - // return if error here + } else if cField.isMap { // return if error here } else { // throw error, if not a slice or map then should not have gotten here + panic("dive error! can't dive on a non slice or map") } - - // dive tags need to be passed to traverse - // traverse needs to call a SliceOrArray recursive function to meet depth requirements - - // for depth, diveTag := range cField.DiveTags { - - // // error returned should be added to SliceOrArrayErrs - // if errs := v.traverseSliceOrArrayField(val, current, depth, currentDepth+1, diveTag, cField, valueField); len(errs) > 0 { - // // result := &FieldError{ - // // Field: cField.name, - // // Kind: cField.kind, - // // Type: cField.typ, - // // Value: valueField.Index(i).Interface(), - // // isPlaceholderErr: true, - // // IsSliceOrArray:true, - // // Index:i, - // // SliceOrArrayErrs: - // // } - // } - - // for _, tag := range diveTag { - - // fmt.Println("Depth:", depth, " Tag:", tag, " SliceType:", cField.SliceSubtype, " MapType:", cField.MapSubtype, " Kind:", cField.kind) - - // } - // } } return nil @@ -753,7 +695,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { - errs := make(map[int]error, 0) + errs := map[int]error{} for i := 0; i < valueField.Len(); i++ { @@ -796,7 +738,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va } default: - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(indexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } } @@ -805,72 +747,6 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va return errs } -// func (v *Validate) traverseSliceOrArrayField(val interface{}, current interface{}, depth uint64, currentDepth uint64, diveTags []*cachedTags, cField *cachedField, valueField reflect.Value) []error { - -// for i := 0; i < valueField.Len(); i++ { - -// if depth != currentDepth { - -// switch cField.SliceSubKind { -// case reflect.Slice, reflect.Array: -// return v.fieldWithNameAndValue(val, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField, currentDepth) - -// // type FieldError struct { -// // Field string -// // Tag string -// // Kind reflect.Kind -// // Type reflect.Type -// // Param string -// // Value interface{} -// // HasErr bool -// // IsSliceOrArray bool -// // IsMap bool -// // Key interface{} -// // Index uint64 -// // SliceOrArrayErrs []*error // counld be FieldError, StructErrors -// // MapErrs map[interface{}]*error // counld be FieldError, StructErrors -// // } - -// // result := &FieldError{ -// // Field: cField.name, -// // Kind: cField.kind, -// // Type: cField.typ, -// // Value: valueField.Index(i).Interface(), -// // isPlaceholderErr: true, -// // IsSliceOrArray:true, -// // Index:i, -// // SliceOrArrayErrs: -// // } -// // validationErrors.Errors[fieldError.Field] = fieldError -// // // free up memory reference -// // fieldError = nil -// // } -// default: -// panic("attempting to dive deeper, but Kind is not a Slice nor Array") -// } -// } - -// // switch cField.SliceSubKind { -// // case reflect.Struct, reflect.Interface: -// // // need to check if required tag and or omitempty just like in struct recirsive -// // if cField.isTimeSubtype || valueField.Type() == reflect.TypeOf(time.Time{}) { - -// // if fieldError := v.fieldWithNameAndValue(top, current, valueField.Index(i).Interface(), cField.tag, cField.name, false, cField); fieldError != nil { -// // validationErrors.Errors[fieldError.Field] = fieldError -// // // free up memory reference -// // fieldError = nil -// // } -// // } -// // } -// fmt.Println(valueField.Index(i)) -// } -// // fmt.Println(v) -// // for _, item := range arr { - -// // } -// return nil -// } - func (v *Validate) fieldWithNameAndSingleTag(val interface{}, current interface{}, f interface{}, key string, param string, name string) (*FieldError, error) { // OK to continue because we checked it's existance before getting into this loop diff --git a/validator_test.go b/validator_test.go index a32d99e..ca60acc 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,70 +226,107 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestMapDiveValidation(t *testing.T) { +} + func TestArrayDiveValidation(t *testing.T) { - type Test struct { - Errs []string `validate:"gt=0,dive,required"` - } + // type Test struct { + // Errs []string `validate:"gt=0,dive,required"` + // } - test := &Test{ - Errs: []string{"ok", "", "ok"}, - } + // test := &Test{ + // Errs: []string{"ok", "", "ok"}, + // } - errs := validate.Struct(test) - NotEqual(t, errs, nil) - Equal(t, len(errs.Errors), 1) + // errs := validate.Struct(test) + // NotEqual(t, errs, nil) + // Equal(t, len(errs.Errors), 1) - fieldErr, ok := errs.Errors["Errs"] - Equal(t, ok, true) - Equal(t, fieldErr.IsPlaceholderErr, true) - Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + // fieldErr, ok := errs.Errors["Errs"] + // Equal(t, ok, true) + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) - Equal(t, ok, true) - Equal(t, innerErr.Tag, required) - Equal(t, innerErr.IsPlaceholderErr, false) - Equal(t, innerErr.Field, "Errs") + // innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + // Equal(t, ok, true) + // Equal(t, innerErr.Tag, required) + // Equal(t, innerErr.IsPlaceholderErr, false) + // Equal(t, innerErr.Field, "Errs") + + // test = &Test{ + // Errs: []string{"ok", "ok", ""}, + // } + + // errs = validate.Struct(test) + // NotEqual(t, errs, nil) + // Equal(t, len(errs.Errors), 1) - test = &Test{ - Errs: []string{"ok", "ok", ""}, + // fieldErr, ok = errs.Errors["Errs"] + // Equal(t, ok, true) + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + // innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + // Equal(t, ok, true) + // Equal(t, innerErr.Tag, required) + // Equal(t, innerErr.IsPlaceholderErr, false) + // Equal(t, innerErr.Field, "Errs") + + type TestMultiDimensional struct { + Errs [][]string `validate:"gt=0,dive,dive,required"` } - errs = validate.Struct(test) + var errArray [][]string + + errArray = append(errArray, []string{"ok", "", ""}) + errArray = append(errArray, []string{"ok", "", ""}) + // fmt.Println(len(errArray)) + // errArray = append(errArray, []string{"", "ok", "ok"}) + // errArray = append(errArray, []string{"", "", "ok"}) + // errArray = append(errArray, []string{"", "", "ok"}) + + tm := &TestMultiDimensional{ + Errs: errArray, + } + + errs := validate.Struct(tm) + fmt.Println(errs) + // validate.Struct(tm) + + // fmt.Printf("%#v\n", errs.Errors["Errs"].SliceOrArrayErrs) + NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) - fieldErr, ok = errs.Errors["Errs"] + fieldErr, ok := errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) - innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + sliceError1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) Equal(t, ok, true) - Equal(t, innerErr.Tag, required) - Equal(t, innerErr.IsPlaceholderErr, false) - Equal(t, innerErr.Field, "Errs") + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) - fmt.Println(errs.Errors["Errs"].IsPlaceholderErr) - - // type TestMap struct { - // Errs *map[int]string `validate:"gt=0,dive,required"` - // } - - // m := map[int]string{} - // m[1] = "ok" - // m[2] = "" - // m[3] = "ok" - - // testMap := &TestMap{ - // Errs: &m, - // } - - // errs = validate.Struct(testMap) - - // fmt.Println(errs) + innerSliceError1, ok := sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.Tag, required) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + // fmt.Println(fieldErr.SliceOrArrayErrs) + + // Equal(t, fieldErr.IsPlaceholderErr, true) + // Equal(t, fieldErr.IsSliceOrArray, true) + // Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + // fmt.Println(fieldErr.SliceOrArrayErrs) + // fmt.Println(len(fieldErr.SliceOrArrayErrs)) } func TestNilStructPointerValidation(t *testing.T) { From 1ba858eec1230a307d29c328460d2e567f6a9a08 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:38:28 -0400 Subject: [PATCH 28/52] correct FieldError error printing idea issue for #78 --- validator.go | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/validator.go b/validator.go index 3f32f4d..74b634c 100644 --- a/validator.go +++ b/validator.go @@ -173,27 +173,14 @@ func (e *FieldError) Error() string { if e.IsSliceOrArray { - for i, err := range e.SliceOrArrayErrs { - - if i != 0 { - buff.WriteString("\n") - } - - buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, i, err)) + for j, err := range e.SliceOrArrayErrs { + buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, j, "\n"+err.Error())) } } else if e.IsMap { - var i uint64 - for key, err := range e.MapErrs { - - if i != 0 { - buff.WriteString("\n") - } - - buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, err)) - i++ + buff.WriteString(fmt.Sprintf(mapErrMsg, e.Field, key, "\n"+err.Error())) } } @@ -233,7 +220,6 @@ func (e *StructErrors) Error() string { } buff.WriteString(err.Error()) - i++ } From 689d3e9989e4598fa6a159a145a9d5c9dd9f65fe Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 07:47:12 -0400 Subject: [PATCH 29/52] finalized array error handling for #78 --- validator.go | 11 +---- validator_test.go | 106 ++++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/validator.go b/validator.go index 74b634c..36918dc 100644 --- a/validator.go +++ b/validator.go @@ -209,21 +209,14 @@ func (e *StructErrors) Error() string { for _, err := range e.Errors { buff.WriteString(err.Error()) + buff.WriteString("\n") } - var i uint64 - for _, err := range e.StructErrors { - - if i != 0 { - buff.WriteString("\n") - } - buff.WriteString(err.Error()) - i++ } - return buff.String() + return strings.TrimSpace(buff.String()) } // Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name diff --git a/validator_test.go b/validator_test.go index ca60acc..970d5dd 100644 --- a/validator_test.go +++ b/validator_test.go @@ -231,49 +231,49 @@ func TestMapDiveValidation(t *testing.T) { func TestArrayDiveValidation(t *testing.T) { - // type Test struct { - // Errs []string `validate:"gt=0,dive,required"` - // } - - // test := &Test{ - // Errs: []string{"ok", "", "ok"}, - // } - - // errs := validate.Struct(test) - // NotEqual(t, errs, nil) - // Equal(t, len(errs.Errors), 1) - - // fieldErr, ok := errs.Errors["Errs"] - // Equal(t, ok, true) - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - - // innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) - // Equal(t, ok, true) - // Equal(t, innerErr.Tag, required) - // Equal(t, innerErr.IsPlaceholderErr, false) - // Equal(t, innerErr.Field, "Errs") - - // test = &Test{ - // Errs: []string{"ok", "ok", ""}, - // } - - // errs = validate.Struct(test) - // NotEqual(t, errs, nil) - // Equal(t, len(errs.Errors), 1) - - // fieldErr, ok = errs.Errors["Errs"] - // Equal(t, ok, true) - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 1) - - // innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) - // Equal(t, ok, true) - // Equal(t, innerErr.Tag, required) - // Equal(t, innerErr.IsPlaceholderErr, false) - // Equal(t, innerErr.Field, "Errs") + type Test struct { + Errs []string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: []string{"ok", "", "ok"}, + } + + errs := validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok := fieldErr.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[1]") + + test = &Test{ + Errs: []string{"ok", "ok", ""}, + } + + errs = validate.Struct(test) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerErr, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.Tag, required) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.Field, "Errs[2]") type TestMultiDimensional struct { Errs [][]string `validate:"gt=0,dive,dive,required"` @@ -283,25 +283,17 @@ func TestArrayDiveValidation(t *testing.T) { errArray = append(errArray, []string{"ok", "", ""}) errArray = append(errArray, []string{"ok", "", ""}) - // fmt.Println(len(errArray)) - // errArray = append(errArray, []string{"", "ok", "ok"}) - // errArray = append(errArray, []string{"", "", "ok"}) - // errArray = append(errArray, []string{"", "", "ok"}) tm := &TestMultiDimensional{ Errs: errArray, } - errs := validate.Struct(tm) - fmt.Println(errs) - // validate.Struct(tm) - - // fmt.Printf("%#v\n", errs.Errors["Errs"].SliceOrArrayErrs) + errs = validate.Struct(tm) NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) - fieldErr, ok := errs.Errors["Errs"] + fieldErr, ok = errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) @@ -319,14 +311,6 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.Tag, required) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - // fmt.Println(fieldErr.SliceOrArrayErrs) - - // Equal(t, fieldErr.IsPlaceholderErr, true) - // Equal(t, fieldErr.IsSliceOrArray, true) - // Equal(t, len(fieldErr.SliceOrArrayErrs), 3) - - // fmt.Println(fieldErr.SliceOrArrayErrs) - // fmt.Println(len(fieldErr.SliceOrArrayErrs)) } func TestNilStructPointerValidation(t *testing.T) { From a0f6d14ada749dca7b7cb5c807b69dc9d3a704b5 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 08:41:37 -0400 Subject: [PATCH 30/52] add more tests correct pointer issue is traverseArray for #78 --- validator.go | 23 +++++++++++--- validator_test.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/validator.go b/validator.go index 36918dc..9b1c3ea 100644 --- a/validator.go +++ b/validator.go @@ -97,6 +97,7 @@ type cachedField struct { mapSubtype reflect.Type sliceSubKind reflect.Kind mapSubKind reflect.Kind + dive bool diveTag string } @@ -174,6 +175,7 @@ func (e *FieldError) Error() string { if e.IsSliceOrArray { for j, err := range e.SliceOrArrayErrs { + buff.WriteString("\n") buff.WriteString(fmt.Sprintf(sliceErrMsg, e.Field, j, "\n"+err.Error())) } @@ -184,7 +186,7 @@ func (e *FieldError) Error() string { } } - return buff.String() + return strings.TrimSpace(buff.String()) } return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) @@ -553,6 +555,8 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { + + fmt.Println(cField.typ) panic("Invalid field passed to ValidateFieldWithTag") } } @@ -569,8 +573,14 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if t == diveTag { + cField.dive = true + if k == 0 { - cField.diveTag = tag[5:] + if len(tag) == 4 { + cField.diveTag = "" + } else { + cField.diveTag = tag[5:] + } } else { cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] } @@ -644,7 +654,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } - if len(cField.diveTag) > 0 { + if cField.dive { if cField.isSliceOrArray { @@ -680,12 +690,17 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va idxField := valueField.Index(i) + if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.sliceSubKind = idxField.Kind() + } + switch cField.sliceSubKind { case reflect.Struct, reflect.Interface: if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, true, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { errs[i] = fieldError } diff --git a/validator_test.go b/validator_test.go index 970d5dd..2e98403 100644 --- a/validator_test.go +++ b/validator_test.go @@ -289,7 +289,6 @@ func TestArrayDiveValidation(t *testing.T) { } errs = validate.Struct(tm) - NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) @@ -311,6 +310,86 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.Tag, required) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + + type Inner struct { + Name string `validate:"required"` + } + + type TestMultiDimensionalStructs struct { + Errs [][]Inner `validate:"gt=0,dive,dive"` + } + + var errStructArray [][]Inner + + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + errStructArray = append(errStructArray, []Inner{Inner{"ok"}, Inner{""}, Inner{""}}) + + tms := &TestMultiDimensionalStructs{ + Errs: errStructArray, + } + + errs = validate.Struct(tms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok := sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 := innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr struct { + Errs [][]*Inner `validate:"gt=0,dive,dive"` + } + + var errStructPtrArray [][]*Inner + + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + + tmsp := &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs = validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) } func TestNilStructPointerValidation(t *testing.T) { From 98f4165fae5722c9cd1621bb55c1e6f52f85e194 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 09:57:02 -0400 Subject: [PATCH 31/52] added time test fix issue with time.Time data type validation --- validator.go | 14 ++-- validator_test.go | 165 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 9 deletions(-) diff --git a/validator.go b/validator.go index 9b1c3ea..36fd3ff 100644 --- a/validator.go +++ b/validator.go @@ -380,7 +380,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter typeField = structType.Field(i) - cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: valueField.Type() == reflect.TypeOf(time.Time{})} + cField = &cachedField{index: i, tag: typeField.Tag.Get(v.tagName), isTime: (valueField.Type() == reflect.TypeOf(time.Time{}) || valueField.Type() == reflect.TypeOf(&time.Time{}))} if cField.tag == noValidationTag { cs.children-- @@ -538,12 +538,12 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Slice, reflect.Array: cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() case reflect.Map: cField.isMap = true cField.mapSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) cField.mapSubKind = cField.mapSubtype.Kind() } } else { @@ -555,8 +555,6 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { - - fmt.Println(cField.typ) panic("Invalid field passed to ValidateFieldWithTag") } } @@ -698,7 +696,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va switch cField.sliceSubKind { case reflect.Struct, reflect.Interface: - if cField.isTimeSubtype || idxField.Type() == reflect.TypeOf(time.Time{}) { + if cField.isTimeSubtype { if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { errs[i] = fieldError @@ -722,9 +720,9 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va Kind: reflect.Ptr, Type: cField.sliceSubtype, } - - continue } + + continue } if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { diff --git a/validator_test.go b/validator_test.go index 2e98403..a1086ab 100644 --- a/validator_test.go +++ b/validator_test.go @@ -361,6 +361,7 @@ func TestArrayDiveValidation(t *testing.T) { errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) tmsp := &TestMultiDimensionalStructsPtr{ Errs: errStructPtrArray, @@ -374,7 +375,46 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) - Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr2 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,required"` + } + + var errStructPtr2Array [][]*Inner + + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp2 := &TestMultiDimensionalStructsPtr2{ + Errs: errStructPtr2Array, + } + + errs = validate.Struct(tmsp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) Equal(t, ok, true) @@ -390,6 +430,129 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerInnersliceError1.IsPlaceholderErr, false) Equal(t, innerInnersliceError1.IsSliceOrArray, false) Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr3 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,omitempty"` + } + + var errStructPtr3Array [][]*Inner + + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr3Array = append(errStructPtr3Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp3 := &TestMultiDimensionalStructsPtr3{ + Errs: errStructPtr3Array, + } + + errs = validate.Struct(tmsp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalTimeTime struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimePtr3Array [][]*time.Time + + t1 := time.Now().UTC() + t2 := time.Now().UTC() + t3 := time.Now().UTC().Add(time.Hour * 24) + + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, &t3}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, &t2, nil}) + errTimePtr3Array = append(errTimePtr3Array, []*time.Time{&t1, nil, nil}) + + tmtp3 := &TestMultiDimensionalTimeTime{ + Errs: errTimePtr3Array, + } + + errs = validate.Struct(tmtp3) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Tag, required) + + type TestMultiDimensionalTimeTime2 struct { + Errs [][]*time.Time `validate:"gt=0,dive,dive,required"` + } + + var errTimeArray [][]*time.Time + + t1 = time.Now().UTC() + t2 = time.Now().UTC() + t3 = time.Now().UTC().Add(time.Hour * 24) + + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, &t3}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, &t2, nil}) + errTimeArray = append(errTimeArray, []*time.Time{&t1, nil, nil}) + + tmtp := &TestMultiDimensionalTimeTime2{ + Errs: errTimeArray, + } + + errs = validate.Struct(tmtp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceError1, ok = sliceError1.SliceOrArrayErrs[1].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceError1.IsPlaceholderErr, false) + Equal(t, innerSliceError1.IsSliceOrArray, false) + Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Tag, required) } func TestNilStructPointerValidation(t *testing.T) { From 14f176e8ac9d7c4393fb26ee8101cf6d8a5bf522 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Fri, 26 Jun 2015 10:18:17 -0400 Subject: [PATCH 32/52] add traverseMap for #78 --- validator.go | 107 ++++++++++++++++++++++++++++++++++++++-------- validator_test.go | 16 ++++++- 2 files changed, 103 insertions(+), 20 deletions(-) diff --git a/validator.go b/validator.go index 36fd3ff..a77cbb5 100644 --- a/validator.go +++ b/validator.go @@ -20,21 +20,22 @@ import ( ) const ( - utf8HexComma = "0x2C" - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - required = "required" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" failed with error(s): %s" - mapErrMsg = "Field validation for \"%s\" failed with key \"%v\" failed with error(s): %s" - structErrMsg = "Struct:%s\n" - diveTag = "dive" - diveSplit = "," + diveTag - indexFieldName = "%s[%d]" + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" + diveSplit = "," + diveTag + arrayIndexFieldName = "%s[%d]" + mapIndexFieldName = "%s[%v]" ) var structPool *pool @@ -670,7 +671,18 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f } } else if cField.isMap { - // return if error here + if errs := v.traverseMap(val, current, valueField, cField); errs != nil && len(errs) > 0 { + + return &FieldError{ + Field: cField.name, + Kind: cField.kind, + Type: cField.typ, + Value: f, + IsPlaceholderErr: true, + IsMap: true, + MapErrs: errs, + } + } } else { // throw error, if not a slice or map then should not have gotten here panic("dive error! can't dive on a non slice or map") @@ -680,6 +692,65 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f return nil } +func (v *Validate) traverseMap(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[interface{}]error { + + errs := map[interface{}]error{} + + for _, key := range valueField.MapKeys() { + + idxField := valueField.MapIndex(key) + + if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + cField.sliceSubKind = idxField.Kind() + } + + switch cField.sliceSubKind { + case reflect.Struct, reflect.Interface: + + if cField.isTimeSubtype { + + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + + continue + } + + if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + + if strings.Contains(cField.tag, omitempty) { + continue + } + + if strings.Contains(cField.tag, required) { + + errs[key.Interface()] = &FieldError{ + Field: cField.name, + Tag: required, + Value: idxField.Interface(), + Kind: reflect.Ptr, + Type: cField.sliceSubtype, + } + } + + continue + } + + if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { + errs[key.Interface()] = structErrors + } + + default: + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + } + } + + return errs +} + func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, valueField reflect.Value, cField *cachedField) map[int]error { errs := map[int]error{} @@ -698,7 +769,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if cField.isTimeSubtype { - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, cField.name, false, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } @@ -730,7 +801,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va } default: - if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(indexFieldName, cField.name, i), false, nil); fieldError != nil { + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { errs[i] = fieldError } } diff --git a/validator_test.go b/validator_test.go index a1086ab..207c948 100644 --- a/validator_test.go +++ b/validator_test.go @@ -227,6 +227,18 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e } func TestMapDiveValidation(t *testing.T) { + + type Test struct { + Errs map[int]string `validate:"gt=0,dive,required"` + } + + test := &Test{ + Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + } + + errs := validate.Struct(test) + + fmt.Println(errs) } func TestArrayDiveValidation(t *testing.T) { @@ -509,7 +521,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.IsPlaceholderErr, false) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Field, "Errs[2][1]") Equal(t, innerSliceError1.Tag, required) type TestMultiDimensionalTimeTime2 struct { @@ -551,7 +563,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.IsPlaceholderErr, false) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) - Equal(t, innerSliceError1.Field, "Errs[2]") + Equal(t, innerSliceError1.Field, "Errs[2][1]") Equal(t, innerSliceError1.Tag, required) } From 8bf793acde6d57df6e685975933c9761632b38fa Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 08:22:37 -0400 Subject: [PATCH 33/52] correct map references pointing to slice after copy/paste for#78 --- validator.go | 60 +++++++++++++++++++----------------------- validator_test.go | 67 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 39 deletions(-) diff --git a/validator.go b/validator.go index a77cbb5..b083a4d 100644 --- a/validator.go +++ b/validator.go @@ -20,20 +20,20 @@ import ( ) const ( - utf8HexComma = "0x2C" - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - required = "required" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" - mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" - structErrMsg = "Struct:%s\n" - diveTag = "dive" - diveSplit = "," + diveTag + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" + // diveSplit = "," + diveTag arrayIndexFieldName = "%s[%d]" mapIndexFieldName = "%s[%v]" ) @@ -457,7 +457,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter case reflect.Slice, reflect.Array: cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.sliceSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { @@ -469,7 +469,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter case reflect.Map: cField.isMap = true cField.mapSubtype = cField.typ.Elem() - cField.isTimeSubtype = cField.mapSubtype == reflect.TypeOf(time.Time{}) + cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) cField.mapSubKind = cField.mapSubtype.Kind() if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, cField); fieldError != nil { @@ -537,11 +537,13 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f switch cField.kind { case reflect.Slice, reflect.Array: + isSingleField = false // cached tags mean nothing because it will be split up while diving cField.isSliceOrArray = true cField.sliceSubtype = cField.typ.Elem() cField.isTimeSubtype = (cField.sliceSubtype == reflect.TypeOf(time.Time{}) || cField.sliceSubtype == reflect.TypeOf(&time.Time{})) cField.sliceSubKind = cField.sliceSubtype.Kind() case reflect.Map: + isSingleField = false // cached tags mean nothing because it will be split up while diving cField.isMap = true cField.mapSubtype = cField.typ.Elem() cField.isTimeSubtype = (cField.mapSubtype == reflect.TypeOf(time.Time{}) || cField.mapSubtype == reflect.TypeOf(&time.Time{})) @@ -556,7 +558,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f case reflect.Struct, reflect.Interface, reflect.Invalid: if cField.typ != reflect.TypeOf(time.Time{}) { - panic("Invalid field passed to ValidateFieldWithTag") + panic("Invalid field passed to fieldWithNameAndValue") } } @@ -568,22 +570,12 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f if !isCached { - for k, t := range strings.Split(tag, tagSeparator) { + for _, t := range strings.Split(tag, tagSeparator) { if t == diveTag { cField.dive = true - - if k == 0 { - if len(tag) == 4 { - cField.diveTag = "" - } else { - cField.diveTag = tag[5:] - } - } else { - cField.diveTag = strings.SplitN(tag, diveSplit, 2)[1][1:] - } - + cField.diveTag = strings.TrimLeft(strings.SplitN(tag, diveTag, 2)[1], ",") break } @@ -700,12 +692,14 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField idxField := valueField.MapIndex(key) - if cField.sliceSubKind == reflect.Ptr && !idxField.IsNil() { + if cField.mapSubKind == reflect.Ptr && !idxField.IsNil() { idxField = idxField.Elem() - cField.sliceSubKind = idxField.Kind() + cField.mapSubKind = idxField.Kind() } - switch cField.sliceSubKind { + // fmt.Println(cField.sliceSubKind) + + switch cField.mapSubKind { case reflect.Struct, reflect.Interface: if cField.isTimeSubtype { @@ -730,7 +724,7 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, - Type: cField.sliceSubtype, + Type: cField.mapSubtype, } } diff --git a/validator_test.go b/validator_test.go index 207c948..90ee5ee 100644 --- a/validator_test.go +++ b/validator_test.go @@ -228,21 +228,76 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e func TestMapDiveValidation(t *testing.T) { - type Test struct { - Errs map[int]string `validate:"gt=0,dive,required"` + m := map[int]string{0: "ok", 3: "", 4: "ok"} + + err := validate.Field(m, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsMap, true) + Equal(t, len(err.MapErrs), 1) + + err = validate.Field(m, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsMap, false) + Equal(t, len(err.MapErrs), 0) + + type Inner struct { + Name string `validate:"required"` } - test := &Test{ - Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + type TestMapStruct struct { + Errs map[int]Inner `validate:"gt=0,dive"` } - errs := validate.Struct(test) + mi := map[int]Inner{0: Inner{"ok"}, 3: Inner{""}, 4: Inner{"ok"}} + + ms := &TestMapStruct{ + Errs: mi, + } + + errs := validate.Struct(ms) fmt.Println(errs) + + // type Test struct { + // Errs map[int]string `validate:"gt=0,dive,required"` + // } + + // test := &Test{ + // Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, + // } + + // errs := validate.Struct(test) + // NotEqual(t, errs, nil) } func TestArrayDiveValidation(t *testing.T) { + arr := []string{"ok", "", "ok"} + + err := validate.Field(arr, "len=3,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsSliceOrArray, true) + Equal(t, len(err.SliceOrArrayErrs), 1) + + err = validate.Field(arr, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsSliceOrArray, false) + Equal(t, len(err.SliceOrArrayErrs), 0) + + type BadDive struct { + Name string `validate:"dive"` + } + + bd := &BadDive{ + Name: "TEST", + } + + PanicMatches(t, func() { validate.Struct(bd) }, "dive error! can't dive on a non slice or map") + type Test struct { Errs []string `validate:"gt=0,dive,required"` } @@ -3204,7 +3259,7 @@ func TestInvalidField(t *testing.T) { Test: "1", } - PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to ValidateFieldWithTag") + PanicMatches(t, func() { validate.Field(s, "required") }, "Invalid field passed to fieldWithNameAndValue") } func TestInvalidTagField(t *testing.T) { From 200a5b4aad158afb5980788aae5ffde87009f133 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 13:41:33 -0400 Subject: [PATCH 34/52] finish map error handling & complete test coverage for #78 --- validator.go | 6 +-- validator_test.go | 105 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 14 deletions(-) diff --git a/validator.go b/validator.go index b083a4d..f421c87 100644 --- a/validator.go +++ b/validator.go @@ -697,8 +697,6 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField cField.mapSubKind = idxField.Kind() } - // fmt.Println(cField.sliceSubKind) - switch cField.mapSubKind { case reflect.Struct, reflect.Interface: @@ -720,7 +718,7 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField if strings.Contains(cField.tag, required) { errs[key.Interface()] = &FieldError{ - Field: cField.name, + Field: fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, @@ -779,7 +777,7 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va if strings.Contains(cField.tag, required) { errs[i] = &FieldError{ - Field: cField.name, + Field: fmt.Sprintf(arrayIndexFieldName, cField.name, i), Tag: required, Value: idxField.Interface(), Kind: reflect.Ptr, diff --git a/validator_test.go b/validator_test.go index 90ee5ee..77e4e15 100644 --- a/validator_test.go +++ b/validator_test.go @@ -257,19 +257,95 @@ func TestMapDiveValidation(t *testing.T) { } errs := validate.Struct(ms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + structErr, ok := fieldError.MapErrs[3].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(structErr.Errors), 1) + + innerErr := structErr.Errors["Name"] + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Name") + Equal(t, innerErr.Tag, "required") + + type TestMapTimeStruct struct { + Errs map[int]*time.Time `validate:"gt=0,dive,required"` + } + + t1 := time.Now().UTC() + + mta := map[int]*time.Time{0: &t1, 3: nil, 4: nil} + + mt := &TestMapTimeStruct{ + Errs: mta, + } + + errs = validate.Struct(mt) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) - fmt.Println(errs) + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 2) - // type Test struct { - // Errs map[int]string `validate:"gt=0,dive,required"` - // } + innerErr, ok = fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerErr.IsPlaceholderErr, false) + Equal(t, innerErr.IsMap, false) + Equal(t, len(innerErr.MapErrs), 0) + Equal(t, innerErr.Field, "Errs[3]") + Equal(t, innerErr.Tag, "required") - // test := &Test{ - // Errs: map[int]string{0: "ok", 1: "", 4: "ok"}, - // } + type TestMapStructPtr struct { + Errs map[int]*Inner `validate:"gt=0,dive,required"` + } - // errs := validate.Struct(test) - // NotEqual(t, errs, nil) + mip := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsMap, false) + Equal(t, len(innerFieldError.MapErrs), 0) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "required") + + type TestMapStructPtr2 struct { + Errs map[int]*Inner `validate:"gt=0,dive,omitempty,required"` + } + + mip2 := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp2 := &TestMapStructPtr2{ + Errs: mip2, + } + + errs = validate.Struct(msp2) + Equal(t, errs, nil) } func TestArrayDiveValidation(t *testing.T) { @@ -437,6 +513,8 @@ func TestArrayDiveValidation(t *testing.T) { errs = validate.Struct(tmsp) NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) fieldErr, ok = errs.Errors["Errs"] Equal(t, ok, true) @@ -483,7 +561,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, fieldErr.IsSliceOrArray, true) Equal(t, len(fieldErr.SliceOrArrayErrs), 3) - sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) Equal(t, ok, true) Equal(t, sliceError1.IsPlaceholderErr, true) Equal(t, sliceError1.IsSliceOrArray, true) @@ -493,6 +571,13 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, ok, true) Equal(t, len(innerSliceStructError1.Errors), 1) + innerSliceStructError2, ok := sliceError1.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceStructError2.IsPlaceholderErr, false) + Equal(t, innerSliceStructError2.IsSliceOrArray, false) + Equal(t, len(innerSliceStructError2.SliceOrArrayErrs), 0) + Equal(t, innerSliceStructError2.Field, "Errs[2][2]") + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] Equal(t, innerInnersliceError1.IsPlaceholderErr, false) Equal(t, innerInnersliceError1.IsSliceOrArray, false) From 22aaa55c7c788576a93bccd26018bb8b9efef55d Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 27 Jun 2015 14:08:07 -0400 Subject: [PATCH 35/52] add dive documentation for #78 --- doc.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc.go b/doc.go index 89142e0..06c940b 100644 --- a/doc.go +++ b/doc.go @@ -173,6 +173,25 @@ Here is a list of the current built in validators: such as min or max won't run, but if a value is set validation will run. (Usage: omitempty) + dive + This tells the validator to dive into a slice, array or map and validate that + level of the slice, array or map with the validation tags that follow. + Multidimensional nesting is also supported, each level you with to dive will + require another dive tag. (Usage: dive) + Example: [][]string with validation tag "gt=0,dive,len=1,dive,required" + gt=0 will be applied to [] + len=1 will be applied to []string + required will be applied to string + Example2: [][]string with validation tag "gt=0,dive,dive,required" + gt=0 will be applied to [] + []string will be spared validation + required will be applied to string + NOTE: in Example2 if the required validation failed, but all others passed + the hierarchy of FieldError's in the middle with have their IsPlaceHolder field + set to true. If a FieldError has IsSliceOrMap=true or IsMap=true then the + FieldError is a Slice or Map field and if IsPlaceHolder=true then contains errors + within its SliceOrArrayErrs or MapErrs fields. + required This validates that the value is not the data types default value. For numbers ensures value is not zero. For strings ensures value is From c7ff296dca4541b59f122426de1600da5c51cf8d Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 28 Jun 2015 21:51:39 -0400 Subject: [PATCH 36/52] correct interface issue add handling of interface validation by determining it's type for issue #85 --- validator.go | 140 +++++++++++++++++++++++++---- validator_test.go | 220 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+), 16 deletions(-) diff --git a/validator.go b/validator.go index f421c87..db01f1f 100644 --- a/validator.go +++ b/validator.go @@ -353,7 +353,6 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter structName = structType.Name() numFields = structValue.NumField() cs = &cachedStruct{name: structName, children: numFields} - structCache.Set(structType, cs) } validationErrors := structPool.Borrow() @@ -429,24 +428,62 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter continue } - if valueField.Kind() == reflect.Ptr && valueField.IsNil() { + if (valueField.Kind() == reflect.Ptr || cField.kind == reflect.Interface) && valueField.IsNil() { if strings.Contains(cField.tag, omitempty) { - continue + goto CACHEFIELD } - if strings.Contains(cField.tag, required) { + tags := strings.Split(cField.tag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } validationErrors.Errors[cField.name] = &FieldError{ Field: cField.name, - Tag: required, + Tag: vals[0], + Param: param, Value: valueField.Interface(), + Kind: valueField.Kind(), + Type: valueField.Type(), } - continue + goto CACHEFIELD + } + } + + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if cField.kind == reflect.Interface { + + valueField = valueField.Elem() + + if valueField.Kind() == reflect.Ptr && !valueField.IsNil() { + valueField = valueField.Elem() + } + + if valueField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(top, current, valueField.Interface(), cField.tag, cField.name, false, nil); fieldError != nil { + validationErrors.Errors[fieldError.Field] = fieldError + // free up memory reference + fieldError = nil } + + goto CACHEFIELD } + VALIDATESTRUCT: if structErrors := v.structRecursive(top, valueField.Interface(), valueField.Interface()); structErrors != nil { validationErrors.StructErrors[cField.name] = structErrors // free up memory map no longer needed @@ -486,11 +523,14 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter } } + CACHEFIELD: if !isCached { cs.fields = append(cs.fields, cField) } } + structCache.Set(structType, cs) + if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 { structPool.Return(validationErrors) return nil @@ -709,19 +749,29 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField continue } - if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + if (idxField.Kind() == reflect.Ptr || idxField.Kind() == reflect.Interface) && idxField.IsNil() { - if strings.Contains(cField.tag, omitempty) { + if strings.Contains(cField.diveTag, omitempty) { continue } - if strings.Contains(cField.tag, required) { + tags := strings.Split(cField.diveTag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } errs[key.Interface()] = &FieldError{ Field: fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), - Tag: required, + Tag: vals[0], + Param: param, Value: idxField.Interface(), - Kind: reflect.Ptr, + Kind: idxField.Kind(), Type: cField.mapSubtype, } } @@ -729,6 +779,30 @@ func (v *Validate) traverseMap(val interface{}, current interface{}, valueField continue } + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if idxField.Kind() == reflect.Interface { + + idxField = idxField.Elem() + + if idxField.Kind() == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + } + + if idxField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(mapIndexFieldName, cField.name, key.Interface()), false, nil); fieldError != nil { + errs[key.Interface()] = fieldError + } + + continue + } + + VALIDATESTRUCT: if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { errs[key.Interface()] = structErrors } @@ -768,19 +842,29 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va continue } - if idxField.Kind() == reflect.Ptr && idxField.IsNil() { + if (idxField.Kind() == reflect.Ptr || idxField.Kind() == reflect.Interface) && idxField.IsNil() { - if strings.Contains(cField.tag, omitempty) { + if strings.Contains(cField.diveTag, omitempty) { continue } - if strings.Contains(cField.tag, required) { + tags := strings.Split(cField.diveTag, tagSeparator) + + if len(tags) > 0 { + + var param string + vals := strings.SplitN(tags[0], tagKeySeparator, 2) + + if len(vals) > 1 { + param = vals[1] + } errs[i] = &FieldError{ Field: fmt.Sprintf(arrayIndexFieldName, cField.name, i), - Tag: required, + Tag: vals[0], + Param: param, Value: idxField.Interface(), - Kind: reflect.Ptr, + Kind: idxField.Kind(), Type: cField.sliceSubtype, } } @@ -788,6 +872,30 @@ func (v *Validate) traverseSliceOrArray(val interface{}, current interface{}, va continue } + // if we get here, the field is interface and could be a struct or a field + // and we need to check the inner type and validate + if idxField.Kind() == reflect.Interface { + + idxField = idxField.Elem() + + if idxField.Kind() == reflect.Ptr && !idxField.IsNil() { + idxField = idxField.Elem() + } + + if idxField.Kind() == reflect.Struct { + goto VALIDATESTRUCT + } + + // sending nil for cField as it was type interface and could be anything + // each time and so must be calculated each time and can't be cached reliably + if fieldError := v.fieldWithNameAndValue(val, current, idxField.Interface(), cField.diveTag, fmt.Sprintf(arrayIndexFieldName, cField.name, i), false, nil); fieldError != nil { + errs[i] = fieldError + } + + continue + } + + VALIDATESTRUCT: if structErrors := v.structRecursive(val, current, idxField.Interface()); structErrors != nil { errs[i] = structErrors } diff --git a/validator_test.go b/validator_test.go index 77e4e15..4edc1a9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,226 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestInterfaceErrValidation(t *testing.T) { + + var v1 interface{} + var v2 interface{} + + v2 = 1 + v1 = v2 + + err := validate.Field(v1, "len=1") + Equal(t, err, nil) + err = validate.Field(v2, "len=1") + Equal(t, err, nil) + + type ExternalCMD struct { + Userid string `json:"userid"` + Action uint32 `json:"action"` + Data interface{} `json:"data,omitempty" validate:"required"` + } + + s := &ExternalCMD{ + Userid: "123456", + Action: 10000, + // Data: 1, + } + + errs := validate.Struct(s) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "required") + + type ExternalCMD2 struct { + Userid string `json:"userid"` + Action uint32 `json:"action"` + Data interface{} `json:"data,omitempty" validate:"len=1"` + } + + s2 := &ExternalCMD2{ + Userid: "123456", + Action: 10000, + // Data: 1, + } + + errs = validate.Struct(s2) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "len") + Equal(t, errs.Errors["Data"].Param, "1") + + s3 := &ExternalCMD2{ + Userid: "123456", + Action: 10000, + Data: 2, + } + + errs = validate.Struct(s3) + NotEqual(t, errs, nil) + Equal(t, errs.Errors["Data"].Field, "Data") + Equal(t, errs.Errors["Data"].Tag, "len") + Equal(t, errs.Errors["Data"].Param, "1") + + type Inner struct { + Name string `validate:"required"` + } + + inner := &Inner{ + Name: "", + } + + s4 := &ExternalCMD{ + Userid: "123456", + Action: 10000, + Data: inner, + } + + errs = validate.Struct(s4) + NotEqual(t, errs, nil) + Equal(t, errs.StructErrors["Data"].Struct, "Inner") + Equal(t, errs.StructErrors["Data"].Errors["Name"].Field, "Name") + Equal(t, errs.StructErrors["Data"].Errors["Name"].Tag, "required") + + type TestMapStructPtr struct { + Errs map[int]interface{} `validate:"gt=0,dive,len=2"` + } + + mip := map[int]interface{}{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsMap, false) + Equal(t, len(innerFieldError.MapErrs), 0) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "len") + + type TestMultiDimensionalStructs struct { + Errs [][]interface{} `validate:"gt=0,dive,dive,len=2"` + } + + var errStructArray [][]interface{} + + errStructArray = append(errStructArray, []interface{}{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructArray = append(errStructArray, []interface{}{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + + tms := &TestMultiDimensionalStructs{ + Errs: errStructArray, + } + + errs = validate.Struct(tms) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok := errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 2) + + sliceError1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok := sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerInnersliceError1 := innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + type TestMultiDimensionalStructsPtr2 struct { + Errs [][]*Inner `validate:"gt=0,dive,dive,len=2"` + } + + var errStructPtr2Array [][]*Inner + + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{""}}) + errStructPtr2Array = append(errStructPtr2Array, []*Inner{&Inner{"ok"}, &Inner{""}, nil}) + + tmsp2 := &TestMultiDimensionalStructsPtr2{ + Errs: errStructPtr2Array, + } + + errs = validate.Struct(tmsp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldErr, ok = errs.Errors["Errs"] + Equal(t, ok, true) + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + + sliceError1, ok = fieldErr.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, sliceError1.IsPlaceholderErr, true) + Equal(t, sliceError1.IsSliceOrArray, true) + Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + + innerSliceStructError1, ok = sliceError1.SliceOrArrayErrs[1].(*StructErrors) + Equal(t, ok, true) + Equal(t, len(innerSliceStructError1.Errors), 1) + + innerSliceStructError2, ok := sliceError1.SliceOrArrayErrs[2].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSliceStructError2.IsPlaceholderErr, false) + Equal(t, innerSliceStructError2.IsSliceOrArray, false) + Equal(t, len(innerSliceStructError2.SliceOrArrayErrs), 0) + Equal(t, innerSliceStructError2.Field, "Errs[2][2]") + + innerInnersliceError1 = innerSliceStructError1.Errors["Name"] + Equal(t, innerInnersliceError1.IsPlaceholderErr, false) + Equal(t, innerInnersliceError1.IsSliceOrArray, false) + Equal(t, len(innerInnersliceError1.SliceOrArrayErrs), 0) + + m := map[int]interface{}{0: "ok", 3: "", 4: "ok"} + + err = validate.Field(m, "len=3,dive,len=2") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsMap, true) + Equal(t, len(err.MapErrs), 1) + + err = validate.Field(m, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsMap, false) + Equal(t, len(err.MapErrs), 0) + + arr := []interface{}{"ok", "", "ok"} + + err = validate.Field(arr, "len=3,dive,len=2") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, true) + Equal(t, err.IsSliceOrArray, true) + Equal(t, len(err.SliceOrArrayErrs), 1) + + err = validate.Field(arr, "len=2,dive,required") + NotEqual(t, err, nil) + Equal(t, err.IsPlaceholderErr, false) + Equal(t, err.IsSliceOrArray, false) + Equal(t, len(err.SliceOrArrayErrs), 0) +} + func TestMapDiveValidation(t *testing.T) { m := map[int]string{0: "ok", 3: "", 4: "ok"} From 7d55bfddde2ac1febee4ff0aeebb0934409c9b43 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 29 Jun 2015 09:48:57 -0400 Subject: [PATCH 37/52] complete flatten logic for array elements for #85 --- validator.go | 158 ++++++++++++++++++++++++++++++++++++++++------ validator_test.go | 71 +++++++++++++++++++++ 2 files changed, 209 insertions(+), 20 deletions(-) diff --git a/validator.go b/validator.go index db01f1f..f70948d 100644 --- a/validator.go +++ b/validator.go @@ -20,20 +20,19 @@ import ( ) const ( - utf8HexComma = "0x2C" - tagSeparator = "," - orSeparator = "|" - noValidationTag = "-" - tagKeySeparator = "=" - structOnlyTag = "structonly" - omitempty = "omitempty" - required = "required" - fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" - sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" - mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" - structErrMsg = "Struct:%s\n" - diveTag = "dive" - // diveSplit = "," + diveTag + utf8HexComma = "0x2C" + tagSeparator = "," + orSeparator = "|" + noValidationTag = "-" + tagKeySeparator = "=" + structOnlyTag = "structonly" + omitempty = "omitempty" + required = "required" + fieldErrMsg = "Field validation for \"%s\" failed on the \"%s\" tag" + sliceErrMsg = "Field validation for \"%s\" failed at index \"%d\" with error(s): %s" + mapErrMsg = "Field validation for \"%s\" failed on key \"%v\" with error(s): %s" + structErrMsg = "Struct:%s\n" + diveTag = "dive" arrayIndexFieldName = "%s[%d]" mapIndexFieldName = "%s[%v]" ) @@ -193,6 +192,112 @@ func (e *FieldError) Error() string { return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } +// func (e *FieldError) flatten(isFromStruct bool) map[string]*FieldError { + +// errs := map[string]*FieldError{} + +// if e.IsPlaceholderErr { + +// if e.IsSliceOrArray { +// for key, err := range e.SliceOrArrayErrs { + +// fe, ok := err.(*FieldError) + +// if ok { + +// if flat := fe.flatten(isFromStruct); flat != nil && len(flat) > 0 { +// for k, v := range flat { +// errs[fmt.Sprintf("[%#v]%s", key, k)] = v +// } +// } +// } else { + +// se := err.(*StructErrors) + +// if flat := se.flatten(isFromStruct); flat != nil && len(flat) > 0 { +// for k, v := range flat { +// errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v +// } +// } +// } +// } + +// } + +// if e.IsMap { +// // for _, err := range e.MapErrs { + +// // if flat := err.Flatten(); flat != nil && len(flat) > 0 { +// // for k, v := range flat { +// // errs[k] = v +// // } +// // } +// // } +// } + +// return errs +// } + +// errs[e.Field] = e + +// return errs +// } + +// Flatten flattens the FieldError hierarchical structure into a flat namespace style field name +// for those that want/need it. +// This is now needed because of the new dive functionality +func (e *FieldError) Flatten() map[string]*FieldError { + + // return e.flatten(false) + errs := map[string]*FieldError{} + + if e.IsPlaceholderErr { + + if e.IsSliceOrArray { + for key, err := range e.SliceOrArrayErrs { + + fe, ok := err.(*FieldError) + + if ok { + + if flat := fe.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } + } + } else { + + se := err.(*StructErrors) + + if flat := se.flatten(false); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v + } + } + } + } + + } + + if e.IsMap { + // for _, err := range e.MapErrs { + + // if flat := err.Flatten(); flat != nil && len(flat) > 0 { + // for k, v := range flat { + // errs[k] = v + // } + // } + // } + } + + return errs + } + + errs[e.Field] = e + + return errs +} + // StructErrors is hierarchical list of field and struct validation errors // for a non hierarchical representation please see the Flatten method for StructErrors type StructErrors struct { @@ -222,10 +327,7 @@ func (e *StructErrors) Error() string { return strings.TrimSpace(buff.String()) } -// Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name -// for those that want/need it -func (e *StructErrors) Flatten() map[string]*FieldError { - +func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { if e == nil { return nil } @@ -234,12 +336,22 @@ func (e *StructErrors) Flatten() map[string]*FieldError { for _, f := range e.Errors { - errs[f.Field] = f + if flat := f.Flatten(); flat != nil && len(flat) > 0 { + + for k, fe := range flat { + + if isFromStruct && f.Field[0:1] == "[" { + errs[f.Field+k] = fe + } else { + errs[k] = fe + } + } + } } for key, val := range e.StructErrors { - otherErrs := val.Flatten() + otherErrs := val.flatten(isFromStruct) for _, f2 := range otherErrs { @@ -251,6 +363,12 @@ func (e *StructErrors) Flatten() map[string]*FieldError { return errs } +// Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name +// for those that want/need it +func (e *StructErrors) Flatten() map[string]*FieldError { + return e.flatten(true) +} + // Func accepts all values needed for file and cross field validation // top = top level struct when validating by struct otherwise nil // current = current level struct when validating by struct otherwise optional comparison value diff --git a/validator_test.go b/validator_test.go index 4edc1a9..53ab455 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,51 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestFlattenValidation(t *testing.T) { + + type Inner struct { + Name string `validate:"required"` + } + + type TestMultiDimensionalStructsPtr struct { + Errs [][]*Inner `validate:"gt=0,dive,dive"` + } + + var errStructPtrArray [][]*Inner + + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, &Inner{""}, &Inner{"ok"}}) + + tmsp := &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs := validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldErr := errs.Errors["Errs"] + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, fieldErr.Field, "Errs") + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerSlice1, ok := fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSlice1.IsPlaceholderErr, true) + Equal(t, innerSlice1.Field, "Errs[0]") + + flatFieldErr, ok := fieldErr.Flatten()["[0][1].Inner.Name"] + Equal(t, ok, true) + Equal(t, flatFieldErr.Field, "Name") + Equal(t, flatFieldErr.Tag, "required") + + // expect Errs[0][1].Inner.Name = error + // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).Field) + // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).IsPlaceholderErr) +} + func TestInterfaceErrValidation(t *testing.T) { var v1 interface{} @@ -578,6 +623,11 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, err.IsSliceOrArray, true) Equal(t, len(err.SliceOrArrayErrs), 1) + // flat := err.Flatten() + // fe, ok := flat["[1]"] + // Equal(t, ok, true) + // Equal(t, fe.Tag, "required") + err = validate.Field(arr, "len=2,dive,required") NotEqual(t, err, nil) Equal(t, err.IsPlaceholderErr, false) @@ -606,6 +656,12 @@ func TestArrayDiveValidation(t *testing.T) { NotEqual(t, errs, nil) Equal(t, len(errs.Errors), 1) + // flat = errs.Flatten() + // me, ok := flat["Errs[1]"] + // Equal(t, ok, true) + // Equal(t, me.Field, "Errs[1]") + // Equal(t, me.Tag, "required") + fieldErr, ok := errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) @@ -666,6 +722,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, sliceError1.IsPlaceholderErr, true) Equal(t, sliceError1.IsSliceOrArray, true) Equal(t, len(sliceError1.SliceOrArrayErrs), 2) + Equal(t, sliceError1.Field, "Errs[0]") innerSliceError1, ok := sliceError1.SliceOrArrayErrs[1].(*FieldError) Equal(t, ok, true) @@ -673,6 +730,7 @@ func TestArrayDiveValidation(t *testing.T) { Equal(t, innerSliceError1.Tag, required) Equal(t, innerSliceError1.IsSliceOrArray, false) Equal(t, len(innerSliceError1.SliceOrArrayErrs), 0) + Equal(t, innerSliceError1.Field, "Errs[0][1]") type Inner struct { Name string `validate:"required"` @@ -736,12 +794,25 @@ func TestArrayDiveValidation(t *testing.T) { // for full test coverage fmt.Sprint(errs.Error()) + // flat := errs.Flatten() + // // fmt.Println(errs) + // fmt.Println(flat) + // expect Errs[0][1].Inner.Name + // me, ok := flat["Errs[1]"] + // Equal(t, ok, true) + // Equal(t, me.Field, "Errs[1]") + // Equal(t, me.Tag, "required") + fieldErr, ok = errs.Errors["Errs"] Equal(t, ok, true) Equal(t, fieldErr.IsPlaceholderErr, true) Equal(t, fieldErr.IsSliceOrArray, true) Equal(t, len(fieldErr.SliceOrArrayErrs), 3) + // flat := fieldErr.Flatten() + // fmt.Println(errs) + // fmt.Println(flat) + sliceError1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) Equal(t, ok, true) Equal(t, sliceError1.IsPlaceholderErr, true) From 4d571655620e6785b67c708b19e2cf144350ba4b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 29 Jun 2015 09:53:08 -0400 Subject: [PATCH 38/52] fix wrong variable used in checking for "[" char for #85 --- validator.go | 34 +++++++++++++++++++++++----------- validator_test.go | 5 +++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/validator.go b/validator.go index f70948d..8bbd891 100644 --- a/validator.go +++ b/validator.go @@ -248,7 +248,6 @@ func (e *FieldError) Error() string { // This is now needed because of the new dive functionality func (e *FieldError) Flatten() map[string]*FieldError { - // return e.flatten(false) errs := map[string]*FieldError{} if e.IsPlaceholderErr { @@ -276,18 +275,31 @@ func (e *FieldError) Flatten() map[string]*FieldError { } } } - } if e.IsMap { - // for _, err := range e.MapErrs { - - // if flat := err.Flatten(); flat != nil && len(flat) > 0 { - // for k, v := range flat { - // errs[k] = v - // } - // } - // } + for key, err := range e.MapErrs { + + fe, ok := err.(*FieldError) + + if ok { + + if flat := fe.Flatten(); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } + } + } else { + + se := err.(*StructErrors) + + if flat := se.flatten(false); flat != nil && len(flat) > 0 { + for k, v := range flat { + errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v + } + } + } + } } return errs @@ -340,7 +352,7 @@ func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { for k, fe := range flat { - if isFromStruct && f.Field[0:1] == "[" { + if isFromStruct && k[0:1] == "[" { errs[f.Field+k] = fe } else { errs[k] = fe diff --git a/validator_test.go b/validator_test.go index 53ab455..4c4e4a9 100644 --- a/validator_test.go +++ b/validator_test.go @@ -266,6 +266,11 @@ func TestFlattenValidation(t *testing.T) { Equal(t, flatFieldErr.Field, "Name") Equal(t, flatFieldErr.Tag, "required") + structErrFlatten, ok := errs.Flatten()["Errs[0][1].Inner.Name"] + Equal(t, ok, true) + Equal(t, structErrFlatten.Field, "Name") + Equal(t, structErrFlatten.Tag, "required") + // expect Errs[0][1].Inner.Name = error // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).Field) // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).IsPlaceholderErr) From f604b6cc960649ab353f6b2fc6186837e66a895a Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 29 Jun 2015 20:10:13 -0400 Subject: [PATCH 39/52] Complete Flatten logic for #88 --- validator.go | 21 +++++++-- validator_test.go | 115 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/validator.go b/validator.go index 8bbd891..722c37a 100644 --- a/validator.go +++ b/validator.go @@ -261,7 +261,12 @@ func (e *FieldError) Flatten() map[string]*FieldError { if flat := fe.Flatten(); flat != nil && len(flat) > 0 { for k, v := range flat { - errs[fmt.Sprintf("[%#v]%s", key, k)] = v + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } + } } } else { @@ -286,7 +291,11 @@ func (e *FieldError) Flatten() map[string]*FieldError { if flat := fe.Flatten(); flat != nil && len(flat) > 0 { for k, v := range flat { - errs[fmt.Sprintf("[%#v]%s", key, k)] = v + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } } } } else { @@ -352,7 +361,13 @@ func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { for k, fe := range flat { - if isFromStruct && k[0:1] == "[" { + // fmt.Println(k) + // if isFromStruct && k[0:1] == "[" { + // errs[f.Field+k] = fe + // } else { + // errs[k] = fe + // } + if f.IsPlaceholderErr { errs[f.Field+k] = fe } else { errs[k] = fe diff --git a/validator_test.go b/validator_test.go index 4c4e4a9..1eda6a0 100644 --- a/validator_test.go +++ b/validator_test.go @@ -233,7 +233,7 @@ func TestFlattenValidation(t *testing.T) { } type TestMultiDimensionalStructsPtr struct { - Errs [][]*Inner `validate:"gt=0,dive,dive"` + Errs [][]*Inner `validate:"gt=0,dive,dive,required"` } var errStructPtrArray [][]*Inner @@ -271,9 +271,116 @@ func TestFlattenValidation(t *testing.T) { Equal(t, structErrFlatten.Field, "Name") Equal(t, structErrFlatten.Tag, "required") - // expect Errs[0][1].Inner.Name = error - // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).Field) - // fmt.Println((fieldErr.SliceOrArrayErrs[0].(*FieldError)).IsPlaceholderErr) + errStructPtrArray = [][]*Inner{} + errStructPtrArray = append(errStructPtrArray, []*Inner{&Inner{"ok"}, nil, &Inner{"ok"}}) + + tmsp = &TestMultiDimensionalStructsPtr{ + Errs: errStructPtrArray, + } + + errs = validate.Struct(tmsp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + // for full test coverage + fmt.Sprint(errs.Error()) + + fieldErr = errs.Errors["Errs"] + Equal(t, fieldErr.IsPlaceholderErr, true) + Equal(t, fieldErr.IsSliceOrArray, true) + Equal(t, fieldErr.Field, "Errs") + Equal(t, len(fieldErr.SliceOrArrayErrs), 1) + + innerSlice1, ok = fieldErr.SliceOrArrayErrs[0].(*FieldError) + Equal(t, ok, true) + Equal(t, innerSlice1.IsPlaceholderErr, true) + Equal(t, innerSlice1.Field, "Errs[0]") + + flatFieldErr, ok = fieldErr.Flatten()["[0][1]"] + Equal(t, ok, true) + Equal(t, flatFieldErr.Field, "Errs[0][1]") + Equal(t, flatFieldErr.Tag, "required") + + type TestMapStructPtr struct { + Errs map[int]*Inner `validate:"gt=0,dive,required"` + } + + mip := map[int]*Inner{0: &Inner{"ok"}, 3: &Inner{""}, 4: &Inner{"ok"}} + + msp := &TestMapStructPtr{ + Errs: mip, + } + + errs = validate.Struct(msp) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError := errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerStructError, ok := fieldError.MapErrs[3].(*StructErrors) + Equal(t, ok, true) + Equal(t, innerStructError.Struct, "Inner") + Equal(t, len(innerStructError.Errors), 1) + + innerInnerFieldError, ok := innerStructError.Errors["Name"] + Equal(t, ok, true) + Equal(t, innerInnerFieldError.IsPlaceholderErr, false) + Equal(t, innerInnerFieldError.IsSliceOrArray, false) + Equal(t, innerInnerFieldError.Field, "Name") + Equal(t, innerInnerFieldError.Tag, "required") + + flatErrs, ok := errs.Flatten()["Errs[3].Inner.Name"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Name") + Equal(t, flatErrs.Tag, "required") + + mip2 := map[int]*Inner{0: &Inner{"ok"}, 3: nil, 4: &Inner{"ok"}} + + msp2 := &TestMapStructPtr{ + Errs: mip2, + } + + errs = validate.Struct(msp2) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + fieldError = errs.Errors["Errs"] + Equal(t, fieldError.IsPlaceholderErr, true) + Equal(t, fieldError.IsMap, true) + Equal(t, len(fieldError.MapErrs), 1) + + innerFieldError, ok := fieldError.MapErrs[3].(*FieldError) + Equal(t, ok, true) + Equal(t, innerFieldError.IsPlaceholderErr, false) + Equal(t, innerFieldError.IsSliceOrArray, false) + Equal(t, innerFieldError.Field, "Errs[3]") + Equal(t, innerFieldError.Tag, "required") + + flatErrs, ok = errs.Flatten()["Errs[3]"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Errs[3]") + Equal(t, flatErrs.Tag, "required") + + type TestMapInnerArrayStruct struct { + Errs map[int][]string `validate:"gt=0,dive,dive,required"` + } + + mias := map[int][]string{0: []string{"ok"}, 3: []string{"ok", ""}, 4: []string{"ok"}} + + mia := &TestMapInnerArrayStruct{ + Errs: mias, + } + + errs = validate.Struct(mia) + NotEqual(t, errs, nil) + Equal(t, len(errs.Errors), 1) + + flatErrs, ok = errs.Flatten()["Errs[3][1]"] + Equal(t, ok, true) + Equal(t, flatErrs.Field, "Errs[3][1]") + Equal(t, flatErrs.Tag, "required") } func TestInterfaceErrValidation(t *testing.T) { From 92bd6b335a5adfdab7f754d814f6b0f00953d1c4 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Mon, 29 Jun 2015 20:14:58 -0400 Subject: [PATCH 40/52] code cleanup for #88 --- doc.go | 2 +- validator.go | 74 +++++----------------------------------------------- 2 files changed, 8 insertions(+), 68 deletions(-) diff --git a/doc.go b/doc.go index 06c940b..f45ef34 100644 --- a/doc.go +++ b/doc.go @@ -176,7 +176,7 @@ Here is a list of the current built in validators: dive This tells the validator to dive into a slice, array or map and validate that level of the slice, array or map with the validation tags that follow. - Multidimensional nesting is also supported, each level you with to dive will + Multidimensional nesting is also supported, each level you wish to dive will require another dive tag. (Usage: dive) Example: [][]string with validation tag "gt=0,dive,len=1,dive,required" gt=0 will be applied to [] diff --git a/validator.go b/validator.go index 722c37a..64f0f85 100644 --- a/validator.go +++ b/validator.go @@ -192,57 +192,6 @@ func (e *FieldError) Error() string { return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } -// func (e *FieldError) flatten(isFromStruct bool) map[string]*FieldError { - -// errs := map[string]*FieldError{} - -// if e.IsPlaceholderErr { - -// if e.IsSliceOrArray { -// for key, err := range e.SliceOrArrayErrs { - -// fe, ok := err.(*FieldError) - -// if ok { - -// if flat := fe.flatten(isFromStruct); flat != nil && len(flat) > 0 { -// for k, v := range flat { -// errs[fmt.Sprintf("[%#v]%s", key, k)] = v -// } -// } -// } else { - -// se := err.(*StructErrors) - -// if flat := se.flatten(isFromStruct); flat != nil && len(flat) > 0 { -// for k, v := range flat { -// errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v -// } -// } -// } -// } - -// } - -// if e.IsMap { -// // for _, err := range e.MapErrs { - -// // if flat := err.Flatten(); flat != nil && len(flat) > 0 { -// // for k, v := range flat { -// // errs[k] = v -// // } -// // } -// // } -// } - -// return errs -// } - -// errs[e.Field] = e - -// return errs -// } - // Flatten flattens the FieldError hierarchical structure into a flat namespace style field name // for those that want/need it. // This is now needed because of the new dive functionality @@ -273,7 +222,7 @@ func (e *FieldError) Flatten() map[string]*FieldError { se := err.(*StructErrors) - if flat := se.flatten(false); flat != nil && len(flat) > 0 { + if flat := se.Flatten(); flat != nil && len(flat) > 0 { for k, v := range flat { errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v } @@ -302,7 +251,7 @@ func (e *FieldError) Flatten() map[string]*FieldError { se := err.(*StructErrors) - if flat := se.flatten(false); flat != nil && len(flat) > 0 { + if flat := se.Flatten(); flat != nil && len(flat) > 0 { for k, v := range flat { errs[fmt.Sprintf("[%#v].%s.%s", key, se.Struct, k)] = v } @@ -348,7 +297,10 @@ func (e *StructErrors) Error() string { return strings.TrimSpace(buff.String()) } -func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { +// Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name +// for those that want/need it +func (e *StructErrors) Flatten() map[string]*FieldError { + if e == nil { return nil } @@ -361,12 +313,6 @@ func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { for k, fe := range flat { - // fmt.Println(k) - // if isFromStruct && k[0:1] == "[" { - // errs[f.Field+k] = fe - // } else { - // errs[k] = fe - // } if f.IsPlaceholderErr { errs[f.Field+k] = fe } else { @@ -378,7 +324,7 @@ func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { for key, val := range e.StructErrors { - otherErrs := val.flatten(isFromStruct) + otherErrs := val.Flatten() for _, f2 := range otherErrs { @@ -390,12 +336,6 @@ func (e *StructErrors) flatten(isFromStruct bool) map[string]*FieldError { return errs } -// Flatten flattens the StructErrors hierarchical structure into a flat namespace style field name -// for those that want/need it -func (e *StructErrors) Flatten() map[string]*FieldError { - return e.flatten(true) -} - // Func accepts all values needed for file and cross field validation // top = top level struct when validating by struct otherwise nil // current = current level struct when validating by struct otherwise optional comparison value From 26333a65c158b6923290c8897f17b9d3e21bcc15 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Sat, 4 Jul 2015 13:18:46 -0400 Subject: [PATCH 41/52] add goveralls test coverage for #92 --- .travis.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 68398d9..8ab4aed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,11 @@ go: - 1.2 - 1.3 - 1.4 - - tip \ No newline at end of file + - tip + +before_install: + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi +script: + - $HOME/gopath/bin/goveralls -service=travis-ci From 82abe38795fb0118ad84c584f24d17da2dc9e395 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Sat, 4 Jul 2015 13:25:49 -0400 Subject: [PATCH 42/52] Add test coverage badge add test coverage badge for issue #92 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c1e9d00..08a69b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ Package validator ================ [![Build Status](https://travis-ci.org/bluesuncorp/validator.svg?branch=v5.1)](https://travis-ci.org/bluesuncorp/validator) +[![Coverage Status](https://coveralls.io/repos/bluesuncorp/validator/badge.svg?branch=v5)](https://coveralls.io/r/bluesuncorp/validator?branch=v5) [![GoDoc](https://godoc.org/gopkg.in/bluesuncorp/validator.v5?status.svg)](https://godoc.org/gopkg.in/bluesuncorp/validator.v5) Package validator implements value validations for structs and individual fields based on tags. From 89a590900460b411a8df2f970d2649c6cebc7e32 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 4 Jul 2015 14:08:59 -0400 Subject: [PATCH 43/52] add test to ensure 100% of the lines of code are called, even in the pool --- .gitignore | 3 ++- validator_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4159020..257a8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,5 @@ _testmain.go *.test *.prof *.test -*.out \ No newline at end of file +*.out +cover.html \ No newline at end of file diff --git a/validator_test.go b/validator_test.go index 1eda6a0..da195dc 100644 --- a/validator_test.go +++ b/validator_test.go @@ -14,6 +14,11 @@ import ( // - Run "gocov test | gocov report" to report on test converage by file // - Run "gocov test | gocov annotate -" to report on all code and functions, those ,marked with "MISS" were never called // +// or +// +// -- may be a good idea to change to output path to somewherelike /tmp +// go test -coverprofile cover.out && go tool cover -html=cover.out -o cover.html +// // // go test -cpuprofile cpu.out // ./validator.test -test.bench=. -test.cpuprofile=cpu.prof @@ -3765,3 +3770,23 @@ func TestInvalidValidatorFunction(t *testing.T) { PanicMatches(t, func() { validate.Field(s.Test, "zzxxBadFunction") }, fmt.Sprintf("Undefined validation function on field %s", "")) } + +func TestPoolObjectMaxSizeValidation(t *testing.T) { + // this will ensure that the pool objects are let go + // when the pool is saturated + validate.SetMaxStructPoolSize(0) + + tSuccess := &TestSlice{ + Required: []int{1}, + Len: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, + Min: []int{1, 2}, + Max: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, + MinMax: []int{1, 2, 3, 4, 5}, + OmitEmpty: []int{}, + } + + for i := 0; i < 2; i++ { + err := validate.Struct(tSuccess) + Equal(t, err, nil) + } +} From c4203fff251eb1aa6e322bcb3666d74fbcf3f18b Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 4 Jul 2015 15:29:12 -0400 Subject: [PATCH 44/52] add file and README example --- .gitignore | 3 +- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++--- examples/simple.go | 85 ++++++++++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 examples/simple.go diff --git a/.gitignore b/.gitignore index 257a8ca..7e9b500 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ _testmain.go *.prof *.test *.out -cover.html \ No newline at end of file +cover.html +README.html \ No newline at end of file diff --git a/README.md b/README.md index 08a69b1..c151340 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,15 @@ Package validator [![GoDoc](https://godoc.org/gopkg.in/bluesuncorp/validator.v5?status.svg)](https://godoc.org/gopkg.in/bluesuncorp/validator.v5) Package validator implements value validations for structs and individual fields based on tags. -It is also capable of Cross Field and Cross Struct validations. + +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. Installation -============ +------------ Use go get. @@ -23,12 +28,101 @@ Then import the validator package into your own code. import "gopkg.in/bluesuncorp/validator.v5" Usage and documentation -======================= +------ Please see http://godoc.org/gopkg.in/bluesuncorp/validator.v5 for detailed usage docs. +##### Example: +```go +package main + +import ( + "fmt" + + "gopkg.in/bluesuncorp/validator.v5" +) + +// User contains user information +type User struct { + FirstName string `validate:"required"` + LastName string `validate:"required"` + 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() { + + validate = validator.New("validate", validator.BakedInValidators) + + address := &Address{ + Street: "Eavesdown Docks", + Planet: "Persphone", + Phone: "none", + } + + user := &User{ + FirstName: "Badger", + LastName: "Smith", + Age: 135, + Email: "Badger.Smith@gmail.com", + FavouriteColor: "#000", + Addresses: []*Address{address}, + } + + // returns nil or *StructErrors + errs := validate.Struct(user) + + if errs != nil { + + // err will be of type *FieldError + err := errs.Errors["Age"] + fmt.Println(err.Error()) // output: Field validation for "Age" failed on the "lte" tag + fmt.Println(err.Field) // output: Age + fmt.Println(err.Tag) // output: lte + fmt.Println(err.Kind) // output: uint8 + fmt.Println(err.Type) // output: uint8 + fmt.Println(err.Param) // output: 130 + fmt.Println(err.Value) // output: 135 + + // or if you prefer you can use the Flatten function + // NOTE: I find this usefull when using a more hard static approach of checking field errors. + // The above, is best for passing to some generic code to say parse the errors. i.e. I pass errs + // to a routine which loops through the errors, creates and translates the error message into the + // users locale and returns a map of map[string]string // field and error which I then use + // within the HTML rendering. + + flat := errs.Flatten() + fmt.Println(flat) // output: map[Age:Field validation for "Age" failed on the "lte" tag Addresses[0].Address.City:Field validation for "City" failed on the "required" tag] + err = flat["Addresses[0].Address.City"] + fmt.Println(err.Field) // output: City + fmt.Println(err.Tag) // output: required + 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 +} +``` + How to Contribute -================= +------ There will always be a development branch for each version i.e. `v1-development`. In order to contribute, please make your pull requests against those branches. @@ -41,5 +135,5 @@ I strongly encourage everyone whom creates a custom validation function to contr help make this package even better. License -======= +------ Distributed under MIT License, please see license file in code for more details. diff --git a/examples/simple.go b/examples/simple.go new file mode 100644 index 0000000..59cb1a9 --- /dev/null +++ b/examples/simple.go @@ -0,0 +1,85 @@ +package main + +import ( + "fmt" + + "gopkg.in/bluesuncorp/validator.v5" +) + +// User contains user information +type User struct { + FirstName string `validate:"required"` + LastName string `validate:"required"` + 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() { + + validate = validator.New("validate", validator.BakedInValidators) + + address := &Address{ + Street: "Eavesdown Docks", + Planet: "Persphone", + Phone: "none", + } + + user := &User{ + FirstName: "Badger", + LastName: "Smith", + Age: 135, + Email: "Badger.Smith@gmail.com", + FavouriteColor: "#000", + Addresses: []*Address{address}, + } + + // returns nil or *StructErrors + errs := validate.Struct(user) + + if errs != nil { + + // err will be of type *FieldError + err := errs.Errors["Age"] + fmt.Println(err.Error()) // output: Field validation for "Age" failed on the "lte" tag + fmt.Println(err.Field) // output: Age + fmt.Println(err.Tag) // output: lte + fmt.Println(err.Kind) // output: uint8 + fmt.Println(err.Type) // output: uint8 + fmt.Println(err.Param) // output: 130 + fmt.Println(err.Value) // output: 135 + + // or if you prefer you can use the Flatten function + // NOTE: I find this usefull when using a more hard static approach of checking field errors. + // The above, is best for passing to some generic code to say parse the errors. i.e. I pass errs + // to a routine which loops through the errors, creates and translates the error message into the + // users locale and returns a map of map[string]string // field and error which I then use + // within the HTML rendering. + + flat := errs.Flatten() + fmt.Println(flat) // output: map[Age:Field validation for "Age" failed on the "lte" tag Addresses[0].Address.City:Field validation for "City" failed on the "required" tag] + err = flat["Addresses[0].Address.City"] + fmt.Println(err.Field) // output: City + fmt.Println(err.Tag) // output: required + 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 +} From 46b655a956f3b1d606d05b10f1f2bdf15544c621 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 4 Jul 2015 15:35:14 -0400 Subject: [PATCH 45/52] add some benchmarks to readme for #72 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index c151340..4a4cff1 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,19 @@ func main() { } ``` +Benchmarks +------ +###### Run on MacBook Pro (Retina, 15-inch, Late 2013) 2.6 GHz Intel Core i7 16 GB 1600 MHz DDR3 +```go +$ go test -cpu=4 -bench=. -benchmem=true +PASS +BenchmarkValidateField-4 3000000 436 ns/op 192 B/op 2 allocs/op +BenchmarkValidateStructSimple-4 500000 2863 ns/op 784 B/op 13 allocs/op +BenchmarkTemplateParallelSimple-4 500000 3044 ns/op 784 B/op 13 allocs/op +BenchmarkValidateStructLarge-4 100000 15226 ns/op 4853 B/op 74 allocs/op +BenchmarkTemplateParallelLarge-4 100000 14637 ns/op 4856 B/op 74 allocs/op +``` + How to Contribute ------ From 562e77833a8c4d8432d107a8cb30197f641fb7a2 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 5 Jul 2015 11:31:09 -0400 Subject: [PATCH 46/52] change to use native sync.Pool for #98 --- validator.go | 51 ++++++++++----------------------------------------- 1 file changed, 10 insertions(+), 41 deletions(-) diff --git a/validator.go b/validator.go index 64f0f85..b2ccb95 100644 --- a/validator.go +++ b/validator.go @@ -37,43 +37,13 @@ const ( mapIndexFieldName = "%s[%v]" ) -var structPool *pool +var structPool *sync.Pool -// Pool holds a channelStructErrors. -type pool struct { - pool chan *StructErrors -} - -// NewPool creates a new pool of Clients. -func newPool(max int) *pool { - return &pool{ - pool: make(chan *StructErrors, max), - } -} - -// Borrow a StructErrors from the pool. -func (p *pool) Borrow() *StructErrors { - var c *StructErrors - - select { - case c = <-p.pool: - default: - c = &StructErrors{ - Errors: map[string]*FieldError{}, - StructErrors: map[string]*StructErrors{}, - } - } - - return c -} - -// Return returns a StructErrors to the pool. -func (p *pool) Return(c *StructErrors) { - - select { - case p.pool <- c: - default: - // let it go, let it go... +// returns new *StructErrors to the pool +func newStructErrors() interface{} { + return &StructErrors{ + Errors: map[string]*FieldError{}, + StructErrors: map[string]*StructErrors{}, } } @@ -357,7 +327,7 @@ type Validate struct { // New creates a new Validate instance for use. func New(tagName string, funcs map[string]Func) *Validate { - structPool = newPool(10) + structPool = &sync.Pool{New: newStructErrors} return &Validate{ tagName: tagName, @@ -377,9 +347,8 @@ func (v *Validate) SetTag(tagName string) { // nearly all cases. only increase if you have a deeply nested struct structure. // NOTE: this method is not thread-safe // NOTE: this is only here to keep compatibility with v5, in v6 the method will be removed -// and the max pool size will be passed into the New function func (v *Validate) SetMaxStructPoolSize(max int) { - structPool = newPool(max) + structPool = &sync.Pool{New: newStructErrors} } // AddFunction adds a validation Func to a Validate's map of validators denoted by the key @@ -440,7 +409,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter cs = &cachedStruct{name: structName, children: numFields} } - validationErrors := structPool.Borrow() + validationErrors := structPool.Get().(*StructErrors) validationErrors.Struct = structName for i := 0; i < numFields; i++ { @@ -617,7 +586,7 @@ func (v *Validate) structRecursive(top interface{}, current interface{}, s inter structCache.Set(structType, cs) if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 { - structPool.Return(validationErrors) + structPool.Put(validationErrors) return nil } From 302c3cffddab1559c7b51199213b5b2b2d3fccbb Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 5 Jul 2015 11:39:16 -0400 Subject: [PATCH 47/52] to use new sync pool remove go 1.2 support for #98 --- .travis.yml | 1 - benchmarks_test.go | 148 ++++++++++++++++++++++----------------------- 2 files changed, 74 insertions(+), 75 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8ab4aed..34645c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ notificaitons: on_failure: always go: - - 1.2 - 1.3 - 1.4 - tip diff --git a/benchmarks_test.go b/benchmarks_test.go index 2517209..ee836c2 100644 --- a/benchmarks_test.go +++ b/benchmarks_test.go @@ -24,23 +24,23 @@ func BenchmarkValidateStructSimple(b *testing.B) { } } -// func BenchmarkTemplateParallelSimple(b *testing.B) { +func BenchmarkTemplateParallelSimple(b *testing.B) { -// type Foo struct { -// StringValue string `validate:"min=5,max=10"` -// IntValue int `validate:"min=5,max=10"` -// } + 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} + validFoo := &Foo{StringValue: "Foobar", IntValue: 7} + invalidFoo := &Foo{StringValue: "Fo", IntValue: 3} -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(validFoo) -// validate.Struct(invalidFoo) -// } -// }) -// } + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(validFoo) + validate.Struct(invalidFoo) + } + }) +} func BenchmarkValidateStructLarge(b *testing.B) { @@ -101,63 +101,63 @@ func BenchmarkValidateStructLarge(b *testing.B) { } } -// func BenchmarkTemplateParallelLarge(b *testing.B) { - -// tFail := &TestString{ -// Required: "", -// Len: "", -// Min: "", -// Max: "12345678901", -// MinMax: "", -// Lt: "0123456789", -// Lte: "01234567890", -// Gt: "1", -// Gte: "1", -// OmitEmpty: "12345678901", -// Sub: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "", -// }, -// Iface: &Impl{ -// F: "12", -// }, -// } - -// tSuccess := &TestString{ -// Required: "Required", -// Len: "length==10", -// Min: "min=1", -// Max: "1234567890", -// MinMax: "12345", -// Lt: "012345678", -// Lte: "0123456789", -// Gt: "01234567890", -// Gte: "0123456789", -// OmitEmpty: "", -// Sub: &SubTest{ -// Test: "1", -// }, -// SubIgnore: &SubTest{ -// Test: "", -// }, -// Anonymous: struct { -// A string `validate:"required"` -// }{ -// A: "1", -// }, -// Iface: &Impl{ -// F: "123", -// }, -// } - -// b.RunParallel(func(pb *testing.PB) { -// for pb.Next() { -// validate.Struct(tSuccess) -// validate.Struct(tFail) -// } -// }) -// } +func BenchmarkTemplateParallelLarge(b *testing.B) { + + tFail := &TestString{ + Required: "", + Len: "", + Min: "", + Max: "12345678901", + MinMax: "", + Lt: "0123456789", + Lte: "01234567890", + Gt: "1", + Gte: "1", + OmitEmpty: "12345678901", + Sub: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "", + }, + Iface: &Impl{ + F: "12", + }, + } + + tSuccess := &TestString{ + Required: "Required", + Len: "length==10", + Min: "min=1", + Max: "1234567890", + MinMax: "12345", + Lt: "012345678", + Lte: "0123456789", + Gt: "01234567890", + Gte: "0123456789", + OmitEmpty: "", + Sub: &SubTest{ + Test: "1", + }, + SubIgnore: &SubTest{ + Test: "", + }, + Anonymous: struct { + A string `validate:"required"` + }{ + A: "1", + }, + Iface: &Impl{ + F: "123", + }, + } + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + validate.Struct(tSuccess) + validate.Struct(tFail) + } + }) +} From 9d4c0ec3023262c2c5837f8fa97525dd19cd9358 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sun, 5 Jul 2015 12:17:07 -0400 Subject: [PATCH 48/52] update travis.ml for overalls, which is giving nothing but problems! --- .travis.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 34645c6..389db22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,11 @@ go: - 1.3 - 1.4 - tip - -before_install: - - go get github.com/axw/gocov/gocov - - go get github.com/mattn/goveralls - - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi + script: - - $HOME/gopath/bin/goveralls -service=travis-ci + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go test -v -covermode=count -coverprofile=cover.out + +after_success: + - goveralls -coverprofile=cover.out -service=travis-ci -repotoken I6M8FiXZzErImgwMotJ7fwFlHOX8Hqdq1 \ No newline at end of file From 7422dd4828caf8066fe72718b2acb95968014726 Mon Sep 17 00:00:00 2001 From: Dean Karn Date: Sun, 5 Jul 2015 14:43:14 -0400 Subject: [PATCH 49/52] update latest benchmarks --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4a4cff1..4a22a00 100644 --- a/README.md +++ b/README.md @@ -127,11 +127,11 @@ Benchmarks ```go $ go test -cpu=4 -bench=. -benchmem=true PASS -BenchmarkValidateField-4 3000000 436 ns/op 192 B/op 2 allocs/op -BenchmarkValidateStructSimple-4 500000 2863 ns/op 784 B/op 13 allocs/op -BenchmarkTemplateParallelSimple-4 500000 3044 ns/op 784 B/op 13 allocs/op -BenchmarkValidateStructLarge-4 100000 15226 ns/op 4853 B/op 74 allocs/op -BenchmarkTemplateParallelLarge-4 100000 14637 ns/op 4856 B/op 74 allocs/op +BenchmarkValidateField-4 3000000 429 ns/op 192 B/op 2 allocs/op +BenchmarkValidateStructSimple-4 500000 2877 ns/op 657 B/op 10 allocs/op +BenchmarkTemplateParallelSimple-4 500000 3097 ns/op 657 B/op 10 allocs/op +BenchmarkValidateStructLarge-4 100000 15228 ns/op 4350 B/op 62 allocs/op +BenchmarkTemplateParallelLarge-4 100000 14257 ns/op 4354 B/op 62 allocs/op ``` How to Contribute From c4ae288afde0d7121adee8a0f5fb03ab041d06dc Mon Sep 17 00:00:00 2001 From: The Gitter Badger Date: Mon, 6 Jul 2015 12:00:40 +0000 Subject: [PATCH 50/52] Added Gitter badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4a22a00..36ac9da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ Package validator ================ + +[![Join the chat at https://gitter.im/bluesuncorp/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bluesuncorp/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/bluesuncorp/validator.svg?branch=v5.1)](https://travis-ci.org/bluesuncorp/validator) [![Coverage Status](https://coveralls.io/repos/bluesuncorp/validator/badge.svg?branch=v5)](https://coveralls.io/r/bluesuncorp/validator?branch=v5) [![GoDoc](https://godoc.org/gopkg.in/bluesuncorp/validator.v5?status.svg)](https://godoc.org/gopkg.in/bluesuncorp/validator.v5) From e1fd32247fdca4ef85661aed75b1d5fbd2bb92ba Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 9 Jul 2015 14:06:48 -0400 Subject: [PATCH 51/52] Fix interface issue when value is set but no validation tag exists --- validator.go | 2 +- validator_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/validator.go b/validator.go index b2ccb95..ccd2a55 100644 --- a/validator.go +++ b/validator.go @@ -610,7 +610,7 @@ func (v *Validate) fieldWithNameAndValue(val interface{}, current interface{}, f var valueField reflect.Value // 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 || tag == "" { return nil } diff --git a/validator_test.go b/validator_test.go index da195dc..1b2e803 100644 --- a/validator_test.go +++ b/validator_test.go @@ -606,6 +606,19 @@ func TestInterfaceErrValidation(t *testing.T) { Equal(t, err.IsPlaceholderErr, false) Equal(t, err.IsSliceOrArray, false) Equal(t, len(err.SliceOrArrayErrs), 0) + + type MyStruct struct { + A, B string + C interface{} + } + + var a MyStruct + + a.A = "value" + a.C = "nu" + + errs = validate.Struct(a) + Equal(t, errs, nil) } func TestMapDiveValidation(t *testing.T) { From 9d2b8ee9d40d85461a06512c6cfbacff9269120d Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 9 Jul 2015 14:18:54 -0400 Subject: [PATCH 52/52] updated coverage tests to be 100% --- validator_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/validator_test.go b/validator_test.go index 1b2e803..8bfff9b 100644 --- a/validator_test.go +++ b/validator_test.go @@ -231,6 +231,18 @@ func AssertMapFieldError(t *testing.T, s map[string]*FieldError, field string, e EqualSkip(t, 2, val.Tag, expectedTag) } +func TestBadKeyValidation(t *testing.T) { + type Test struct { + Name string `validate:"required, "` + } + + tst := &Test{ + Name: "test", + } + + PanicMatches(t, func() { validate.Struct(tst) }, "Invalid validation tag on field Name") +} + func TestFlattenValidation(t *testing.T) { type Inner struct { @@ -623,9 +635,12 @@ func TestInterfaceErrValidation(t *testing.T) { func TestMapDiveValidation(t *testing.T) { + n := map[int]interface{}{0: nil} + err := validate.Field(n, "omitempty,required") + m := map[int]string{0: "ok", 3: "", 4: "ok"} - err := validate.Field(m, "len=3,dive,required") + err = validate.Field(m, "len=3,dive,required") NotEqual(t, err, nil) Equal(t, err.IsPlaceholderErr, true) Equal(t, err.IsMap, true)