Skip to content

Commit

Permalink
fix ValidateMap and field references
Browse files Browse the repository at this point in the history
  • Loading branch information
jippi committed Feb 1, 2024
1 parent 55313db commit ac59f8b
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 3 deletions.
39 changes: 36 additions & 3 deletions validator_instance.go
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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 any, field interface{}, tag string) (err error) {

Check failure on line 671 in validator_instance.go

View workflow job for this annotation

GitHub Actions / test (1.17.x, ubuntu-latest)

undefined: any
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"
Expand Down
95 changes: 95 additions & 0 deletions validator_test.go
Expand Up @@ -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) {
Expand Down

0 comments on commit ac59f8b

Please sign in to comment.