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 db01f1f..64f0f85 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,82 @@ func (e *FieldError) Error() string { return fmt.Sprintf(fieldErrMsg, e.Field, e.Tag) } +// 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 { + + 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 { + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } + + } + } + } else { + + se := err.(*StructErrors) + + 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 + } + } + } + } + } + + if e.IsMap { + 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 { + if fe.IsPlaceholderErr { + errs[fmt.Sprintf("[%#v]%s", key, k)] = v + } else { + errs[fmt.Sprintf("[%#v]", key)] = v + } + } + } + } else { + + se := err.(*StructErrors) + + 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 + } + } + } + } + } + + 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 { @@ -234,7 +309,17 @@ 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 f.IsPlaceholderErr { + errs[f.Field+k] = fe + } else { + errs[k] = fe + } + } + } } for key, val := range e.StructErrors { diff --git a/validator_test.go b/validator_test.go index 4edc1a9..1eda6a0 100644 --- a/validator_test.go +++ b/validator_test.go @@ -226,6 +226,163 @@ 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,required"` + } + + 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") + + structErrFlatten, ok := errs.Flatten()["Errs[0][1].Inner.Name"] + Equal(t, ok, true) + Equal(t, structErrFlatten.Field, "Name") + Equal(t, structErrFlatten.Tag, "required") + + 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) { var v1 interface{} @@ -578,6 +735,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 +768,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 +834,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 +842,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 +906,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)