From 3be19e58e1a3600964e0b415b8ac1cba8b8556bc Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 22 Nov 2021 20:28:47 +0800 Subject: [PATCH] recursive validation feature for package gvalid --- util/gutil/gutil_dump.go | 9 + util/gvalid/gvalid_validator_check_map.go | 46 +++- util/gvalid/gvalid_validator_check_struct.go | 192 ++++++---------- util/gvalid/gvalid_validator_check_value.go | 75 +++++- ...unit_feature_checkstruct_recursive_test.go | 103 --------- .../gvalid_z_unit_feature_recursive_test.go | 217 ++++++++++++++++++ 6 files changed, 405 insertions(+), 237 deletions(-) delete mode 100755 util/gvalid/gvalid_z_unit_feature_checkstruct_recursive_test.go create mode 100755 util/gvalid/gvalid_z_unit_feature_recursive_test.go diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 2e67c4ac6..4572ce1d9 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -22,6 +22,11 @@ type iString interface { String() string } +// iError is used for type assert api for Error(). +type iError interface { + Error() string +} + // iMarshalJSON is the interface for custom Json marshaling. type iMarshalJSON interface { MarshalJSON() ([]byte, error) @@ -231,6 +236,7 @@ func doDumpMap(in doDumpInternalInput) { } else { mapKeyStr = fmt.Sprintf(`%v`, mapKey.Interface()) } + // Map key and indent string dump. if !in.Option.WithType { in.Buffer.WriteString(fmt.Sprintf( "%s%v:%s", @@ -247,6 +253,7 @@ func doDumpMap(in doDumpInternalInput) { strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), )) } + // Map value dump. doDump(in.ReflectValue.MapIndex(mapKey).Interface(), in.NewIndent, in.Buffer, in.Option) in.Buffer.WriteString(",\n") } @@ -265,6 +272,8 @@ func doDumpStruct(in doDumpInternalInput) { ) if v, ok := in.Value.(iString); ok { structContentStr = v.String() + } else if v, ok := in.Value.(iError); ok { + structContentStr = v.Error() } else if v, ok := in.Value.(iMarshalJSON); ok { b, _ := v.MarshalJSON() structContentStr = string(b) diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index 9bbf3d62a..d87f6c28f 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -9,9 +9,11 @@ package gvalid import ( "context" "errors" + "reflect" "strings" "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/util/gconv" ) @@ -22,8 +24,7 @@ func (v *Validator) CheckMap(ctx context.Context, params interface{}) Error { } func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { - // If there's no validation rules, it does nothing and returns quickly. - if params == nil || v.rules == nil { + if params == nil { return nil } var ( @@ -76,12 +77,8 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { }) } } - // If there's no validation rules, it does nothing and returns quickly. - if len(checkRules) == 0 { - return nil - } - data := gconv.Map(params) - if data == nil { + inputParamMap := gconv.Map(params) + if inputParamMap == nil { return newValidationErrorByStr( internalParamsErrRuleName, errors.New("invalid params type: convert to map failed"), @@ -97,14 +94,41 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } } var ( - value interface{} + value interface{} + validator = v.Clone() ) + + // It checks the struct recursively if its attribute is an embedded struct. + // Ignore inputParamMap, rules and messages from parent. + validator.rules = nil + validator.messages = nil + for _, item := range inputParamMap { + originTypeAndKind := utils.OriginTypeAndKind(item) + switch originTypeAndKind.OriginKind { + case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array: + v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ + Value: item, + Type: originTypeAndKind.InputType, + OriginKind: originTypeAndKind.OriginKind, + ErrorMaps: errorMaps, + }) + } + // Bail feature. + if v.bail && len(errorMaps) > 0 { + break + } + } + if v.bail && len(errorMaps) > 0 { + return newValidationError(gcode.CodeValidationFailed, nil, errorMaps) + } + + // The following logic is the same as some of CheckStruct but without sequence support. for _, checkRuleItem := range checkRules { if len(checkRuleItem.Rule) == 0 { continue } value = nil - if valueItem, ok := data[checkRuleItem.Name]; ok { + if valueItem, ok := inputParamMap[checkRuleItem.Name]; ok { value = valueItem } // It checks each rule and its value in loop. @@ -114,7 +138,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { Rule: checkRuleItem.Rule, Messages: customMessage[checkRuleItem.Name], DataRaw: params, - DataMap: data, + DataMap: inputParamMap, }); validatedError != nil { _, errorItem := validatedError.FirstItem() // =========================================================== diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index baa9f4ced..b579db500 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -8,13 +8,13 @@ package gvalid import ( "context" - "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/internal/structs" - "github.com/gogf/gf/v2/internal/utils" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/util/gutil" "reflect" "strings" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/internal/structs" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/gutil" ) // CheckStruct validates struct and returns the error result. @@ -23,80 +23,6 @@ func (v *Validator) CheckStruct(ctx context.Context, object interface{}) Error { return v.doCheckStruct(ctx, object) } -type doCheckAttributeRecursivelyInput struct { - Field *structs.Field - ErrorMaps map[string]map[string]error - ResultSequenceRules []fieldRule -} - -func (v *Validator) doCheckAttributeRecursively(ctx context.Context, in doCheckAttributeRecursivelyInput) { - switch in.Field.OriginalKind() { - case reflect.Struct: - var ( - dataValue interface{} - fieldValue = in.Field.Value.Interface() - ) - if v.data != nil { - dataMap := gconv.Map(v.data) - if value, ok := dataMap[in.Field.TagValue]; ok { - dataValue = value - } - if dataValue == nil { - if value, ok := dataMap[in.Field.Name()]; ok { - dataValue = value - } - } - } - // No validation interface implements check. - if _, ok := fieldValue.(iNoValidation); ok { - return - } - // No validation field tag check. - if _, ok := in.Field.TagLookup(noValidationTagName); ok { - return - } - // Ignore rules and messages from parent. - validator := v.Clone() - validator.rules = nil - validator.messages = nil - if err := validator.Data(dataValue).doCheckStruct(ctx, fieldValue); err != nil { - // It merges the errors into single error map. - for k, m := range err.(*validationError).errors { - in.ErrorMaps[k] = m - if rules := err.(*validationError).rules; len(rules) > 0 { - in.ResultSequenceRules = append(in.ResultSequenceRules, rules...) - } - } - } - - case reflect.Map: - - case reflect.Slice, reflect.Array: - var ( - dataValue interface{} - dataArray = gconv.Interfaces(v.data) - sliceObjects = make([]interface{}, 0) - ) - if in.Field.Value.Len() == 0 { - sliceObjects = append(sliceObjects, reflect.New(in.Field.Value.Elem().Type())) - } else { - for i := 0; i < in.Field.Value.Len(); i++ { - sliceObjects = append(sliceObjects, in.Field.Value.Index(i).Interface()) - } - } - for index, obj := range sliceObjects { - dataValue = nil - if index < len(dataArray) { - dataValue = dataArray[index] - } - originValueAndKind := utils.OriginValueAndKind(obj) - switch originValueAndKind.OriginKind { - - } - } - } -} - func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error { var ( errorMaps = make(map[string]map[string]error) // Returning error. @@ -111,42 +37,14 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error if err != nil { return newValidationErrorByStr(internalObjectErrRuleName, err) } - // It checks the struct recursively if its attribute is an embedded struct. - for _, field := range fieldMap { - if field.IsEmbedded() { - // No validation interface implements check. - if _, ok := field.Value.Interface().(iNoValidation); ok { - continue - } - // No validation field tag check. - if _, ok := field.TagLookup(noValidationTagName); ok { - continue - } - if err = v.doCheckStruct(ctx, field.Value); err != nil { - // It merges the errors into single error map. - for k, m := range err.(*validationError).errors { - errorMaps[k] = m - } - } - } else { - if field.TagValue != "" { - fieldToAliasNameMap[field.Name()] = field.TagValue - } - // Recursively check attribute struct/[]string/map/[]map. - v.doCheckAttributeRecursively(ctx, doCheckAttributeRecursivelyInput{ - Field: field, - ErrorMaps: errorMaps, - ResultSequenceRules: resultSequenceRules, - }) - } - } + // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. - tagField, err := structs.TagFields(object, structTagPriority) + tagFields, err := structs.TagFields(object, structTagPriority) if err != nil { return newValidationErrorByStr(internalObjectErrRuleName, err) } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagField) == 0 && v.messages == nil { + if len(tagFields) == 0 && v.messages == nil { return nil } @@ -174,7 +72,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error msgArray = strings.Split(msg, "|") ruleArray = strings.Split(rule, "|") ) - for k, v := range ruleArray { + for k, ruleKey := range ruleArray { // If length of custom messages is lesser than length of rules, // the rest rules use the default error messages. if len(msgArray) <= k { @@ -183,7 +81,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error if len(msgArray[k]) == 0 { continue } - array := strings.Split(v, ":") + array := strings.Split(ruleKey, ":") if _, ok := customMessage[name]; !ok { customMessage[name] = make(map[string]string) } @@ -209,7 +107,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagField) == 0 && len(checkRules) == 0 { + if len(tagFields) == 0 && len(checkRules) == 0 { return nil } // Input parameter map handling. @@ -230,15 +128,15 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // Merge the custom validation rules with rules in struct tag. // The custom rules has the most high priority that can overwrite the struct tag rules. - for _, field := range tagField { + for _, field := range tagFields { var ( fieldName = field.Name() // Attribute name. name, rule, msg = parseSequenceTag(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only. ) if len(name) == 0 { - if v, ok := fieldToAliasNameMap[fieldName]; ok { + if value, ok := fieldToAliasNameMap[fieldName]; ok { // It uses alias name of the attribute if its alias name tag exists. - name = v + name = value } else { // It or else uses the attribute name directly. name = fieldName @@ -291,7 +189,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error msgArray = strings.Split(msg, "|") ruleArray = strings.Split(rule, "|") ) - for k, v := range ruleArray { + for k, ruleKey := range ruleArray { // If length of custom messages is lesser than length of rules, // the rest rules use the default error messages. if len(msgArray) <= k { @@ -300,7 +198,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error if len(msgArray[k]) == 0 { continue } - array := strings.Split(v, ":") + array := strings.Split(ruleKey, ":") if _, ok := customMessage[name]; !ok { customMessage[name] = make(map[string]string) } @@ -312,22 +210,71 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // Custom error messages, // which have the most priority than `rules` and struct tag. if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 { - for k, v := range msg { + for k, msgName := range msg { if a, ok := fieldToAliasNameMap[k]; ok { // Overwrite the key of field name. - customMessage[a] = v + customMessage[a] = msgName } else { - customMessage[k] = v + customMessage[k] = msgName } } } - // The following logic is the same as some of CheckMap but with sequence support. + // Temporary variable for value. var ( value interface{} ) + + // It checks the struct recursively if its attribute is an embedded struct. + for _, field := range fieldMap { + // No validation interface implements check. + if _, ok := field.Value.Interface().(iNoValidation); ok { + continue + } + // No validation field tag check. + if _, ok := field.TagLookup(noValidationTagName); ok { + continue + } + if field.IsEmbedded() { + if err = v.doCheckStruct(ctx, field.Value); err != nil { + // It merges the errors into single error map. + for k, m := range err.(*validationError).errors { + errorMaps[k] = m + } + } + } else { + if field.TagValue != "" { + fieldToAliasNameMap[field.Name()] = field.TagValue + } + switch field.OriginalKind() { + case reflect.Map, reflect.Struct, reflect.Slice, reflect.Array: + // Recursively check attribute struct/[]string/map/[]map. + _, value = gutil.MapPossibleItemByKey(inputParamMap, field.Name()) + v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ + Value: value, + OriginKind: field.OriginalKind(), + Type: field.Type().Type, + ErrorMaps: errorMaps, + ResultSequenceRules: &resultSequenceRules, + }) + } + } + if v.bail && len(errorMaps) > 0 { + break + } + } + if v.bail && len(errorMaps) > 0 { + return newValidationError(gcode.CodeValidationFailed, resultSequenceRules, errorMaps) + } + + // The following logic is the same as some of CheckMap but with sequence support. for _, checkRuleItem := range checkRules { _, value = gutil.MapPossibleItemByKey(inputParamMap, checkRuleItem.Name) + if value == nil { + if aliasName := fieldToAliasNameMap[checkRuleItem.Name]; aliasName != "" { + _, value = gutil.MapPossibleItemByKey(inputParamMap, aliasName) + } + } // It checks each rule and its value in loop. if validatedError := v.doCheckValue(ctx, doCheckValueInput{ Name: checkRuleItem.Name, @@ -363,6 +310,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error for ruleKey, errorItemMsgMap := range errorItem { errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap } + // Bail feature. if v.bail { break } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 3ecbe92d1..3bcb56f10 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -9,6 +9,7 @@ package gvalid import ( "context" "errors" + "reflect" "strconv" "strings" "time" @@ -17,6 +18,7 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/net/gipv4" "github.com/gogf/gf/v2/net/gipv6" "github.com/gogf/gf/v2/os/gtime" @@ -48,7 +50,7 @@ type doCheckValueInput struct { Name string // Name specifies the name of parameter `value`. Value interface{} // Value specifies the value for the rules to be validated. Rule string // Rule specifies the validation rules string, like "required", "required|between:1,100", etc. - Messages interface{} // Messages specifies the custom error messages for this rule, which is usually type of map/slice. + Messages interface{} // Messages specifies the custom error messages for this rule from parameters input, which is usually type of map/slice. DataRaw interface{} // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value. DataMap map[string]interface{} // DataMap specifies the map that is converted from `dataRaw`. It is usually used internally } @@ -72,6 +74,7 @@ func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Erro switch messages := in.Messages.(type) { case string: msgArray = strings.Split(messages, "|") + default: for k, message := range gconv.Map(in.Messages) { customMsgMap[k] = gconv.String(message) @@ -544,3 +547,73 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBui } return match, nil } + +type doCheckValueRecursivelyInput struct { + Value interface{} + Type reflect.Type + OriginKind reflect.Kind + ErrorMaps map[string]map[string]error + ResultSequenceRules *[]fieldRule +} + +func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) { + switch in.OriginKind { + case reflect.Struct: + // Ignore data, rules and messages from parent. + validator := v.Clone() + validator.rules = nil + validator.messages = nil + if err := validator.Data(in.Value).doCheckStruct(ctx, reflect.New(in.Type).Interface()); err != nil { + // It merges the errors into single error map. + for k, m := range err.(*validationError).errors { + in.ErrorMaps[k] = m + } + if in.ResultSequenceRules != nil { + *in.ResultSequenceRules = append(*in.ResultSequenceRules, err.(*validationError).rules...) + } + } + + case reflect.Map: + var ( + dataMap = gconv.Map(in.Value) + validator = v.Clone() + ) + // Ignore data, rules and messages from parent. + validator.rules = nil + validator.messages = nil + for _, item := range dataMap { + originTypeAndKind := utils.OriginTypeAndKind(item) + v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ + Value: item, + Type: originTypeAndKind.InputType, + OriginKind: originTypeAndKind.OriginKind, + ErrorMaps: in.ErrorMaps, + ResultSequenceRules: in.ResultSequenceRules, + }) + // Bail feature. + if v.bail && len(in.ErrorMaps) > 0 { + break + } + } + + case reflect.Slice, reflect.Array: + array := gconv.Interfaces(in.Value) + if len(array) == 0 { + return + } + for _, item := range array { + originTypeAndKind := utils.OriginTypeAndKind(item) + v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{ + Value: item, + Type: originTypeAndKind.InputType, + OriginKind: originTypeAndKind.OriginKind, + ErrorMaps: in.ErrorMaps, + ResultSequenceRules: in.ResultSequenceRules, + }) + // Bail feature. + if v.bail && len(in.ErrorMaps) > 0 { + break + } + } + } +} diff --git a/util/gvalid/gvalid_z_unit_feature_checkstruct_recursive_test.go b/util/gvalid/gvalid_z_unit_feature_checkstruct_recursive_test.go deleted file mode 100755 index 94357dd01..000000000 --- a/util/gvalid/gvalid_z_unit_feature_checkstruct_recursive_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gvalid_test - -import ( - "context" - "testing" - - "github.com/gogf/gf/v2/frame/g" - - "github.com/gogf/gf/v2/test/gtest" - "github.com/gogf/gf/v2/util/gvalid" -) - -func Test_CheckStruct_Recursive_Struct(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - type Pass struct { - Pass1 string `v:"required|same:Pass2"` - Pass2 string `v:"required|same:Pass1"` - } - type User struct { - Id int - Name string `v:"required"` - Pass Pass - } - user := &User{ - Name: "", - Pass: Pass{ - Pass1: "1", - Pass2: "2", - }, - } - err := gvalid.CheckStruct(context.TODO(), user, nil) - t.AssertNE(err, nil) - t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) - t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `1` must be the same as field Pass2"}) - t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `2` must be the same as field Pass1"}) - }) -} - -func Test_CheckStruct_Recursive_Struct_WithData(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - type Pass struct { - Pass1 string `v:"required|same:Pass2"` - Pass2 string `v:"required|same:Pass1"` - } - type User struct { - Id int - Name string `v:"required"` - Pass Pass - } - user := &User{} - data := g.Map{ - "Name": "john", - "Pass": g.Map{ - "Pass1": 100, - "Pass2": 200, - }, - } - err := g.Validator().Data(data).CheckStruct(context.TODO(), user) - t.AssertNE(err, nil) - t.Assert(err.Maps()["Name"], nil) - t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `100` must be the same as field Pass2"}) - t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `200` must be the same as field Pass1"}) - }) -} - -func Test_CheckStruct_Recursive_SliceStruct(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - type Pass struct { - Pass1 string `v:"required|same:Pass2"` - Pass2 string `v:"required|same:Pass1"` - } - type User struct { - Id int - Name string `v:"required"` - Passes []Pass - } - user := &User{ - Name: "", - Passes: []Pass{ - { - Pass1: "1", - Pass2: "2", - }, - { - Pass1: "3", - Pass2: "4", - }, - }, - } - err := gvalid.CheckStruct(context.TODO(), user, nil) - g.Dump(err.Items()) - t.AssertNE(err, nil) - t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) - t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `1` must be the same as field Pass2"}) - t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `2` must be the same as field Pass1"}) - }) -} diff --git a/util/gvalid/gvalid_z_unit_feature_recursive_test.go b/util/gvalid/gvalid_z_unit_feature_recursive_test.go new file mode 100755 index 000000000..2c6d3e0a3 --- /dev/null +++ b/util/gvalid/gvalid_z_unit_feature_recursive_test.go @@ -0,0 +1,217 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gvalid_test + +import ( + "testing" + + "github.com/gogf/gf/v2/frame/g" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gvalid" +) + +func Test_CheckStruct_Recursive_Struct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Pass Pass + } + user := &User{ + Name: "", + Pass: Pass{ + Pass1: "1", + Pass2: "2", + }, + } + err := gvalid.CheckStruct(ctx, user, nil) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) + t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `1` must be the same as field Pass2"}) + t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `2` must be the same as field Pass1"}) + }) +} + +func Test_CheckStruct_Recursive_Struct_WithData(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Pass Pass + } + user := &User{} + data := g.Map{ + "Name": "john", + "Pass": g.Map{ + "Pass1": 100, + "Pass2": 200, + }, + } + err := g.Validator().Data(data).CheckStruct(ctx, user) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], nil) + t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `100` must be the same as field Pass2"}) + t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `200` must be the same as field Pass1"}) + }) +} + +func Test_CheckStruct_Recursive_SliceStruct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Passes []Pass + } + user := &User{ + Name: "", + Passes: []Pass{ + { + Pass1: "1", + Pass2: "2", + }, + { + Pass1: "3", + Pass2: "4", + }, + }, + } + err := gvalid.CheckStruct(ctx, user, nil) + g.Dump(err.Items()) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) + t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `3` must be the same as field Pass2"}) + t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `4` must be the same as field Pass1"}) + }) +} + +func Test_CheckStruct_Recursive_SliceStruct_Bail(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Passes []Pass + } + user := &User{ + Name: "", + Passes: []Pass{ + { + Pass1: "1", + Pass2: "2", + }, + { + Pass1: "3", + Pass2: "4", + }, + }, + } + err := g.Validator().Bail().CheckStruct(ctx, user) + g.Dump(err.Items()) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], nil) + t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `1` must be the same as field Pass2"}) + t.Assert(err.Maps()["Pass2"], nil) + }) +} + +func Test_CheckStruct_Recursive_SliceStruct_Required(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Passes []Pass + } + user := &User{} + err := gvalid.CheckStruct(ctx, user, nil) + g.Dump(err.Items()) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) + t.Assert(err.Maps()["Pass1"], nil) + t.Assert(err.Maps()["Pass2"], nil) + }) +} + +func Test_CheckStruct_Recursive_MapStruct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + type User struct { + Id int + Name string `v:"required"` + Passes map[string]Pass + } + user := &User{ + Name: "", + Passes: map[string]Pass{ + "test1": { + Pass1: "1", + Pass2: "2", + }, + "test2": { + Pass1: "3", + Pass2: "4", + }, + }, + } + err := gvalid.CheckStruct(ctx, user, nil) + g.Dump(err.Items()) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], g.Map{"required": "The Name field is required"}) + t.AssertNE(err.Maps()["Pass1"], nil) + t.AssertNE(err.Maps()["Pass2"], nil) + }) +} + +func Test_CheckMap_Recursive_SliceStruct(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `v:"required|same:Pass2"` + Pass2 string `v:"required|same:Pass1"` + } + user := g.Map{ + "Name": "", + "Pass": []Pass{ + { + Pass1: "1", + Pass2: "2", + }, + { + Pass1: "3", + Pass2: "4", + }, + }, + } + err := gvalid.CheckMap(ctx, user, nil) + g.Dump(err.Items()) + t.AssertNE(err, nil) + t.Assert(err.Maps()["Name"], nil) + t.Assert(err.Maps()["Pass1"], g.Map{"same": "The Pass1 value `3` must be the same as field Pass2"}) + t.Assert(err.Maps()["Pass2"], g.Map{"same": "The Pass2 value `4` must be the same as field Pass1"}) + }) +}