Skip to content

Commit

Permalink
👔 up: enhanced support multi level slice item value check. see issues #…
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Jul 31, 2023
1 parent 025951a commit 0e59d42
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .github/changelog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:']
Expand Down
43 changes: 43 additions & 0 deletions issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
}
3 changes: 1 addition & 2 deletions messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
43 changes: 42 additions & 1 deletion util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
46 changes: 15 additions & 31 deletions validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand All @@ -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 {
Expand Down
18 changes: 11 additions & 7 deletions validating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/gookit/goutil/dump"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/testutil/assert"
)

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down

0 comments on commit 0e59d42

Please sign in to comment.