Always return ValidationErrors, expose failing index

This way callers can always expect ValidationErrors, and in case
of slice validation, they can also get indexes of the failing elements.
This commit is contained in:
Krzysztof Szafrański 2021-09-21 21:54:03 +02:00
parent efa3175007
commit cb9b68b1b5
3 changed files with 48 additions and 84 deletions

View File

@ -7,7 +7,6 @@ package binding
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"sync" "sync"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
@ -18,34 +17,33 @@ type defaultValidator struct {
validate *validator.Validate validate *validator.Validate
} }
type sliceValidateError []error // SliceFieldError is returned for invalid slice or array elements.
// It extends validator.FieldError with the index of the failing element.
type SliceFieldError interface {
validator.FieldError
Index() int
}
// Error concatenates all error elements in sliceValidateError into a single string separated by \n. type sliceFieldError struct {
func (err sliceValidateError) Error() string { validator.FieldError
n := len(err) index int
switch n { }
case 0:
return "" func (fe sliceFieldError) Index() int {
default: return fe.index
var b strings.Builder }
if err[0] != nil {
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error()) func (fe sliceFieldError) Error() string {
} return fmt.Sprintf("[%d]: %s", fe.index, fe.FieldError.Error())
if n > 1 { }
for i := 1; i < n; i++ {
if err[i] != nil { func (fe sliceFieldError) Unwrap() error {
b.WriteString("\n") return fe.FieldError
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
}
}
}
return b.String()
}
} }
var _ StructValidator = &defaultValidator{} var _ StructValidator = &defaultValidator{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. // ValidateStruct receives any kind of type, but validates only structs, pointers, slices, and arrays.
func (v *defaultValidator) ValidateStruct(obj interface{}) error { func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if obj == nil { if obj == nil {
return nil return nil
@ -59,16 +57,18 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
return v.validateStruct(obj) return v.validateStruct(obj)
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
count := value.Len() count := value.Len()
validateRet := make(sliceValidateError, 0) var errs validator.ValidationErrors
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil {
validateRet = append(validateRet, err) for _, fieldError := range err.(validator.ValidationErrors) { // nolint: errorlint
errs = append(errs, sliceFieldError{fieldError, i})
}
} }
} }
if len(validateRet) == 0 { if len(errs) > 0 {
return nil return errs
} }
return validateRet return nil
default: default:
return nil return nil
} }

View File

@ -1,20 +0,0 @@
package binding
import (
"errors"
"strconv"
"testing"
)
func BenchmarkSliceValidateError(b *testing.B) {
const size int = 100
for i := 0; i < b.N; i++ {
e := make(sliceValidateError, size)
for j := 0; j < size; j++ {
e[j] = errors.New(strconv.Itoa(j))
}
if len(e.Error()) == 0 {
b.Errorf("error")
}
}
}

View File

@ -7,43 +7,27 @@ package binding
import ( import (
"errors" "errors"
"testing" "testing"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert"
) )
func TestSliceValidateError(t *testing.T) { func TestSliceFieldError(t *testing.T) {
tests := []struct { var fe validator.FieldError = dummyFieldError{msg: "test error"}
name string
err sliceValidateError var err SliceFieldError = sliceFieldError{fe, 10}
want string assert.Equal(t, 10, err.Index())
}{ assert.Equal(t, "[10]: test error", err.Error())
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"}, assert.Equal(t, fe, errors.Unwrap(err))
{"has zero elements", sliceValidateError{}, ""}, }
{"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements", type dummyFieldError struct {
sliceValidateError{ validator.FieldError
errors.New("first error"), msg string
errors.New("second error"), }
},
"[0]: first error\n[1]: second error", func (fe dummyFieldError) Error() string {
}, return fe.msg
{"has many elements",
sliceValidateError{
errors.New("first error"),
errors.New("second error"),
nil,
nil,
nil,
errors.New("last error"),
},
"[0]: first error\n[1]: second error\n[5]: last error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want)
}
})
}
} }
func TestDefaultValidator(t *testing.T) { func TestDefaultValidator(t *testing.T) {