diff --git a/.github/changelog.yml b/.github/changelog.yml index d60b2cb..a5d7387 100644 --- a/.github/changelog.yml +++ b/.github/changelog.yml @@ -28,7 +28,7 @@ rules: contains: ['refactor:'] - name: Fixed start_withs: [fix] - contains: ['fix:'] + contains: ['fix:', 'bug:'] - name: Feature start_withs: [feat, new] contains: [feature, 'feat:'] diff --git a/issues_test.go b/issues_test.go index b3ec624..6fe9937 100644 --- a/issues_test.go +++ b/issues_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gookit/goutil" + "github.com/gookit/goutil/maputil" "github.com/gookit/goutil/timex" "github.com/gookit/validate/locales/zhcn" @@ -1319,3 +1320,45 @@ func TestIssues_217(t *testing.T) { assert.False(t, ok3) // Should fail and fails. } + +// https://github.com/gookit/validate/issues/221 +func TestIssues_221(t *testing.T) { + m := map[string]any{ + "clinics": []map[string]any{ + { + "clinic_id": "1", + "doctors": []map[string]any{ + { + "doctor_id": 1, + "dates": []map[string]string{ + { + "date": "2023-01-01", + }, + }, + }, + }, + }, + }, + } + + dump.Println(maputil.GetByPath("clinics.*.doctors.*.dates.*.date", m)) + + v := validate.Map(m) + + v.StringRule("clinics", "required|array") + v.StringRule("clinics.*.clinic_id", "required|string") + v.StringRule("clinics.*.doctors", "required|array") + v.StringRule("clinics.*.doctors.*.doctor_id", "required") + v.StringRule("clinics.*.doctors.*.dates", "required|array") + v.StringRule("clinics.*.doctors.*.dates.*.date", "required|string") + + if assert.True(t, v.Validate()) { // validate ok + safeData := v.SafeData() + + fmt.Println("Validation OK:") + dump.Println(safeData) + } else { + fmt.Println("Validation Fail:") + fmt.Println(v.Errors) // all error messages + } +} diff --git a/messages.go b/messages.go index b162a4c..4bc63e7 100644 --- a/messages.go +++ b/messages.go @@ -292,8 +292,7 @@ type Translator struct { // format: {"field": "translate name"} labelMap map[string]string // the error message data map. - // key allow: - // TODO + // key allow: TODO messages map[string]string } diff --git a/util.go b/util.go index ad0e401..b9d2016 100644 --- a/util.go +++ b/util.go @@ -84,7 +84,7 @@ func stringSplit(str, sep string) (ss []string) { return } -// TODO use arrutil.StringsToSlice() +// TODO use arrutil.StringsToAnys() func strings2Args(strings []string) []any { args := make([]any, len(strings)) for i, s := range strings { @@ -111,6 +111,47 @@ func buildArgs(val any, args []any) []any { return newArgs } +var anyType = reflect.TypeOf((*any)(nil)).Elem() + +// FlatSlice flatten multi-level slice to given depth-level slice. +// +// Example: +// +// FlatSlice([]any{ []any{3, 4}, []any{5, 6} }, 1) // Output: []any{3, 4, 5, 6} +// +// always return reflect.Value of []any. note: maybe flatSl.Cap != flatSl.Len +func flatSlice(sl reflect.Value, depth int) reflect.Value { + items := make([]reflect.Value, 0, sl.Cap()) + slCap := addSliceItem(sl, depth, func(item reflect.Value) { + items = append(items, item) + }) + + flatSl := reflect.MakeSlice(reflect.SliceOf(anyType), 0, slCap) + flatSl = reflect.Append(flatSl, items...) + + return flatSl +} + +func addSliceItem(sl reflect.Value, depth int, collector func(item reflect.Value)) (c int) { + for i := 0; i < sl.Len(); i++ { + v := reflects.Elem(sl.Index(i)) + + if depth > 0 { + if v.Kind() != reflect.Slice { + panic(fmt.Sprintf("depth: %d, the value of index %d is not slice", depth, i)) + } + c += addSliceItem(v, depth-1, collector) + } else { + collector(v) + } + } + + if depth == 0 { + c = sl.Cap() + } + return c +} + // ValueIsEmpty check. alias of reflects.IsEmpty() func ValueIsEmpty(v reflect.Value) bool { return reflects.IsEmpty(v) diff --git a/util_test.go b/util_test.go index 14ef545..48a6c36 100644 --- a/util_test.go +++ b/util_test.go @@ -31,6 +31,49 @@ func TestValueLen(t *testing.T) { is.Equal(-1, ValueLen(reflect.ValueOf(nil))) } +func TestFlatSlice(t *testing.T) { + sl := []any{ + []string{"a", "b"}, + } + fsl := flatSlice(reflect.ValueOf(sl), 1) + // dump.P(fsl.Interface()) + assert.Equal(t, 2, fsl.Len()) + assert.Equal(t, 2, fsl.Cap()) + + // make slice len=2, cap=3 + sub1 := make([]string, 0, 3) + sub1 = append(sub1, "a", "b") + + sl = []any{ + sub1, + } + fsl = flatSlice(reflect.ValueOf(sl), 1) + dump.P(fsl.Interface()) + assert.Equal(t, 2, fsl.Len()) + assert.Equal(t, 3, fsl.Cap()) + + sl = []any{ + []string{"a", "b"}, + sub1, + } + fsl = flatSlice(reflect.ValueOf(sl), 1) + // dump.P(fsl.Interface()) + assert.Equal(t, 4, fsl.Len()) + assert.Equal(t, 5, fsl.Cap()) + + // 3 level + sl = []any{ + []any{ + []string{"a", "b"}, + }, + } + + fsl = flatSlice(reflect.ValueOf(sl), 2) + dump.P(fsl.Interface()) + assert.Equal(t, 2, fsl.Len()) + assert.Equal(t, 2, fsl.Cap()) +} + func TestCallByValue(t *testing.T) { is := assert.New(t) is.Panics(func() { diff --git a/validating.go b/validating.go index dc65d82..a402e3a 100644 --- a/validating.go +++ b/validating.go @@ -244,11 +244,6 @@ func (r *Rule) valueValidate(field, name string, val any, v *Validation) (ok boo 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 { @@ -279,23 +274,21 @@ func (r *Rule) valueValidate(field, name string, val any, v *Validation) (ok boo // 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] - // } - - // feat: support check sub element in a slice list. eg: field=names.* - 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. + + // feat: support check sub element in a slice list. eg: field=top.user.*.name + dotStarNum := strings.Count(field, ".*") + if valKind == reflect.Slice && dotStarNum > 0 { + sliceLen, sliceCap := rftVal.Len(), rftVal.Cap() + + // if dotStarNum > 1, need flatten multi level slice with depth=dotStarNum. + if dotStarNum > 1 { + rftVal = flatSlice(rftVal, dotStarNum-1) + sliceLen, sliceCap = rftVal.Len(), rftVal.Cap() + } + + // check requiredXX validate - flatten multi level slice, count ".*" number. // TIP: if len < cap: not enough elements in the slice. use empty val call validator. - if !r.nameNotRequired && sliceLen < rftVal.Cap() { + if !r.nameNotRequired && sliceLen < sliceCap { return callValidator(v, fm, field, nil, r.arguments) } @@ -316,15 +309,6 @@ func (r *Rule) valueValidate(field, name string, val any, v *Validation) (ok boo subVal = subRv.Interface() } - // 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 @@ -334,7 +318,7 @@ func (r *Rule) valueValidate(field, name string, val any, v *Validation) (ok boo return true } - // 1.1 convert field value type, is func first argument. + // 1 convert field value type, is func first argument. if r.nameNotRequired && arg0Kind != reflect.Interface && arg0Kind != valKind { val, ok = convValAsFuncArg0Type(arg0Kind, valKind, val) if !ok { diff --git a/validating_test.go b/validating_test.go index 96ca57d..e7f1247 100644 --- a/validating_test.go +++ b/validating_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/gookit/goutil/dump" + "github.com/gookit/goutil/maputil" "github.com/gookit/goutil/testutil/assert" ) @@ -254,15 +255,15 @@ var nestedMap = map[string]any{ func TestRequired_AllItemsPassed(t *testing.T) { v := Map(nestedMap) v.StopOnError = false - // v.StringRule("coding.*.details", "required") + v.StringRule("coding.*.details", "required") v.StringRule("coding.*.details.em", "required") v.StringRule("coding.*.details.cpt.*.encounter_uid", "required") - v.StringRule("coding.*.details.cpt.*.work_item_uid", "required") + v.StringRule("coding.*.details.cpt.*.work_item_uid", "required|string") assert.True(t, v.Validate()) - // fmt.Println(v.Errors) + assert.Empty(t, v.Errors) } -func TestRequired_MissingField(t *testing.T) { +func TestRequired__map_subSlice_mDotStar(t *testing.T) { m := map[string]any{ "names": []string{"John", "Jane", "abc"}, "coding": []map[string]any{ @@ -302,17 +303,20 @@ func TestRequired_MissingField(t *testing.T) { }, } + dump.Println(maputil.GetByPath("coding.*.details.cpt.*.encounter_uid", m)) + dump.Println(maputil.GetByPath("coding.*.details.cpt.*.work_item_uid", m)) + v := Map(m) v.StopOnError = false // v.StringRule("coding.*.details", "required") // v.StringRule("coding.*.details.em", "required") v.StringRule("coding.*.details.cpt.*.encounter_uid", "required") - // v.StringRule("coding.*.details.cpt.*.work_item_uid", "required") - // assert.False(t, v.Validate()) // TODO ... fix this + v.StringRule("coding.*.details.cpt.*.work_item_uid", "required") + assert.False(t, v.Validate()) dump.Println(v.Errors) } -func TestValidate_sliceValue_1dotStar(t *testing.T) { +func TestValidate_map_subSlice_1dotStar(t *testing.T) { mp := map[string]any{ "top": map[string]any{ "cpt": []map[string]any{