improve recursilve validation feature for package gvalid

This commit is contained in:
John Guo
2022-03-17 20:27:59 +08:00
parent bceb5fc7de
commit 97b8f0f781
10 changed files with 167 additions and 60 deletions

View File

@ -9,6 +9,7 @@ package gconv
import (
"reflect"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
)
@ -62,9 +63,13 @@ func Interfaces(any interface{}) []interface{} {
array[k] = v
}
case []uint8:
array = make([]interface{}, len(value))
for k, v := range value {
array[k] = v
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]interface{}, len(value))
for k, v := range value {
array[k] = v
}
}
case []uint16:
array = make([]interface{}, len(value))

View File

@ -9,6 +9,7 @@ package gconv
import (
"reflect"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
)
@ -81,9 +82,13 @@ func Float32s(any interface{}) []float32 {
array = append(array, Float32(v))
}
case []uint8:
array = make([]float32, len(value))
for k, v := range value {
array[k] = Float32(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]float32, len(value))
for k, v := range value {
array[k] = Float32(v)
}
}
case []uint16:
array = make([]float32, len(value))
@ -201,9 +206,13 @@ func Float64s(any interface{}) []float64 {
array = append(array, Float64(v))
}
case []uint8:
array = make([]float64, len(value))
for k, v := range value {
array[k] = Float64(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]float64, len(value))
for k, v := range value {
array[k] = Float64(v)
}
}
case []uint16:
array = make([]float64, len(value))

View File

@ -9,6 +9,7 @@ package gconv
import (
"reflect"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
)
@ -69,9 +70,13 @@ func Ints(any interface{}) []int {
array[k] = int(v)
}
case []uint8:
array = make([]int, len(value))
for k, v := range value {
array[k] = int(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]int, len(value))
for k, v := range value {
array[k] = int(v)
}
}
case []uint16:
array = make([]int, len(value))
@ -194,9 +199,13 @@ func Int32s(any interface{}) []int32 {
array[k] = int32(v)
}
case []uint8:
array = make([]int32, len(value))
for k, v := range value {
array[k] = int32(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]int32, len(value))
for k, v := range value {
array[k] = int32(v)
}
}
case []uint16:
array = make([]int32, len(value))
@ -319,9 +328,13 @@ func Int64s(any interface{}) []int64 {
array[k] = int64(v)
}
case []uint8:
array = make([]int64, len(value))
for k, v := range value {
array[k] = int64(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]int64, len(value))
for k, v := range value {
array[k] = int64(v)
}
}
case []uint16:
array = make([]int64, len(value))

View File

@ -9,6 +9,7 @@ package gconv
import (
"reflect"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
)
@ -57,9 +58,13 @@ func Strings(any interface{}) []string {
array[k] = String(v)
}
case []uint8:
array = make([]string, len(value))
for k, v := range value {
array[k] = String(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]string, len(value))
for k, v := range value {
array[k] = String(v)
}
}
case []uint16:
array = make([]string, len(value))

View File

@ -10,6 +10,7 @@ import (
"reflect"
"strings"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/internal/utils"
)
@ -76,9 +77,13 @@ func Uints(any interface{}) []uint {
case []uint:
array = value
case []uint8:
array = make([]uint, len(value))
for k, v := range value {
array[k] = uint(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]uint, len(value))
for k, v := range value {
array[k] = uint(v)
}
}
case []uint16:
array = make([]uint, len(value))
@ -210,9 +215,13 @@ func Uint32s(any interface{}) []uint32 {
array[k] = uint32(v)
}
case []uint8:
array = make([]uint32, len(value))
for k, v := range value {
array[k] = uint32(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]uint32, len(value))
for k, v := range value {
array[k] = uint32(v)
}
}
case []uint16:
array = make([]uint32, len(value))
@ -341,9 +350,13 @@ func Uint64s(any interface{}) []uint64 {
array[k] = uint64(v)
}
case []uint8:
array = make([]uint64, len(value))
for k, v := range value {
array[k] = uint64(v)
if json.Valid(value) {
_ = json.UnmarshalUseNumber(value, &array)
} else {
array = make([]uint64, len(value))
for k, v := range value {
array[k] = uint64(v)
}
}
case []uint16:
array = make([]uint64, len(value))

View File

@ -9,6 +9,7 @@ package gvalid
import (
"strings"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/text/gstr"
@ -32,7 +33,7 @@ type Error interface {
// validationError is the validation error for validation result.
type validationError struct {
code gcode.Code // Error code.
rules []fieldRule // Rules by sequence, which is used for keeping error sequence.
rules []fieldRule // Rules by sequence, which is used for keeping error sequence only.
errors map[string]map[string]error // Error map:map[field]map[rule]message
firstKey string // The first error rule key(empty in default).
firstItem map[string]error // The first error rule value(nil in default).
@ -52,6 +53,16 @@ func newValidationError(code gcode.Code, rules []fieldRule, fieldRuleErrorMap ma
}
fieldRuleErrorMap[field] = ruleErrorMap
}
// Filter repeated sequence rules.
var ruleNameSet = gset.NewStrSet()
for i := 0; i < len(rules); {
if !ruleNameSet.AddIfNotExist(rules[i].Name) {
// Delete repeated rule.
rules = append(rules[:i], rules[i+1:]...)
continue
}
i++
}
return &validationError{
code: code,
rules: rules,

View File

@ -19,15 +19,15 @@ import (
// Validator is the validation manager for chaining operations.
type Validator struct {
i18nManager *gi18n.Manager // I18n manager for error message translation.
data interface{} // Validation data, which can be a map, struct or a certain value to be validated.
assoc interface{} // Associated data, which is usually a map, for union validation.
rules interface{} // Custom validation data.
messages interface{} // Custom validation error messages, which can be string or type of CustomMsg.
ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator.
useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`.
bail bool // Stop validation after the first validation error.
caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison.
i18nManager *gi18n.Manager // I18n manager for error message translation.
data interface{} // Validation data, which can be a map, struct or a certain value to be validated.
assoc interface{} // Associated data, which is usually a map, for union validation.
rules interface{} // Custom validation data.
messages interface{} // Custom validation error messages, which can be string or type of CustomMsg.
ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator.
useAssocInsteadOfObjectAttributes bool // Using `assoc` as its validation source instead of attribute values from `Object`.
bail bool // Stop validation after the first validation error.
caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison.
}
// New creates and returns a new Validator.
@ -125,14 +125,14 @@ func (v *Validator) Data(data interface{}) *Validator {
// Assoc is a chaining operation function, which sets associated validation data for current operation.
// The optional parameter `assoc` is usually type of map, which specifies the parameter map used in union validation.
// Calling this function with `assoc` also sets `useDataInsteadOfObjectAttributes` true
// Calling this function with `assoc` also sets `useAssocInsteadOfObjectAttributes` true
func (v *Validator) Assoc(assoc interface{}) *Validator {
if assoc == nil {
return v
}
newValidator := v.Clone()
newValidator.assoc = assoc
newValidator.useDataInsteadOfObjectAttributes = true
newValidator.useAssocInsteadOfObjectAttributes = true
return newValidator
}

View File

@ -109,13 +109,13 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error
return nil
}
// Input parameter map handling.
if v.assoc == nil || !v.useDataInsteadOfObjectAttributes {
if v.assoc == nil || !v.useAssocInsteadOfObjectAttributes {
inputParamMap = make(map[string]interface{})
} else {
inputParamMap = gconv.Map(v.assoc)
}
// Checks and extends the parameters map with struct alias tag.
if !v.useDataInsteadOfObjectAttributes {
if !v.useAssocInsteadOfObjectAttributes {
for nameOrTag, field := range fieldMap {
inputParamMap[nameOrTag] = field.Value.Interface()
if nameOrTag != field.Name() {
@ -147,7 +147,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error
// It here extends the params map using alias names.
// Note that the variable `name` might be alias name or attribute name.
if _, ok := inputParamMap[name]; !ok {
if !v.useDataInsteadOfObjectAttributes {
if !v.useAssocInsteadOfObjectAttributes {
inputParamMap[name] = field.Value.Interface()
} else {
if name != fieldName {
@ -226,7 +226,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error
// Temporary variable for value.
var value interface{}
// It checks the struct recursively if its attribute is an embedded struct.
// It checks the struct recursively if its attribute is a struct/struct slice.
for _, field := range fieldMap {
// No validation interface implements check.
if _, ok := field.Value.Interface().(iNoValidation); ok {

View File

@ -15,10 +15,10 @@ import (
"time"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/net/gipv4"
"github.com/gogf/gf/v2/net/gipv6"
"github.com/gogf/gf/v2/os/gtime"
@ -538,11 +538,11 @@ func (v *Validator) doCheckSingleBuildInRules(ctx context.Context, in doCheckBui
}
type doCheckValueRecursivelyInput struct {
Value interface{}
Type reflect.Type
OriginKind reflect.Kind
ErrorMaps map[string]map[string]error
ResultSequenceRules *[]fieldRule
Value interface{} // Value to be validated.
Type reflect.Type // Struct/map/slice type which to be recursively validated.
OriginKind reflect.Kind // Struct/map/slice kind to be asserted in following switch case.
ErrorMaps map[string]map[string]error // The validated failed error map.
ResultSequenceRules *[]fieldRule // The validated failed rule in sequence.
}
func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValueRecursivelyInput) {
@ -563,13 +563,16 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue
}
case reflect.Map:
var dataMap = gconv.Map(in.Value)
var (
dataMap = gconv.Map(in.Value)
mapTypeElem = in.Type.Elem()
mapTypeKind = mapTypeElem.Kind()
)
for _, item := range dataMap {
originTypeAndKind := reflection.OriginTypeAndKind(item)
v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
Value: item,
Type: originTypeAndKind.InputType,
OriginKind: originTypeAndKind.OriginKind,
Type: mapTypeElem,
OriginKind: mapTypeKind,
ErrorMaps: in.ErrorMaps,
ResultSequenceRules: in.ResultSequenceRules,
})
@ -580,16 +583,20 @@ func (v *Validator) doCheckValueRecursively(ctx context.Context, in doCheckValue
}
case reflect.Slice, reflect.Array:
array := gconv.Interfaces(in.Value)
var array []interface{}
if gjson.Valid(in.Value) {
array = gconv.Interfaces(gconv.Bytes(in.Value))
} else {
array = gconv.Interfaces(in.Value)
}
if len(array) == 0 {
return
}
for _, item := range array {
originTypeAndKind := reflection.OriginTypeAndKind(item)
v.doCheckValueRecursively(ctx, doCheckValueRecursivelyInput{
Value: item,
Type: originTypeAndKind.InputType,
OriginKind: originTypeAndKind.OriginKind,
Type: in.Type.Elem(),
OriginKind: in.Type.Elem().Kind(),
ErrorMaps: in.ErrorMaps,
ResultSequenceRules: in.ResultSequenceRules,
})

View File

@ -404,6 +404,50 @@ func Test_CheckStruct_InvalidRule(t *testing.T) {
})
}
func Test_CheckStruct_Recursively_SliceAttribute(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string `v:"required#Student Name is required"`
Age int `v:"required"`
}
type Teacher struct {
Name string `v:"required#Teacher Name is required"`
Students []Student `v:"required"`
}
var (
teacher = Teacher{}
data = g.Map{
"name": "john",
"students": `[{"age":2}, {"name":"jack", "age":4}]`,
}
)
err := g.Validator().Assoc(data).Data(teacher).Run(ctx)
t.Assert(err, `Student Name is required`)
})
}
func Test_CheckStruct_Recursively_MapAttribute(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Student struct {
Name string `v:"required#Student Name is required"`
Age int `v:"required"`
}
type Teacher struct {
Name string `v:"required#Teacher Name is required"`
Students map[string]Student `v:"required"`
}
var (
teacher = Teacher{}
data = g.Map{
"name": "john",
"students": `{"john":{"age":18}}`,
}
)
err := g.Validator().Assoc(data).Data(teacher).Run(ctx)
t.Assert(err, `Student Name is required`)
})
}
func TestValidator_CheckStructWithData(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type UserApiSearch struct {