improve struct validation for package gvalid

This commit is contained in:
John Guo 2021-05-11 20:57:30 +08:00
parent 034a3f1808
commit a4240bdfb7
3 changed files with 103 additions and 65 deletions

View File

@ -9,11 +9,19 @@ package gvalid
import (
"github.com/gogf/gf/internal/structs"
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gutil"
"reflect"
"strings"
)
// doCheckStructWithParamMapInput is used for struct validation for internal function.
type doCheckStructWithParamMapInput struct {
Object interface{} // Can be type of struct/*struct.
ParamMap interface{} // Validation parameter map. Note that it acts different according attribute `ParamMapStrict`.
ParamMapStrict bool // Strictly using `ParamMap` as its validation source. If false, it ignores `ParamMap` and retrieves values from `Object`.
CustomRules interface{} // Custom validation rules.
CustomErrorMessageMap CustomMsg // Custom error message map for validation rules.
}
var (
structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array.
aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array.
@ -25,8 +33,18 @@ var (
// 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 {
return v.CheckStructWithParamMap(object, nil, rules, messages...)
func (v *Validator) CheckStruct(object interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error {
var message CustomMsg
if len(customErrorMessageMap) > 0 {
message = customErrorMessageMap[0]
}
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
Object: object,
ParamMap: nil,
ParamMapStrict: false,
CustomRules: customRules,
CustomErrorMessageMap: message,
})
}
// CheckStructWithParamMap validates struct with given parameter map and returns the error result.
@ -35,18 +53,32 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages
// 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) CheckStructWithParamMap(object interface{}, paramMap interface{}, rules interface{}, messages ...CustomMsg) *Error {
func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interface{}, customRules interface{}, customErrorMessageMap ...CustomMsg) *Error {
var message CustomMsg
if len(customErrorMessageMap) > 0 {
message = customErrorMessageMap[0]
}
return v.doCheckStructWithParamMap(&doCheckStructWithParamMapInput{
Object: object,
ParamMap: paramMap,
ParamMapStrict: true,
CustomRules: customRules,
CustomErrorMessageMap: message,
})
}
func (v *Validator) doCheckStructWithParamMap(input *doCheckStructWithParamMapInput) *Error {
var (
errorMaps = make(ErrorMap) // Returning error.
)
fieldMap, err := structs.FieldMap(object, aliasNameTagPriority)
fieldMap, err := structs.FieldMap(input.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 fieldMap {
if field.OriginalKind() == reflect.Struct {
if err := v.CheckStruct(field.Value, rules, messages...); err != nil {
if err := v.CheckStruct(field.Value, input.CustomRules, input.CustomErrorMessageMap); err != nil {
// It merges the errors into single error map.
for k, m := range err.errors {
errorMaps[k] = m
@ -55,12 +87,12 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa
}
}
// It here must use structs.TagFields not structs.FieldMap to ensure error sequence.
tagField, err := structs.TagFields(object, structTagPriority)
tagField, err := structs.TagFields(input.Object, structTagPriority)
if err != nil {
return newErrorStr("invalid_object", err.Error())
}
// If there's no struct tag and validation rules, it does nothing and returns quickly.
if len(tagField) == 0 && rules == nil {
if len(tagField) == 0 && input.CustomRules == nil {
return nil
}
@ -71,12 +103,7 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa
fieldAliases = make(map[string]string) // Alias names for `messages` overwriting struct tag names.
errorRules = make([]string, 0) // Sequence rules.
)
if paramMap == nil {
inputParamMap = make(map[string]interface{})
} else {
inputParamMap = gconv.Map(paramMap)
}
switch v := rules.(type) {
switch v := input.CustomRules.(type) {
// Sequence tag: []sequence tag
// Sequence has order for error results.
case []string:
@ -119,21 +146,22 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa
if len(tagField) == 0 && len(checkRules) == 0 {
return nil
}
// Input parameter map handling.
if input.ParamMap == nil || !input.ParamMapStrict {
inputParamMap = make(map[string]interface{})
} else {
inputParamMap = gconv.Map(input.ParamMap)
}
// Checks and extends the parameters map with struct alias tag.
for nameOrTag, field := range fieldMap {
if _, ok := inputParamMap[nameOrTag]; !ok {
if foundKey, foundValue := gutil.MapPossibleItemByKey(inputParamMap, nameOrTag); foundKey != "" {
inputParamMap[nameOrTag] = foundValue
inputParamMap[field.Name()] = foundValue
} else {
// If the custom tag alias name is quite different from the attribute name,
// it just uses the value of the attribute.
// Note that the attribute may have default value according to its type.
inputParamMap[nameOrTag] = field.Value.Interface()
if !input.ParamMapStrict {
for nameOrTag, field := range fieldMap {
inputParamMap[nameOrTag] = field.Value.Interface()
if nameOrTag != field.Name() {
inputParamMap[field.Name()] = field.Value.Interface()
}
}
}
for _, field := range tagField {
fieldName := field.Name()
// sequence tag == struct tag
@ -187,8 +215,8 @@ func (v *Validator) CheckStructWithParamMap(object interface{}, paramMap interfa
// Custom error messages,
// which have the most priority than `rules` and struct tag.
if len(messages) > 0 && len(messages[0]) > 0 {
for k, v := range messages[0] {
if len(input.CustomErrorMessageMap) > 0 {
for k, v := range input.CustomErrorMessageMap {
if a, ok := fieldAliases[k]; ok {
// Overwrite the key of field name.
customMessage[a] = v

View File

@ -8,6 +8,7 @@ package gvalid_test
import (
"github.com/gogf/gf/container/gvar"
"github.com/gogf/gf/os/gtime"
"testing"
"github.com/gogf/gf/frame/g"
@ -352,3 +353,51 @@ func Test_CheckStruct_InvalidRule(t *testing.T) {
t.AssertNE(err, nil)
})
}
func TestValidator_CheckStructWithParamMap(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid" v:"required"`
Nickname string `json:"nickname" v:"required-with:Uid"`
}
data := UserApiSearch{}
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid" v:"required"`
Nickname string `json:"nickname" v:"required-with:Uid"`
}
data := UserApiSearch{
Uid: 1,
}
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid"`
Nickname string `json:"nickname" v:"required-with:Uid"`
StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"`
EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"`
}
data := UserApiSearch{
StartTime: nil,
EndTime: nil,
}
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid"`
Nickname string `json:"nickname" v:"required-with:Uid"`
StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"`
EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"`
}
data := UserApiSearch{
StartTime: gtime.Now(),
EndTime: nil,
}
t.AssertNE(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
}

View File

@ -1,39 +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 (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/test/gtest"
"github.com/gogf/gf/util/gvalid"
"testing"
)
func TestValidator_CheckStructWithParamMap(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid" v:"required"`
Nickname string `json:"nickname" v:"required-with:Uid"`
}
data := UserApiSearch{}
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {
Uid int64 `json:"uid"`
Nickname string `json:"nickname" v:"required-with:Uid"`
StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"`
EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"`
}
data := UserApiSearch{
StartTime: nil,
EndTime: nil,
}
t.Assert(gvalid.CheckStructWithParamMap(data, g.Map{}, nil), nil)
})
}