diff --git a/maputil/get.go b/maputil/get.go index a856fbc47..5a4153bed 100644 --- a/maputil/get.go +++ b/maputil/get.go @@ -26,6 +26,21 @@ func QuietGet(mp map[string]any, path string) (val any) { return } +// GetFromAny get value by key path from any(map,slice) data. eg "top" "top.sub" +func GetFromAny(path string, data any) (val any, ok bool) { + // empty data + if data == nil { + return nil, false + } + + keys := strings.Split(path, ".") + if len(keys) == 0 { + return data, true + } + + return getByPathKeys(data, keys) +} + // GetByPath get value by key path from a map(map[string]any). eg "top" "top.sub" func GetByPath(path string, mp map[string]any) (val any, ok bool) { if val, ok := mp[path]; ok { @@ -60,14 +75,19 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { // find top item data use top key var item any - topK := keys[0] if item, ok = mp[topK]; !ok { return } // find sub item data use sub key - for i, k := range keys[1:] { + return getByPathKeys(item, keys[1:]) +} + +func getByPathKeys(item any, keys []string) (val any, ok bool) { + kl := len(keys) + + for i, k := range keys { switch tData := item.(type) { case map[string]string: // is string map if item, ok = tData[k]; !ok { @@ -83,14 +103,14 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { } case []map[string]any: // is an any-map slice if k == Wildcard { - if kl == i+2 { // * is last key + if kl == i+1 { // * is last key return tData, true } // * is not last key, find sub item data - sl := make([]any, 0) + sl := make([]any, 0, len(tData)) for _, v := range tData { - if val, ok = GetByPathKeys(v, keys[i+2:]); ok { + if val, ok = getByPathKeys(v, keys[i+1:]); ok { sl = append(sl, val) } } @@ -108,7 +128,7 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { } item = tData[idx] default: - if k == Wildcard && kl == i+2 { // * is last key + if k == Wildcard && kl == i+1 { // * is last key return tData, true } @@ -117,7 +137,7 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { if rv.Kind() == reflect.Slice { if k == Wildcard { // * is not last key, find sub item data - sl := make([]any, 0) + sl := make([]any, 0, rv.Len()) for si := 0; si < rv.Len(); si++ { el := reflects.Indirect(rv.Index(si)) if el.Kind() != reflect.Map { @@ -125,7 +145,7 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { } // el is map value. - if val, ok = GetByPathKeys(ToAnyMap(el.Interface()), keys[i+2:]); ok { + if val, ok = getByPathKeys(el.Interface(), keys[i+1:]); ok { sl = append(sl, val) } } @@ -149,6 +169,11 @@ func GetByPathKeys(mp map[string]any, keys []string) (val any, ok bool) { // as error return nil, false } + + // next is last key and it is * + if kl == i+2 && keys[i+1] == Wildcard { + return item, true + } } return item, true diff --git a/maputil/get_test.go b/maputil/get_test.go index e279b5844..2eaa477b9 100644 --- a/maputil/get_test.go +++ b/maputil/get_test.go @@ -69,9 +69,9 @@ func TestGetByPath(t *testing.T) { assert.Eq(t, tt.want, v, tt.path) } - // v, ok := maputil.GetByPath("mlMp.*.names.1", mp) - // assert.True(t, ok) - // assert.Eq(t, []any{"abc", "def"}, v) + v, ok := maputil.GetByPath("mlMp.*.names.1", mp) + assert.True(t, ok) + assert.Eq(t, []any{"abc", "def"}, v) } var mlMp = map[string]any{ @@ -104,10 +104,10 @@ var mlMp = map[string]any{ // "resident_provider": "Test Resident Provider", }, { - "code": "OBS01", - "encounter_uid": "3", - "work_item_uid": "4", - "billing_provider": "Test provider OBS01", + "code": "OBS01", + "encounter_uid": "3", + "work_item_uid": "4", + // "billing_provider": "Test provider OBS01", "resident_provider": "Test Resident Provider", }, { @@ -124,20 +124,32 @@ var mlMp = map[string]any{ } func TestGetByPath_deepPath(t *testing.T) { - val, ok := maputil.GetByPath("coding.0.details.em.code", mlMp) - assert.True(t, ok) - assert.NotEmpty(t, val) + t.Run("direct multi level key", func(t *testing.T) { + val, ok := maputil.GetByPath("coding.0.details.em.code", mlMp) + assert.True(t, ok) + assert.NotEmpty(t, val) + }) - val, ok = maputil.GetByPath("coding.*.details", mlMp) - assert.True(t, ok) - assert.NotEmpty(t, val) - // dump.P(ok, val) + t.Run("dot star 2-level", func(t *testing.T) { + val, ok := maputil.GetByPath("coding.*.details", mlMp) + assert.True(t, ok) + assert.NotEmpty(t, val) + // dump.P(ok, val) + }) - val, ok = maputil.GetByPath("coding.*.details.em", mlMp) - dump.P(ok, val) - assert.True(t, ok) + t.Run("dot star 3-level", func(t *testing.T) { + val, ok := maputil.GetByPath("coding.*.details.em", mlMp) + dump.P(ok, val) + assert.True(t, ok) + }) + + t.Run("last is dot star", func(t *testing.T) { + val, ok := maputil.GetByPath("coding.*.details.em.*", mlMp) + dump.P(ok, val) + assert.True(t, ok) + }) - val, ok = maputil.GetByPath("coding.*.details.em.code", mlMp) + val, ok := maputil.GetByPath("coding.*.details.em.code", mlMp) dump.P(ok, val) assert.True(t, ok) assert.IsType(t, []any{}, val) @@ -148,10 +160,12 @@ func TestGetByPath_deepPath(t *testing.T) { assert.Len(t, val, 1) assert.IsType(t, []any{}, val) - val, ok = maputil.GetByPath("coding.*.details.cpt.*.work_item_uid", mlMp) - // dump.P(ok, val) - assert.True(t, ok) - assert.IsType(t, []any{}, val) + t.Run("missing a field", func(t *testing.T) { + val, ok = maputil.GetByPath("coding.*.details.cpt.*.billing_provider", mlMp) + dump.P(ok, val) + assert.True(t, ok) + assert.IsType(t, []any{}, val) + }) val, ok = maputil.GetByPath("coding.*.details.cpt.*.resident_provider", mlMp) // dump.P(ok, val) @@ -163,6 +177,28 @@ func TestGetByPath_deepPath(t *testing.T) { assert.False(t, ok) } +func TestGetFromAny_sliceSubValue(t *testing.T) { + val, ok := maputil.GetByPath("coding.*.details.cpt", mlMp) + assert.True(t, ok) + assert.IsKind(t, reflect.Slice, val) + dump.P(val) + + // get sub value in slice + for _, sl := range val.([]any) { + val, ok = maputil.GetFromAny("*.code", sl) + assert.True(t, ok) + dump.P(val) + } + + val, ok = maputil.GetFromAny("", map[string]any{"a": "b"}) + assert.True(t, ok) + assert.NotEmpty(t, val) + + val, ok = maputil.GetFromAny("a", nil) + assert.False(t, ok) + assert.Nil(t, val) +} + func TestKeys(t *testing.T) { mp := map[string]any{ "key0": "v0",