Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down
115 changes: 100 additions & 15 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
183 changes: 183 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -666,13 +834,15 @@ 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)
Equal(t, innerSliceError1.IsPlaceholderErr, false)
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"`
Expand Down Expand Up @@ -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)
Expand Down