developing recursive validation

This commit is contained in:
John Guo
2021-11-15 16:50:30 +08:00
parent e23704d694
commit 5a1209c82d
4 changed files with 208 additions and 2 deletions

View File

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

View File

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

View File

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

View 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"})
})
}