Skip to content

Commit

Permalink
✨ feat: maputil - enhance the func GetByPath() support like top.*.fie…
Browse files Browse the repository at this point in the history
…ld match paths
  • Loading branch information
inhere committed Jun 11, 2023
1 parent 1d52b3e commit e025933
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 74 deletions.
99 changes: 71 additions & 28 deletions maputil/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -25,66 +31,103 @@ 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
}
}

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))
Expand Down
158 changes: 113 additions & 45 deletions maputil/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down
15 changes: 14 additions & 1 deletion testutil/assert/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
}
Expand Down

0 comments on commit e025933

Please sign in to comment.