diff --git a/binding/binding.go b/binding/binding.go index deb71661..7042101d 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -52,9 +52,9 @@ type BindingUri interface { // https://github.com/go-playground/validator/tree/v10.6.1. type StructValidator interface { // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is a slice|array, the validation should be performed travel on every element. - // If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned. - // If the received type is a struct or pointer to a struct, the validation should be performed. + // If the received type is a slice/array/map, the validation should be performed on every element. + // If the received type is not a struct or slice/array/map, any validation should be skipped and nil must be returned. + // If the received type is a pointer to a struct/slice/array/map, the validation should be performed. // If the struct is not valid or the validation itself fails, a descriptive error should be returned. // Otherwise nil must be returned. ValidateStruct(interface{}) error diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go index 23424470..00d63036 100644 --- a/binding/binding_nomsgpack.go +++ b/binding/binding_nomsgpack.go @@ -50,8 +50,9 @@ type BindingUri interface { // https://github.com/go-playground/validator/tree/v10.6.1. type StructValidator interface { // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is not a struct, any validation should be skipped and nil must be returned. - // If the received type is a struct or pointer to a struct, the validation should be performed. + // If the received type is a slice/array/map, the validation should be performed on every element. + // If the received type is not a struct or slice/array/map, any validation should be skipped and nil must be returned. + // If the received type is a pointer to a struct/slice/array/map, the validation should be performed. // If the struct is not valid or the validation itself fails, a descriptive error should be returned. // Otherwise nil must be returned. ValidateStruct(interface{}) error diff --git a/binding/default_validator.go b/binding/default_validator.go index 16225302..10e9bb12 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -41,9 +41,33 @@ func (fe sliceFieldError) Unwrap() error { return fe.FieldError } +// MapFieldError is returned for invalid map values. +// It extends validator.FieldError with the key of the failing value. +type MapFieldError interface { + validator.FieldError + Key() interface{} +} + +type mapFieldError struct { + validator.FieldError + key interface{} +} + +func (fe mapFieldError) Key() interface{} { + return fe.key +} + +func (fe mapFieldError) Error() string { + return fmt.Sprintf("[%v]: %s", fe.key, fe.FieldError.Error()) +} + +func (fe mapFieldError) Unwrap() error { + return fe.FieldError +} + var _ StructValidator = &defaultValidator{} -// ValidateStruct receives any kind of type, but validates only structs, pointers, slices, and arrays. +// ValidateStruct receives any kind of type, but validates only structs, pointers, slices, arrays, and maps. func (v *defaultValidator) ValidateStruct(obj interface{}) error { if obj == nil { return nil @@ -56,8 +80,8 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error { case reflect.Struct: return v.validateStruct(obj) case reflect.Slice, reflect.Array: - count := value.Len() var errs validator.ValidationErrors + count := value.Len() for i := 0; i < count; i++ { if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { for _, fieldError := range err.(validator.ValidationErrors) { // nolint: errorlint @@ -69,6 +93,19 @@ func (v *defaultValidator) ValidateStruct(obj interface{}) error { return errs } return nil + case reflect.Map: + var errs validator.ValidationErrors + for _, key := range value.MapKeys() { + if err := v.ValidateStruct(value.MapIndex(key).Interface()); err != nil { + for _, fieldError := range err.(validator.ValidationErrors) { // nolint: errorlint + errs = append(errs, mapFieldError{fieldError, key.Interface()}) + } + } + } + if len(errs) > 0 { + return errs + } + return nil default: return nil } diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go index fec34ef6..51139fe1 100644 --- a/binding/default_validator_test.go +++ b/binding/default_validator_test.go @@ -21,6 +21,20 @@ func TestSliceFieldError(t *testing.T) { assert.Equal(t, fe, errors.Unwrap(err)) } +func TestMapFieldError(t *testing.T) { + var fe validator.FieldError = dummyFieldError{msg: "test error"} + + var err MapFieldError = mapFieldError{fe, "test key"} + assert.Equal(t, "test key", err.Key()) + assert.Equal(t, "[test key]: test error", err.Error()) + assert.Equal(t, fe, errors.Unwrap(err)) + + err = mapFieldError{fe, 123} + assert.Equal(t, 123, err.Key()) + assert.Equal(t, "[123]: test error", err.Error()) + assert.Equal(t, fe, errors.Unwrap(err)) +} + type dummyFieldError struct { validator.FieldError msg string @@ -61,6 +75,18 @@ func TestDefaultValidator(t *testing.T) { {"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true}, {"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true}, {"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false}, + {"validate map[string]struct failed-1", &defaultValidator{}, map[string]exampleStruct{"x": {A: "123456789", B: 1}}, true}, + {"validate map[string]struct failed-2", &defaultValidator{}, map[string]exampleStruct{"x": {A: "12345678", B: 0}}, true}, + {"validate map[string]struct passed", &defaultValidator{}, map[string]exampleStruct{"x": {A: "12345678", B: 1}}, false}, + {"validate map[string]*struct failed-1", &defaultValidator{}, map[string]*exampleStruct{"x": {A: "123456789", B: 1}}, true}, + {"validate map[string]*struct failed-2", &defaultValidator{}, map[string]*exampleStruct{"x": {A: "12345678", B: 0}}, true}, + {"validate map[string]*struct passed", &defaultValidator{}, map[string]*exampleStruct{"x": {A: "12345678", B: 1}}, false}, + {"validate *map[string]struct failed-1", &defaultValidator{}, &map[string]exampleStruct{"x": {A: "123456789", B: 1}}, true}, + {"validate *map[string]struct failed-2", &defaultValidator{}, &map[string]exampleStruct{"x": {A: "12345678", B: 0}}, true}, + {"validate *map[string]struct passed", &defaultValidator{}, &map[string]exampleStruct{"x": {A: "12345678", B: 1}}, false}, + {"validate *map[string]*struct failed-1", &defaultValidator{}, &map[string]*exampleStruct{"x": {A: "123456789", B: 1}}, true}, + {"validate *map[string]*struct failed-2", &defaultValidator{}, &map[string]*exampleStruct{"x": {A: "12345678", B: 0}}, true}, + {"validate *map[string]*struct passed", &defaultValidator{}, &map[string]*exampleStruct{"x": {A: "12345678", B: 1}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {