From 20f2a6c00333b93c63edc6c54af4390508dac8ef Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 10 Mar 2021 23:28:34 +0800 Subject: [PATCH] add recursive validation feature of struct attribute for package gvalid for #1165 --- internal/structs/structs_field.go | 20 +++++++++++ test/gtest/gtest_util.go | 2 +- util/gvalid/gvalid_validator_check_struct.go | 34 +++++++++++++------ util/gvalid/gvalid_z_unit_checkstruct_test.go | 28 ++++++++++++++- 4 files changed, 72 insertions(+), 12 deletions(-) diff --git a/internal/structs/structs_field.go b/internal/structs/structs_field.go index 8d3e0afe8..a0ad1a1cb 100644 --- a/internal/structs/structs_field.go +++ b/internal/structs/structs_field.go @@ -6,6 +6,8 @@ package structs +import "reflect" + // Tag returns the value associated with key in the tag string. If there is no // such key in the tag, Tag returns the empty string. func (f *Field) Tag(key string) string { @@ -34,6 +36,24 @@ func (f *Field) Type() Type { } } +// Kind returns the reflect.Kind for Value of Field `f`. +func (f *Field) Kind() reflect.Kind { + return f.Value.Kind() +} + +// OriginalKind retrieves and returns the original reflect.Kind for Value of Field `f`. +func (f *Field) OriginalKind() reflect.Kind { + var ( + kind = f.Value.Kind() + value = f.Value + ) + for kind == reflect.Ptr { + value = value.Elem() + kind = value.Kind() + } + return kind +} + // FieldMap retrieves and returns struct field as map[name/tag]*Field from `pointer`. // // The parameter `pointer` should be type of struct/*struct. diff --git a/test/gtest/gtest_util.go b/test/gtest/gtest_util.go index 60b90d8de..a1644afa2 100644 --- a/test/gtest/gtest_util.go +++ b/test/gtest/gtest_util.go @@ -341,7 +341,7 @@ func compareMap(value, expect interface{}) error { return fmt.Errorf(`[ASSERT] EXPECT MAP LENGTH %d == %d`, rvValue.Len(), rvExpect.Len()) } } else { - return fmt.Errorf(`[ASSERT] EXPECT VALUE TO BE A MAP`) + return fmt.Errorf(`[ASSERT] EXPECT VALUE TO BE A MAP, BUT GIVEN "%s"`, rvValue.Kind()) } } return nil diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 580defc75..f56c54177 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -9,6 +9,7 @@ package gvalid import ( "github.com/gogf/gf/internal/structs" "github.com/gogf/gf/util/gconv" + "reflect" "strings" ) @@ -17,13 +18,31 @@ var ( aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. ) -// CheckStruct validates strcut and returns the error result. +// CheckStruct validates struct and returns the error result. // // The parameter should be type of struct/*struct. // The parameter can be type of []string/map[string]string. It supports sequence in error result // if is type of []string. // The optional parameter specifies the custom error messages for specified keys and rules. func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *Error { + var ( + errorMaps = make(ErrorMap) // Returned error. + ) + mapField, err := structs.FieldMap(object, aliasNameTagPriority) + if err != nil { + return newErrorStr("invalid_object", err.Error()) + } + // It checks the struct recursively the its attribute is also a struct. + for _, field := range mapField { + if field.OriginalKind() == reflect.Struct { + if err := v.CheckStruct(field.Value, rules, messages...); err != nil { + // It merges the errors into single error map. + for k, m := range err.errors { + errorMaps[k] = m + } + } + } + } // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. tagField, err := structs.TagFields(object, structTagPriority) if err != nil { @@ -39,7 +58,6 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages customMessage = make(CustomMsg) fieldAliases = make(map[string]string) // Alias names for overwriting struct tag names. errorRules = make([]string, 0) // Sequence rules. - errorMaps = make(ErrorMap) // Returned error ) switch v := rules.(type) { // Sequence tag: []sequence tag @@ -85,10 +103,6 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages return nil } // Checks and extends the parameters map with struct alias tag. - mapField, err := structs.FieldMap(object, aliasNameTagPriority) - if err != nil { - return newErrorStr("invalid_object", err.Error()) - } for nameOrTag, field := range mapField { params[nameOrTag] = field.Value.Interface() params[field.Name()] = field.Value.Interface() @@ -167,10 +181,10 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages // It checks each rule and its value in loop. if e := v.doCheck(key, value, rule, customMessage[key], params); e != nil { _, item := e.FirstItem() - // =========================================================== - // Only in map and struct validations, if value is nil or empty - // string and has no required* rules, it clears the error message. - // =========================================================== + // =================================================================== + // Only in map and struct validations, if value is nil or empty string + // and has no required* rules, it clears the error message. + // =================================================================== if value == nil || gconv.String(value) == "" { required := false // rule => error diff --git a/util/gvalid/gvalid_z_unit_checkstruct_test.go b/util/gvalid/gvalid_z_unit_checkstruct_test.go index 4f630acae..97867763f 100755 --- a/util/gvalid/gvalid_z_unit_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_checkstruct_test.go @@ -226,7 +226,7 @@ func Test_CheckStruct(t *testing.T) { }) } -func Test_CheckStruct_With_EmbedObject(t *testing.T) { +func Test_CheckStruct_With_EmbeddedObject(t *testing.T) { gtest.C(t, func(t *gtest.T) { type Pass struct { Pass1 string `valid:"password1@required|same:password2#请输入您的密码|您两次输入的密码不一致"` @@ -252,6 +252,32 @@ func Test_CheckStruct_With_EmbedObject(t *testing.T) { }) } +func Test_CheckStruct_With_StructAttribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Pass struct { + Pass1 string `valid:"password1@required|same:password2#请输入您的密码|您两次输入的密码不一致"` + Pass2 string `valid:"password2@required|same:password1#请再次输入您的密码|您两次输入的密码不一致"` + } + type User struct { + Id int + Name string `valid:"name@required#请输入您的姓名"` + Passwords Pass + } + user := &User{ + Name: "", + Passwords: Pass{ + Pass1: "1", + Pass2: "2", + }, + } + err := gvalid.CheckStruct(user, nil) + t.AssertNE(err, nil) + t.Assert(err.Maps()["name"], g.Map{"required": "请输入您的姓名"}) + t.Assert(err.Maps()["password1"], g.Map{"same": "您两次输入的密码不一致"}) + t.Assert(err.Maps()["password2"], g.Map{"same": "您两次输入的密码不一致"}) + }) +} + func Test_CheckStruct_Optional(t *testing.T) { gtest.C(t, func(t *gtest.T) { type Params struct {