Skip to content

Commit

Permalink
fix: [maps.Flatten] better handle keys with delimiter character via e…
Browse files Browse the repository at this point in the history
…scape sequence
  • Loading branch information
crandles committed Sep 30, 2024
1 parent ec1d17c commit b61ea9b
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 15 deletions.
39 changes: 36 additions & 3 deletions maps/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import (
"github.com/mitchellh/copystructure"
)

const (
escapeChar = "~"
tildeEscape = escapeChar + "0"
delimEscape = escapeChar + "1"
)

// Flatten takes a map[string]interface{} and traverses it and flattens
// nested children into keys delimited by delim.
//
Expand All @@ -33,6 +39,21 @@ func Flatten(m map[string]interface{}, keys []string, delim string) (map[string]
return out, keyMap
}

func escape(keyPairs []string, delim string) []string {
if delim == "" {
return keyPairs
}
var result []string
for _, kp := range keyPairs {
// first pass, escape all escape characters
out := strings.ReplaceAll(kp, escapeChar, tildeEscape)
// second pass, escape the delimiter
out = strings.Replace(out, delim, delimEscape, -1)
result = append(result, out)
}
return result
}

func flatten(m map[string]interface{}, keys []string, delim string, out map[string]interface{}, keyMap map[string][]string) {
for key, val := range m {
// Copy the incoming key paths into a fresh list
Expand All @@ -45,7 +66,7 @@ func flatten(m map[string]interface{}, keys []string, delim string, out map[stri
case map[string]interface{}:
// Empty map.
if len(cur) == 0 {
newKey := strings.Join(kp, delim)
newKey := strings.Join(escape(kp, delim), delim)
out[newKey] = val
keyMap[newKey] = kp
continue
Expand All @@ -54,13 +75,25 @@ func flatten(m map[string]interface{}, keys []string, delim string, out map[stri
// It's a nested map. Flatten it recursively.
flatten(cur, kp, delim, out, keyMap)
default:
newKey := strings.Join(kp, delim)
newKey := strings.Join(escape(kp, delim), delim)
out[newKey] = val
keyMap[newKey] = kp
}
}
}

func unescape(keyPairs []string, delim string) []string {
var result []string
for _, kp := range keyPairs {
// first pass, unescape the delimiter
out := strings.Replace(kp, delimEscape, delim, -1)
// second pass, unescape all escape characters
out = strings.Replace(out, tildeEscape, escapeChar, -1)
result = append(result, out)
}
return result
}

// Unflatten takes a flattened key:value map (non-nested with delimited keys)
// and returns a nested map where the keys are split into hierarchies by the given
// delimiter. For instance, `parent.child.key: 1` to `{parent: {child: {key: 1}}}`
Expand All @@ -79,7 +112,7 @@ func Unflatten(m map[string]interface{}, delim string) map[string]interface{} {
)

if delim != "" {
keys = strings.Split(k, delim)
keys = unescape(strings.Split(k, delim), delim)
} else {
keys = []string{k}
}
Expand Down
7 changes: 5 additions & 2 deletions providers/nats/nats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ func TestNats(t *testing.T) {
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: "test",
})
if err != nil {
t.Fatal(err)
}
_, err = kv.Put("some.test.color", []byte("blue"))
if err != nil {
t.Fatal(err)
Expand All @@ -46,8 +49,8 @@ func TestNats(t *testing.T) {
t.Fatal(err)
}

assert.Equal(t, k.Keys(), []string{"some.test.color"})
assert.Equal(t, k.Get("some.test.color"), "blue")
assert.Equal(t, []string{"some.test.color"}, k.Keys())
assert.Equal(t, "blue", k.Get("some.test.color"))

err = provider.Watch(func(event interface{}, err error) {
if err != nil {
Expand Down
20 changes: 10 additions & 10 deletions tests/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,16 @@ var testMap3 = map[string]interface{}{
func TestFlatten(t *testing.T) {
f, k := maps.Flatten(testMap, nil, delim)
assert.Equal(t, map[string]interface{}{
"parent.child.key": 123,
"parent.child.key.with.dot": 456,
"top": 789,
"empty": map[string]interface{}{},
"parent.child.key": 123,
"parent.child.key~1with~1dot": 456,
"top": 789,
"empty": map[string]interface{}{},
}, f)
assert.Equal(t, map[string][]string{
"parent.child.key": {"parent", "child", "key"},
"parent.child.key.with.dot": {"parent", "child", "key.with.dot"},
"top": {"top"},
"empty": {"empty"},
"parent.child.key": {"parent", "child", "key"},
"parent.child.key~1with~1dot": {"parent", "child", "key.with.dot"},
"top": {"top"},
"empty": {"empty"},
}, k)
}

Expand All @@ -125,11 +125,11 @@ func BenchmarkFlatten(b *testing.B) {
func TestUnflatten(t *testing.T) {
m, _ := maps.Flatten(testMap, nil, delim)
um := maps.Unflatten(m, delim)
assert.NotEqual(t, um, testMap)
assert.Equal(t, testMap, um)

m, _ = maps.Flatten(testMap2, nil, delim)
um = maps.Unflatten(m, delim)
assert.Equal(t, um, testMap2)
assert.Equal(t, testMap2, um)
}

func TestIntfaceKeysToStrings(t *testing.T) {
Expand Down

0 comments on commit b61ea9b

Please sign in to comment.