mirror of
https://gitee.com/johng/gf
synced 2026-06-07 02:12:11 +08:00
developing recursive validation
This commit is contained in:
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
103
util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go
Executable file
103
util/gvalid/gvalid_z_unit_checkstruct_recursive_test.go
Executable file
@ -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"})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user