From 0b97b35ef3266dd96d78fe65b3f5748787af5eba Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Thu, 1 Feb 2024 22:32:27 +0100 Subject: [PATCH 1/2] fix ValidateMap and field references --- util.go | 11 ++++- validator_instance.go | 39 ++++++++++++++++-- validator_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/util.go b/util.go index 16851593..ca225e4a 100644 --- a/util.go +++ b/util.go @@ -132,6 +132,9 @@ BEGIN: case reflect.Map: idx := strings.Index(namespace, leftBracket) + 1 idx2 := strings.Index(namespace, rightBracket) + if idx2 == -1 { + idx2 = len(namespace) + } endIdx := idx2 @@ -212,7 +215,13 @@ BEGIN: // reflect.Type = string default: val = current.MapIndex(reflect.ValueOf(key)) - namespace = namespace[endIdx+1:] + // If we exceeded the length of the namespace, then we're at the end + // of the namespace traversal, so set it to empty value to break loop + if endIdx+1 > len(namespace) { + namespace = "" + } else { + namespace = namespace[endIdx+1:] + } } goto BEGIN diff --git a/validator_instance.go b/validator_instance.go index d5a7be1d..25ab62b7 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -162,17 +162,22 @@ func (v *Validate) SetTagName(name string) { // ValidateMapCtx validates a map using a map of validation rules and allows passing of contextual // validation information via context.Context. func (v Validate) ValidateMapCtx(ctx context.Context, data map[string]interface{}, rules map[string]interface{}) map[string]interface{} { + return v.validateMapCtx(ctx, data, data, rules) +} + +// validateMapCtx will track the original "root" map (in case of nesting) which will allow for Field reference validation +func (v Validate) validateMapCtx(ctx context.Context, root map[string]interface{}, data map[string]interface{}, rules map[string]interface{}) map[string]interface{} { errs := make(map[string]interface{}) for field, rule := range rules { if ruleObj, ok := rule.(map[string]interface{}); ok { if dataObj, ok := data[field].(map[string]interface{}); ok { - err := v.ValidateMapCtx(ctx, dataObj, ruleObj) + err := v.validateMapCtx(ctx, root, dataObj, ruleObj) if len(err) > 0 { errs[field] = err } } else if dataObjs, ok := data[field].([]map[string]interface{}); ok { for _, obj := range dataObjs { - err := v.ValidateMapCtx(ctx, obj, ruleObj) + err := v.validateMapCtx(ctx, root, obj, ruleObj) if len(err) > 0 { errs[field] = err } @@ -181,7 +186,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.VarCtxMap(ctx, root, data[field], ruleStr) if err != nil { errs[field] = err } @@ -656,6 +661,34 @@ func (v *Validate) VarCtx(ctx context.Context, field interface{}, tag string) (e return } +// VarCtxMap validates a single variable (in a map) using tag style validation and allows passing of contextual +// validation information via context.Context. This allow usage of "Field" validations via ValidateMap() by using +// map lookup format "eqfield=[AnotherMapKey] AnotherMapKeyValue" and "required_if=[AnotherMapKey] AnotherMapKeyValue" +// +// 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) VarCtxMap(ctx context.Context, parent interface{}, field interface{}, tag string) (err error) { + if len(tag) == 0 || tag == skipValidationTag { + return nil + } + + ctag := v.fetchCacheTag(tag) + + val := reflect.ValueOf(field) + vd := v.pool.Get().(*validate) + vd.top = val + vd.isPartial = false + vd.traverseField(ctx, reflect.ValueOf(parent), val, vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) + + if len(vd.errs) > 0 { + err = vd.errs + vd.errs = nil + } + v.pool.Put(vd) + return +} + // VarWithValue validates a single variable, against another variable/field's value using tag style validation // eg. // s1 := "abcd" diff --git a/validator_test.go b/validator_test.go index 2826ae70..ff3f3750 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13185,6 +13185,101 @@ func TestValidate_ValidateMapCtx(t *testing.T) { }, want: 1, }, + + { + name: "test map with field reference", + args: args{ + data: map[string]interface{}{ + "Field_A": "Value_A", + "Field_B": "Value_A", + }, + rules: map[string]interface{}{ + "Field_A": "required", + "Field_B": "required,eqfield=[Field_A]", + }, + }, + want: 0, + }, + + { + name: "test map with field+value reference (fail)", + args: args{ + data: map[string]interface{}{ + "Field_A": "Value_A", + "Field_B": "", + }, + rules: map[string]interface{}{ + "Field_A": "required", + "Field_B": "required_if=[Field_A] Value_A", + }, + }, + want: 1, + }, + + { + name: "test map with field+value reference (success)", + args: args{ + data: map[string]interface{}{ + "Field_A": "Value_A", + "Field_B": "not empty", + }, + rules: map[string]interface{}{ + "Field_A": "required", + "Field_B": "required_if=[Field_A] Value_A", + }, + }, + want: 0, + }, + + { + name: "test nested map in slice field falidation (success)", + args: args{ + data: map[string]interface{}{ + "Test_A": map[string]interface{}{ + "Test_B": "SomethingEqual", + "Test_C": []map[string]interface{}{ + { + "Test_D": "SomethingEqual", + }, + }, + }, + }, + rules: map[string]interface{}{ + "Test_A": map[string]interface{}{ + "Test_B": "required", + "Test_C": map[string]interface{}{ + "Test_D": "eqfield=[Test_A].[Test_B]", + }, + }, + }, + }, + want: 0, + }, + + { + name: "test nested map in slice field falidation (fail)", + args: args{ + data: map[string]interface{}{ + "Test_A": map[string]interface{}{ + "Test_B": "SomethingEqual", + "Test_C": []map[string]interface{}{ + { + "Test_D": "SomethingDifferent", + }, + }, + }, + }, + rules: map[string]interface{}{ + "Test_A": map[string]interface{}{ + "Test_B": "required", + "Test_C": map[string]interface{}{ + "Test_D": "eqfield=[Test_A].[Test_B]", + }, + }, + }, + }, + want: 1, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From be965b89f3aab57b1b016b24807f74300c768796 Mon Sep 17 00:00:00 2001 From: Christian Winther Date: Fri, 2 Feb 2024 20:33:43 +0100 Subject: [PATCH 2/2] fix error message format for maps to include the map key --- validator_instance.go | 7 ++++--- validator_test.go | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/validator_instance.go b/validator_instance.go index 25ab62b7..3f1e6130 100644 --- a/validator_instance.go +++ b/validator_instance.go @@ -186,7 +186,7 @@ func (v Validate) validateMapCtx(ctx context.Context, root 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.VarCtxMap(ctx, root, data[field], ruleStr) + err := v.VarCtxMap(ctx, field, root, data[field], ruleStr) if err != nil { errs[field] = err } @@ -668,7 +668,7 @@ func (v *Validate) VarCtx(ctx context.Context, field interface{}, tag string) (e // 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) VarCtxMap(ctx context.Context, parent interface{}, field interface{}, tag string) (err error) { +func (v *Validate) VarCtxMap(ctx context.Context, key string, parent interface{}, field interface{}, tag string) (err error) { if len(tag) == 0 || tag == skipValidationTag { return nil } @@ -679,7 +679,8 @@ func (v *Validate) VarCtxMap(ctx context.Context, parent interface{}, field inte vd := v.pool.Get().(*validate) vd.top = val vd.isPartial = false - vd.traverseField(ctx, reflect.ValueOf(parent), val, vd.ns[0:0], vd.actualNs[0:0], defaultCField, ctag) + + vd.traverseField(ctx, reflect.ValueOf(parent), val, vd.ns[0:0], vd.actualNs[0:0], &cField{altName: key}, ctag) if len(vd.errs) > 0 { err = vd.errs diff --git a/validator_test.go b/validator_test.go index ff3f3750..2ff918a3 100644 --- a/validator_test.go +++ b/validator_test.go @@ -13280,6 +13280,21 @@ func TestValidate_ValidateMapCtx(t *testing.T) { }, want: 1, }, + + { + name: "test map with field+value reference - no brackets (fail)", + args: args{ + data: map[string]interface{}{ + "Field_A": "Value_A", + "Field_B": "", + }, + rules: map[string]interface{}{ + "Field_A": "required", + "Field_B": "required_if=Field_A Value_A", + }, + }, + want: 1, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {