diff --git a/maputil/get.go b/maputil/get.go index 8c6c9c2db..c2236c47a 100644 --- a/maputil/get.go +++ b/maputil/get.go @@ -6,6 +6,12 @@ import ( "strings" ) +// some consts for separators +const ( + Wildcard = "*" + PathSep = "." +) + // DeepGet value by key path. eg "top" "top.sub" func DeepGet(mp map[string]any, path string) (val any) { val, _ = GetByPath(path, mp) @@ -25,48 +31,96 @@ func GetByPath(path string, mp map[string]any) (val any, ok bool) { } // no sub key - if len(mp) == 0 || !strings.ContainsRune(path, '.') { + if len(mp) == 0 || strings.IndexByte(path, '.') < 1 { return nil, false } // has sub key. eg. "top.sub" keys := strings.Split(path, ".") - topK := keys[0] + return GetByPathKeys(mp, keys) +} + +// GetByPathKeys get value by path keys from a map(map[string]any). eg "top" "top.sub" +// +// Example: +// +// mp := map[string]any{ +// "top": map[string]any{ +// "sub": "value", +// }, +// } +// val, ok := GetByPathKeys(mp, []string{"top", "sub"}) // return "value", true +func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { + kl := len(keys) + if kl == 0 { + return mp, true + } // find top item data use top key var item any + + topK := keys[0] if item, ok = mp[topK]; !ok { return } - for _, k := range keys[1:] { + // find sub item data use sub key + for i, k := range keys[1:] { switch tData := item.(type) { - case map[string]string: // is simple map + case map[string]string: // is string map if item, ok = tData[k]; !ok { return } - case map[string]any: // is map(decode from toml/json) + case map[string]any: // is map(decode from toml/json/yaml) if item, ok = tData[k]; !ok { return } - case map[any]any: // is map(decode from yaml) + case map[any]any: // is map(decode from yaml.v2) if item, ok = tData[k]; !ok { return } - case []any: // is a slice - if item, ok = getBySlice(k, tData); !ok { - return + case []map[string]any: // is an any-map slice + if k == Wildcard { + if kl == i+2 { + return tData, true + } + + sl := make([]any, 0, len(tData)) + for _, v := range tData { + if val, ok = GetByPathKeys(v, keys[i+2:]); ok { + sl = append(sl, val) + } + } + return sl, true + } + + // k is index number + idx, err := strconv.Atoi(k) + if err != nil { + return nil, false } - case []string, []int, []float32, []float64, []bool, []rune: - slice := reflect.ValueOf(tData) - sData := make([]any, slice.Len()) - for i := 0; i < slice.Len(); i++ { - sData[i] = slice.Index(i).Interface() + + if idx >= len(tData) { + return nil, false } - if item, ok = getBySlice(k, sData); !ok { - return + item = tData[idx] + default: + rv := reflect.ValueOf(tData) + // check is slice + if rv.Kind() == reflect.Slice { + i, err := strconv.Atoi(k) + if err != nil { + return nil, false + } + if i >= rv.Len() { + return nil, false + } + + item = rv.Index(i).Interface() + continue } - default: // error + + // as error return nil, false } } @@ -74,17 +128,6 @@ func GetByPath(path string, mp map[string]any) (val any, ok bool) { return item, true } -func getBySlice(k string, slice []any) (val any, ok bool) { - i, err := strconv.ParseInt(k, 10, 64) - if err != nil { - return nil, false - } - if size := int64(len(slice)); i >= size { - return nil, false - } - return slice[i], true -} - // Keys get all keys of the given map. func Keys(mp any) (keys []string) { rftVal := reflect.Indirect(reflect.ValueOf(mp)) diff --git a/maputil/get_test.go b/maputil/get_test.go index f5551a495..67e304d49 100644 --- a/maputil/get_test.go +++ b/maputil/get_test.go @@ -3,6 +3,7 @@ package maputil_test import ( "testing" + "github.com/gookit/goutil/dump" "github.com/gookit/goutil/maputil" "github.com/gookit/goutil/testutil/assert" ) @@ -15,68 +16,135 @@ func TestGetByPath(t *testing.T) { "key3": map[string]any{"sk1": "sv1"}, "key4": []int{1, 2}, "key5": []any{1, "2", true}, + "mlMp": []map[string]any{ + { + "code": "001", + "names": []string{"John", "abc"}, + }, + { + "code": "002", + "names": []string{"Tom", "def"}, + }, + }, } - v, ok := maputil.GetByPath("key0", mp) - assert.True(t, ok) - assert.Eq(t, "val0", v) + tests := []struct { + path string + want any + ok bool + }{ + {"key0", "val0", true}, + {"key1.sk0", "sv0", true}, + {"key3.sk1", "sv1", true}, + // not exists + {"not-exits", nil, false}, + {"key2.not-exits", nil, false}, + {"not-exits.subkey", nil, false}, + // slices behaviour + {"key2", mp["key2"], true}, + {"key2.0", "sv1", true}, + {"key2.1", "sv2", true}, + {"key4.0", 1, true}, + {"key4.1", 2, true}, + {"key5.0", 1, true}, + {"key5.1", "2", true}, + {"key5.2", true, true}, + // out of bound + {"key4.3", nil, false}, + // deep sub map + {"mlMp.*.code", []any{"001", "002"}, true}, + {"mlMp.*.names", []any{ + []string{"John", "abc"}, + []string{"Tom", "def"}, + }, true}, + {"mlMp.*.names.1", []any{"abc", "def"}, true}, + } - v, ok = maputil.GetByPath("key1.sk0", mp) - assert.True(t, ok) - assert.Eq(t, "sv0", v) + for _, tt := range tests { + v, ok := maputil.GetByPath(tt.path, mp) + assert.Eq(t, tt.ok, ok, tt.path) + assert.Eq(t, tt.want, v, tt.path) + } - v, ok = maputil.GetByPath("key3.sk1", mp) - assert.True(t, ok) - assert.Eq(t, "sv1", v) - - // not exists - v, ok = maputil.GetByPath("not-exits", mp) - assert.False(t, ok) - assert.Nil(t, v) - v, ok = maputil.GetByPath("key2.not-exits", mp) - assert.False(t, ok) - assert.Nil(t, v) - v, ok = maputil.GetByPath("not-exits.subkey", mp) - assert.False(t, ok) - assert.Nil(t, v) - - // Slices behaviour - v, ok = maputil.GetByPath("key2", mp) - assert.True(t, ok) - assert.Eq(t, mp["key2"], v) + // v, ok := maputil.GetByPath("mlMp.*.names.1", mp) + // assert.True(t, ok) + // assert.Eq(t, []any{"abc", "def"}, v) +} - v, ok = maputil.GetByPath("key2.0", mp) - assert.True(t, ok) - assert.Eq(t, "sv1", v) +var mlMp = map[string]any{ + "names": []string{"John", "Jane", "abc"}, + "coding": []map[string]any{ + { + "details": map[string]any{ + "em": map[string]any{ + "code": "001-1", + "encounter_uid": "1-1", + "billing_provider": "Test provider 01-1", + "resident_provider": "Test Resident Provider-1", + }, + }, + }, + { + "details": map[string]any{ + "em": map[string]any{ + "code": "001", + "encounter_uid": "1", + "billing_provider": "Test provider 01", + "resident_provider": "Test Resident Provider", + }, + "cpt": []map[string]any{ + { + "code": "001", + "encounter_uid": "2", + "work_item_uid": "3", + "billing_provider": "Test provider 001", + "resident_provider": "Test Resident Provider", + }, + { + "code": "OBS01", + "encounter_uid": "3", + "work_item_uid": "4", + "billing_provider": "Test provider OBS01", + "resident_provider": "Test Resident Provider", + }, + { + "code": "SU002", + "encounter_uid": "5", + "work_item_uid": "6", + "billing_provider": "Test provider SU002", + "resident_provider": "Test Resident Provider", + }, + }, + }, + }, + }, +} - v, ok = maputil.GetByPath("key2.1", mp) +func TestGetByPath_deepPath(t *testing.T) { + val, ok := maputil.GetByPath("coding.0.details.em.code", mlMp) assert.True(t, ok) - assert.Eq(t, "sv2", v) + assert.NotEmpty(t, val) - v, ok = maputil.GetByPath("key4.0", mp) + val, ok = maputil.GetByPath("coding.*.details", mlMp) assert.True(t, ok) - assert.Eq(t, 1, v) + assert.NotEmpty(t, val) + // dump.P(ok, val) - v, ok = maputil.GetByPath("key4.1", mp) + val, ok = maputil.GetByPath("coding.*.details.em", mlMp) + dump.P(ok, val) assert.True(t, ok) - assert.Eq(t, 2, v) - v, ok = maputil.GetByPath("key5.0", mp) + val, ok = maputil.GetByPath("coding.*.details.em.code", mlMp) + dump.P(ok, val) assert.True(t, ok) - assert.Eq(t, 1, v) - v, ok = maputil.GetByPath("key5.1", mp) + val, ok = maputil.GetByPath("coding.*.details.cpt.*.encounter_uid", mlMp) + dump.P(ok, val) assert.True(t, ok) - assert.Eq(t, "2", v) - v, ok = maputil.GetByPath("key5.2", mp) + val, ok = maputil.GetByPath("coding.*.details.cpt.*.work_item_uid", mlMp) + dump.P(ok, val) assert.True(t, ok) - assert.Eq(t, true, v) - - // Out of bound value - v, ok = maputil.GetByPath("key2.2", mp) - assert.False(t, ok) - assert.Nil(t, v) } func TestKeys(t *testing.T) { diff --git a/testutil/assert/asserts.go b/testutil/assert/asserts.go index 5354fbaf1..741167ead 100644 --- a/testutil/assert/asserts.go +++ b/testutil/assert/asserts.go @@ -313,7 +313,20 @@ func StrContains(t TestingT, s, sub string, fmtAndArgs ...any) bool { t.Helper() return fail(t, - fmt.Sprintf("String value check fail:\nGiven string: %#v\nNot contains: %#v", s, sub), + fmt.Sprintf("String check fail:\nGiven string: %#v\nNot contains: %#v", s, sub), + fmtAndArgs, + ) +} + +// StrCount asserts that the given strings is contains sub-string and count +func StrCount(t TestingT, s, sub string, count int, fmtAndArgs ...any) bool { + if strings.Count(s, sub) == count { + return true + } + + t.Helper() + return fail(t, + fmt.Sprintf("String check fail:\nGiven string: %s\nNot contains %q count: %d", s, sub, count), fmtAndArgs, ) }