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 (
"fmt"
"reflect"
"strings"
"sync"
"github.com/go-playground/validator/v10"
@ -18,34 +17,33 @@ type defaultValidator struct {
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.
func (err sliceValidateError) Error() string {
n := len(err)
switch n {
case 0:
return ""
default:
var b strings.Builder
if err[0] != nil {
fmt.Fprintf(&b, "[%d]: %s", 0, err[0].Error())
}
if n > 1 {
for i := 1; i < n; i++ {
if err[i] != nil {
b.WriteString("\n")
fmt.Fprintf(&b, "[%d]: %s", i, err[i].Error())
}
}
}
return b.String()
}
type sliceFieldError struct {
validator.FieldError
index int
}
func (fe sliceFieldError) Index() int {
return fe.index
}
func (fe sliceFieldError) Error() string {
return fmt.Sprintf("[%d]: %s", fe.index, fe.FieldError.Error())
}
func (fe sliceFieldError) Unwrap() error {
return fe.FieldError
}
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 {
if obj == nil {
return nil
@ -59,16 +57,18 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error {
return v.validateStruct(obj)
case reflect.Slice, reflect.Array:
count := value.Len()
validateRet := make(sliceValidateError, 0)
var errs validator.ValidationErrors
for i := 0; i < count; i++ {
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 errs
}
return nil
}
return validateRet
default:
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 (
"errors"
"testing"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert"
)
func TestSliceValidateError(t *testing.T) {
tests := []struct {
name string
err sliceValidateError
want string
}{
{"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"},
{"has zero elements", sliceValidateError{}, ""},
{"has one element", sliceValidateError{errors.New("test one error")}, "[0]: test one error"},
{"has two elements",
sliceValidateError{
errors.New("first error"),
errors.New("second error"),
},
"[0]: first error\n[1]: second error",
},
{"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 TestSliceFieldError(t *testing.T) {
var fe validator.FieldError = dummyFieldError{msg: "test error"}
var err SliceFieldError = sliceFieldError{fe, 10}
assert.Equal(t, 10, err.Index())
assert.Equal(t, "[10]: test error", err.Error())
assert.Equal(t, fe, errors.Unwrap(err))
}
type dummyFieldError struct {
validator.FieldError
msg string
}
func (fe dummyFieldError) Error() string {
return fe.msg
}
func TestDefaultValidator(t *testing.T) {