Skip to content

Commit

Permalink
♻️ update: refactoring the slice item validate logic
Browse files Browse the repository at this point in the history
- add new method Validation.ValidateErr(scene ...string) error
  • Loading branch information
inhere committed Jul 24, 2023
1 parent 9a0269c commit b5f1569
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 69 deletions.
7 changes: 6 additions & 1 deletion data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ func (d *StructData) TryGet(field string) (val interface{}, exist, zero bool) {

var fv reflect.Value
// want to get sub struct field.
if strings.IndexRune(field, '.') > 0 {
if strings.IndexByte(field, '.') > 0 {
fieldNodes := strings.Split(field, ".")
topLevelField, ok := d.valueTpy.FieldByName(fieldNodes[0])
if !ok {
Expand All @@ -529,6 +529,11 @@ func (d *StructData) TryGet(field string) (val interface{}, exist, zero bool) {
fieldNodes = fieldNodes[1:]
// lastIndex := len(fieldNodes) - 1

// last key is wildcard, return all sub-value
if len(fieldNodes) == 1 && fieldNodes[0] == maputil.Wildcard {
return fv.Interface(), true, fv.IsZero()
}

kind = fv.Type().Kind()
for _, fieldNode := range fieldNodes {
// fieldNode = strings.ReplaceAll(fieldNode, "\"", "") // for strings as keys
Expand Down
2 changes: 1 addition & 1 deletion data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestData(t *testing.T) {
is.Equal("inhere", val)
is.Nil(d.BindJSON(nil))

// mp := map[string]interface{}{"age": "45"}
// mp := map[string]any{"age": "45"}
// d = FromMap(&mp)

// StructData
Expand Down
12 changes: 6 additions & 6 deletions issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"testing"
"time"

"github.com/gookit/goutil/stdutil"
"github.com/gookit/goutil"
"github.com/gookit/goutil/timex"
"github.com/gookit/validate/locales/zhcn"

Expand Down Expand Up @@ -1087,7 +1087,7 @@ func TestIssues_148(t *testing.T) {
}

a := &A{}
stdutil.PanicIf(jsonutil.DecodeString(`{"T2":"xxx"}`, a))
goutil.PanicErr(jsonutil.DecodeString(`{"T2":"xxx"}`, a))

v := validate.Struct(a)
v.Validate()
Expand All @@ -1102,7 +1102,7 @@ func TestIssues_148(t *testing.T) {
}

b := &B{}
stdutil.PanicIf(jsonutil.DecodeString(`{"T2":"xxx"}`, b))
goutil.PanicIfErr(jsonutil.DecodeString(`{"T2":"xxx"}`, b))

// dump.Println(b)
v = validate.Struct(b)
Expand Down Expand Up @@ -1261,7 +1261,7 @@ func TestIssues_172(t *testing.T) {
}

// https://github.com/gookit/validate/issues/213
func TestIssues_141(t *testing.T) {
func TestIssues_213(t *testing.T) {
type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required"`
Expand All @@ -1271,7 +1271,7 @@ func TestIssues_141(t *testing.T) {
}

f := &Form{}
v := validate.Struct(&f) // nolint:varnamelen
v := validate.Struct(f) // nolint:varnamelen
assert.False(t, v.Validate())
fmt.Println(v.Errors)

Expand All @@ -1280,7 +1280,7 @@ func TestIssues_141(t *testing.T) {
{Name: "tome"},
},
}
v = validate.Struct(&f) // nolint:varnamelen
v = validate.Struct(f) // nolint:varnamelen
assert.False(t, v.Validate())
fmt.Println(v.Errors)
}
2 changes: 1 addition & 1 deletion messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ func (t *Translator) format(errMsg, field string, args []interface{}) string {
msgArgs := []string{
"{field}", field,
"{values}", arrutil.ToString(args),
"{args0}", strutil.MustString(args[0]),
"{args0}", strutil.SafeString(args[0]),
}

// {args1end} -> args[1:]
Expand Down
40 changes: 11 additions & 29 deletions util.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ var nilRVal = reflect.ValueOf(nilObj)
// NilValue TODO a reflect nil value, use for instead of nilRVal
var NilValue = reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem())

// From package "text/template" -> text/template/funcs.go
var (
emptyValue = reflect.Value{}
errorType = reflect.TypeOf((*error)(nil)).Elem()
// fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
// reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
)

// IsNilObj check value is internal NilObject
func IsNilObj(val interface{}) bool {
_, ok := val.(NilObject)
Expand Down Expand Up @@ -103,28 +111,9 @@ func buildArgs(val interface{}, args []interface{}) []interface{} {
return newArgs
}

// ValueIsEmpty check. TODO use reflects.IsEmpty()
// ValueIsEmpty check. alias of reflects.IsEmpty()
func ValueIsEmpty(v reflect.Value) bool {
switch v.Kind() {
case reflect.Invalid:
return true
case reflect.String, reflect.Array:
return v.Len() == 0
case reflect.Map, reflect.Slice:
return v.Len() == 0 || v.IsNil()
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}

return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
return reflects.IsEmpty(v)
}

// ValueLen get value length.
Expand Down Expand Up @@ -270,14 +259,6 @@ func convToBasicType(val interface{}) (value interface{}, err error) {
return
}

// From package "text/template" -> text/template/funcs.go
var (
emptyValue = reflect.Value{}
errorType = reflect.TypeOf((*error)(nil)).Elem()
// fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
// reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
)

func panicf(format string, args ...interface{}) {
panic("validate: " + fmt.Sprintf(format, args...))
}
Expand All @@ -297,6 +278,7 @@ func checkValidatorFunc(name string, fn interface{}) reflect.Value {
panicf("validator '%s' func at least one parameter position", name)
}

// TODO support return error as validate error.
if ft.NumOut() != 1 || ft.Out(0).Kind() != reflect.Bool {
panicf("validator '%s' func must be return a bool value", name)
}
Expand Down
100 changes: 77 additions & 23 deletions validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,17 @@ func (v *Validation) ValidateData(data DataFace) bool {
return v.Validate()
}

// ValidateE do validate processing and return error
// ValidateErr do validate processing and return error
func (v *Validation) ValidateErr(scene ...string) error {
if v.Validate(scene...) {
return nil
}
return v.Errors
}

// ValidateE do validate processing and return Errors
//
// TIP: need use len() to check the Errors is empty or not.
func (v *Validation) ValidateE(scene ...string) Errors {
if v.Validate(scene...) {
return nil
Expand Down Expand Up @@ -159,12 +169,11 @@ func (r *Rule) Apply(v *Validation) (stop bool) {

// validate field value
if r.valueValidate(field, name, val, v) {
v.safeData[field] = val // save validated value.
v.safeData[field] = val
} else { // build and collect error message
v.AddError(field, r.validator, r.errorMessage(field, r.validator, v))
}

// stop on error
if v.shouldStop() {
return true
}
Expand Down Expand Up @@ -213,17 +222,37 @@ func (r *Rule) fileValidate(field, name string, v *Validation) uint8 {
return statusFail
}

// value by tryGet(key) TODO
type value struct {
val any
key string
// has dot-star ".*" in the key. eg: details.sub.*.field
dotStar bool
// last index of dot-star on the key. eg: details.sub.*.field, lastIdx=11
lastIdx int
// is required or requiredXX check
require bool
}

// validate the field value
//
// - field: the field name. eg: "name", "details.sub.*.field"
// - name: the validator name. eg: "required", "min"
func (r *Rule) valueValidate(field, name string, val interface{}, v *Validation) (ok bool) {
// "-" OR "safe" mark field value always is safe.
if name == "-" || name == "safe" {
return true
}

// "required": check field value is not empty.
// if name == "required" {
// return !IsEmpty(val)
// }

// call custom validator in the rule.
fm := r.checkFuncMeta
if fm == nil {
// get validator for global or validation
// fallback: get validator for global or validation
fm = v.validatorMeta(name)
if fm == nil {
panicf("the validator '%s' does not exist", r.validator)
Expand All @@ -244,25 +273,37 @@ func (r *Rule) valueValidate(field, name string, val interface{}, v *Validation)
return false
}

ft := fm.fv.Type()
ft := fm.fv.Type() // type of check func
arg0Kind := ft.In(0).Kind()

// rftVal := reflect.Indirect(reflect.ValueOf(val))
rftVal := reflect.ValueOf(val)
valKind := rftVal.Kind()
isRequired := fm.name == "required"
arrField := ""
if strings.Contains(field, ".*.") {
arrField = strings.Split(field, ".*.")[1]
}
// isRequired := fm.name == "required"
// arrField := ""
// if strings.Contains(field, ".*.") {
// arrField = strings.Split(field, ".*.")[1]
// }

// feat: support check sub element in a slice list. eg: field=names.*
hasSliceSuffix := len(strings.Split(field, ".*")) > 1
if valKind == reflect.Slice && hasSliceSuffix {
var subVal interface{}
for i := 0; i < rftVal.Len(); i++ {
dotStarIdx := strings.LastIndex(field, ".*")
// hasSliceSuffix := len(strings.Split(field, ".*")) > 1
if valKind == reflect.Slice && dotStarIdx > 1 {
sliceLen := rftVal.Len()
// dsIsLast := dotStarIdx == len(field)-2
// dsCount := strings.Count(field, ".*")

// check requiredXX validate TODO need flatten multi level slice, count ".*" number.
if !r.nameNotRequired && (sliceLen == 0 || sliceLen < rftVal.Cap()) {
return callValidator(v, fm, field, val, r.arguments)
}

var subVal any
// check each element in the slice.
for i := 0; i < sliceLen; i++ {
subRv := rftVal.Index(i)
subKind := subRv.Kind()

// 1.1 convert field value type, is func first argument.
if r.nameNotRequired && arg0Kind != reflect.Interface && arg0Kind != subKind {
subVal, ok = convValAsFuncArg0Type(arg0Kind, subKind, subRv.Interface())
Expand All @@ -273,20 +314,22 @@ func (r *Rule) valueValidate(field, name string, val interface{}, v *Validation)
} else {
subVal = subRv.Interface()
}
switch subVal := subVal.(type) {
case map[string]any:
if arrField != "" {
if _, exists := subVal[arrField]; !exists && isRequired {
return false
}
}
}

// switch subVal := subVal.(type) {
// case map[string]any:
// if arrField != "" {
// if _, exists := subVal[arrField]; !exists && isRequired {
// return false
// }
// }
// }

// 2. call built in validator
if !callValidator(v, fm, field, subVal, r.arguments) {
return false
}
}

return true
}

Expand Down Expand Up @@ -315,13 +358,13 @@ func convValAsFuncArg0Type(arg0Kind, valKind reflect.Kind, val interface{}) (int
if nVal, _ := convTypeByBaseKind(val, bk, arg0Kind); nVal != nil {
return nVal, true
}

// TODO return nil, false
return val, true
}

func callValidator(v *Validation, fm *funcMeta, field string, val interface{}, args []interface{}) (ok bool) {
// use `switch` can avoid using reflection to call methods and improve speed
// fm.name please see pkg var: validatorValues
switch fm.name {
case "required":
ok = v.Required(field, val)
Expand Down Expand Up @@ -502,6 +545,17 @@ func callValidatorValue(fv reflect.Value, val interface{}, args []interface{}) b
argIn[i+1] = rftValA
}

// TODO panic recover, refer the text/template/funcs.go
// defer func() {
// if r := recover(); r != nil {
// if e, ok := r.(error); ok {
// err = e
// } else {
// err = fmt.Errorf("%v", r)
// }
// }
// }()

// NOTICE: f.CallSlice()与Call() 不一样的是,CallSlice参数的最后一个会被展开
// vs := fv.Call(argIn)
return fv.Call(argIn)[0].Bool()
Expand Down
Loading

0 comments on commit b5f1569

Please sign in to comment.