Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [maps.Flatten] better handle keys with delimiter character via escape sequence #328

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 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,28 @@ 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 {
if delim == "" {
return keyPairs
}
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 +115,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
Loading