add recursive validation feature of struct attribute for package gvalid for #1165

This commit is contained in:
John Guo 2021-03-10 23:28:34 +08:00
parent 0d4c1c47d5
commit 20f2a6c003
4 changed files with 72 additions and 12 deletions

View File

@ -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.

View File

@ -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

View File

@ -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 <object> should be type of struct/*struct.
// The parameter <rules> can be type of []string/map[string]string. It supports sequence in error result
// if <rules> is type of []string.
// The optional parameter <messages> 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 <messages> 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

View File

@ -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 {