diff --git a/util/gconv/gconv_slice_any.go b/util/gconv/gconv_slice_any.go index 9041886d3..415f47634 100644 --- a/util/gconv/gconv_slice_any.go +++ b/util/gconv/gconv_slice_any.go @@ -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)) diff --git a/util/gconv/gconv_slice_float.go b/util/gconv/gconv_slice_float.go index 4816d7044..3d7b49949 100644 --- a/util/gconv/gconv_slice_float.go +++ b/util/gconv/gconv_slice_float.go @@ -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)) diff --git a/util/gconv/gconv_slice_int.go b/util/gconv/gconv_slice_int.go index 1ca0ab4d6..f28e7fd18 100644 --- a/util/gconv/gconv_slice_int.go +++ b/util/gconv/gconv_slice_int.go @@ -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)) diff --git a/util/gconv/gconv_slice_str.go b/util/gconv/gconv_slice_str.go index 31e766f52..c085d271f 100644 --- a/util/gconv/gconv_slice_str.go +++ b/util/gconv/gconv_slice_str.go @@ -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)) diff --git a/util/gconv/gconv_slice_uint.go b/util/gconv/gconv_slice_uint.go index e77dfc2c0..a1ffa7617 100644 --- a/util/gconv/gconv_slice_uint.go +++ b/util/gconv/gconv_slice_uint.go @@ -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)) diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index ba252d8c6..c489f710e 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -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, diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index a625d6f66..944774f3c 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -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 } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index e4cee548d..38d570b34 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -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 { diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index ae6808791..bd69f9f77 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -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, }) diff --git a/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go b/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go index 7e182d84c..fbd6aa18f 100755 --- a/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_feature_checkstruct_test.go @@ -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 {