From 5a1209c82da4093d31a579b5f53faca9577b3085 Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 15 Nov 2021 16:50:30 +0800 Subject: [PATCH] developing recursive validation --- util/gvalid/gvalid_validator.go | 15 +++ util/gvalid/gvalid_validator_check_struct.go | 90 ++++++++++++++- util/gvalid/gvalid_validator_check_value.go | 2 +- ...valid_z_unit_checkstruct_recursive_test.go | 103 ++++++++++++++++++ 4 files changed, 208 insertions(+), 2 deletions(-) create mode 100755 util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 33772534c..058a18d24 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -40,6 +40,9 @@ func (v *Validator) Clone() *Validator { // I18n sets the i18n manager for the validator. func (v *Validator) I18n(i18nManager *gi18n.Manager) *Validator { + if i18nManager == nil { + return v + } newValidator := v.Clone() newValidator.i18nManager = i18nManager return newValidator @@ -56,6 +59,9 @@ func (v *Validator) Bail() *Validator { // The parameter `data` is usually type of map, which specifies the parameter map used in validation. // Calling this function also sets `useDataInsteadOfObjectAttributes` true no mather the `data` is nil or not. func (v *Validator) Data(data interface{}) *Validator { + if data == nil { + return v + } newValidator := v.Clone() newValidator.data = data newValidator.useDataInsteadOfObjectAttributes = true @@ -64,6 +70,9 @@ func (v *Validator) Data(data interface{}) *Validator { // Rules is a chaining operation function, which sets custom validation rules for current operation. func (v *Validator) Rules(rules interface{}) *Validator { + if rules == nil { + return v + } newValidator := v.Clone() newValidator.rules = rules return newValidator @@ -73,6 +82,9 @@ func (v *Validator) Rules(rules interface{}) *Validator { // The parameter `messages` can be type of string/[]string/map[string]string. It supports sequence in error result // if `rules` is type of []string. func (v *Validator) Messages(messages interface{}) *Validator { + if messages == nil { + return v + } newValidator := v.Clone() newValidator.messages = messages return newValidator @@ -87,6 +99,9 @@ func (v *Validator) RuleFunc(rule string, f RuleFunc) *Validator { // RuleFuncMap registers multiple custom rule functions to current Validator. func (v *Validator) RuleFuncMap(m map[string]RuleFunc) *Validator { + if m == nil { + return v + } newValidator := v.Clone() for k, v := range m { newValidator.ruleFuncMap[k] = v diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 69db1341b..af6d9e08b 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -10,8 +10,10 @@ 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" ) @@ -21,10 +23,85 @@ func (v *Validator) CheckStruct(ctx context.Context, object interface{}) Error { return v.doCheckStruct(ctx, object) } +type doCheckStructRecursivelyInput struct { + Field *structs.Field + ErrorMaps map[string]map[string]error + ResultSequenceRules []fieldRule +} + +func (v *Validator) doCheckStructRecursively(ctx context.Context, in doCheckStructRecursivelyInput) { + 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. fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. + resultSequenceRules = make([]fieldRule, 0) ) fieldMap, err := structs.FieldMap(structs.FieldMapInput{ Pointer: object, @@ -41,6 +118,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error if _, ok := field.Value.Interface().(iNoValidation); ok { continue } + // No validation field tag check. if _, ok := field.TagLookup(noValidationTagName); ok { continue } @@ -54,6 +132,12 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error if field.TagValue != "" { fieldToAliasNameMap[field.Name()] = field.TagValue } + // Recursively check attribute struct/[]string/map/[]map. + v.doCheckStructRecursively(ctx, doCheckStructRecursivelyInput{ + Field: field, + ErrorMaps: errorMaps, + ResultSequenceRules: resultSequenceRules, + }) } } // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. @@ -285,7 +369,11 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } if len(errorMaps) > 0 { - return newValidationError(gcode.CodeValidationFailed, checkRules, errorMaps) + return newValidationError( + gcode.CodeValidationFailed, + append(checkRules, resultSequenceRules...), + errorMaps, + ) } return nil } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index bb6cd4dc2..1d54b7a74 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -46,7 +46,7 @@ func (v *Validator) CheckValue(ctx context.Context, value interface{}) Error { type doCheckValueInput struct { Name string // Name specifies the name of parameter `value`. - Value interface{} // Value specifies the value for this rules to be validated. + 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. DataRaw interface{} // DataRaw specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value. diff --git a/util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go b/util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go new file mode 100755 index 000000000..94357dd01 --- /dev/null +++ b/util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go @@ -0,0 +1,103 @@ +// 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"}) + }) +}