diff --git a/validator_instance.go b/validator_instance.go index d9f148dba..b16cd13c3 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -182,7 +182,7 @@ func (v Validate) ValidateMapCtx(ctx context.Context, data map[string]interface{ errs[field] = errors.New("The field: '" + field + "' is not a map to dive") } } else if ruleStr, ok := rule.(string); ok { - err := v.VarCtx(ctx, data[field], ruleStr) + err := v.VarWithKeyCtx(ctx, field, data[field], ruleStr) if err != nil { errs[field] = err } @@ -708,3 +708,61 @@ func (v *Validate) VarWithValueCtx(ctx context.Context, field interface{}, other v.pool.Put(vd) return } + +// VarWithKey validates a single variable with a key to be included in the returned error using tag style validation +// eg. +// var s string +// validate.VarWithKey("email_address", s, "required,email") +// +// WARNING: a struct can be passed for validation eg. time.Time is a struct or +// if you have a custom type and have registered a custom type handler, so must +// allow it; however unforeseen validations will occur if trying to validate a +// struct that is meant to be passed to 'validate.Struct' +// +// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise. +// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors. +// validate Array, Slice and maps fields which may contain more than one error +func (v *Validate) VarWithKey(key string, field interface{}, tag string) error { + return v.VarWithKeyCtx(context.Background(), key, field, tag) +} + +// VarWithKeyCtx validates a single variable with a key to be included in the returned error using tag style validation +// and allows passing of contextual validation information via context.Context. +// eg. +// var s string +// validate.VarWithKeyCtx("email_address", s, "required,email") +// +// WARNING: a struct can be passed for validation eg. time.Time is a struct or +// if you have a custom type and have registered a custom type handler, so must +// allow it; however unforeseen validations will occur if trying to validate a +// struct that is meant to be passed to 'validate.Struct' +// +// It returns InvalidValidationError for bad values passed in and nil or ValidationErrors as error otherwise. +// You will need to assert the error if it's not nil eg. err.(validator.ValidationErrors) to access the array of errors. +// validate Array, Slice and maps fields which may contain more than one error +func (v *Validate) VarWithKeyCtx(ctx context.Context, key string, field interface{}, tag string) (err error) { + if len(tag) == 0 || tag == skipValidationTag { + return nil + } + + ctag := v.fetchCacheTag(tag) + + cField := &cField{ + name: key, + altName: key, + namesEqual: true, + } + + val := reflect.ValueOf(field) + vd := v.pool.Get().(*validate) + vd.top = val + vd.isPartial = false + vd.traverseField(ctx, val, val, vd.ns[0:0], vd.actualNs[0:0], cField, ctag) + + if len(vd.errs) > 0 { + err = vd.errs + vd.errs = nil + } + v.pool.Put(vd) + return +} diff --git a/validator_test.go b/validator_test.go index 0f96b7775..ee1e585b1 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13351,6 +13351,100 @@ func TestValidate_ValidateMapCtx(t *testing.T) { } } +func TestValidate_ValidateMapCtxWithKeys(t *testing.T) { + type args struct { + data map[string]interface{} + rules map[string]interface{} + errors map[string]interface{} + } + tests := []struct { + name string + args args + want int + }{ + { + name: "test invalid email", + args: args{ + data: map[string]interface{}{ + "email": "emailaddress", + }, + rules: map[string]interface{}{ + "email": "required,email", + }, + errors: map[string]interface{}{ + "email": "Key: 'email' Error:Field validation for 'email' failed on the 'email' tag", + }, + }, + want: 1, + }, + { + name: "test multiple errors with capitalized keys", + args: args{ + data: map[string]interface{}{ + "Email": "emailaddress", + "Age": 15, + }, + rules: map[string]interface{}{ + "Email": "required,email", + "Age": "number,gt=16", + }, + errors: map[string]interface{}{ + "Email": "Key: 'Email' Error:Field validation for 'Email' failed on the 'email' tag", + "Age": "Key: 'Age' Error:Field validation for 'Age' failed on the 'gt' tag", + }, + }, + want: 2, + }, + { + name: "test valid map data", + args: args{ + data: map[string]interface{}{ + "email": "email@example.com", + "age": 17, + }, + rules: map[string]interface{}{ + "email": "required,email", + "age": "number,gt=16", + }, + errors: map[string]interface{}{}, + }, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + validate := New() + errs := validate.ValidateMapCtx(context.Background(), tt.args.data, tt.args.rules) + NotEqual(t, errs, nil) + Equal(t, len(errs), tt.want) + for key, err := range errs { + Equal(t, err.(ValidationErrors)[0].Error(), tt.args.errors[key]) + } + }) + } +} + +func TestValidate_VarWithKey(t *testing.T) { + validate := New() + errs := validate.VarWithKey("email", "invalidemail", "required,email") + NotEqual(t, errs, nil) + AssertError(t, errs, "email", "email", "email", "email", "email") + + errs = validate.VarWithKey("email", "email@example.com", "required,email") + Equal(t, errs, nil) +} + +func TestValidate_VarWithKeyCtx(t *testing.T) { + validate := New() + errs := validate.VarWithKeyCtx(context.Background(), "age", 15, "required,gt=16") + NotEqual(t, errs, nil) + AssertError(t, errs, "age", "age", "age", "age", "gt") + + errs = validate.VarWithKey("age", 17, "required,gt=16") + Equal(t, errs, nil) +} + func TestMongoDBObjectIDFormatValidation(t *testing.T) { tests := []struct { value string `validate:"mongodb"`