From e0bfa17b226425fcca3d08709cf3f4ab6059de60 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Sat, 20 Jun 2015 10:04:55 -0400 Subject: [PATCH 01/12] 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 4afdc19aef655b5c36a3089d548ac25e9a9b6b64 Mon Sep 17 00:00:00 2001 From: joeybloggs Date: Thu, 25 Jun 2015 09:15:46 -0400 Subject: [PATCH 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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 11/12] 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 12/12] 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