Skip to content

Instantly share code, notes, and snippets.

@maplebed
Created January 25, 2019 22:13
Show Gist options
  • Save maplebed/28132e95a42caf557f52438e81d58fcd to your computer and use it in GitHub Desktop.
Save maplebed/28132e95a42caf557f52438e81d58fcd to your computer and use it in GitHub Desktop.
turns deep json objects into flat objects with dotted keys
// unpackDeepEvent takes a parsed body (straight from json) and returns the
// unmodified original or a copy with any nested maps turned into dotted keys.
// (eg `"headers": {"a":5,"b":3}` turns in to `"headers.a":5,"headers.b":3`).
// Additionally returns a flag saying whether nested json was present, and an
// int indicating the depth actually unpacked.
func unpackDeepEvent(raw map[string]interface{}, maxDepth int) (map[string]interface{}, bool, int) {
unpacked := make(map[string]interface{}, len(raw))
nestedKeys := make(map[string]struct{})
var actualDepth int
for k, v := range raw {
switch v := v.(type) {
case map[string]interface{}:
nestedKeys[k] = struct{}{}
if newDepth := dotifyMap(k, unpacked, v, maxDepth); newDepth > actualDepth {
actualDepth = newDepth
}
default:
}
}
// only use unpacked and make the copy if there is nested json
if len(unpacked) != 0 {
for k, v := range raw {
if _, found := nestedKeys[k]; !found {
unpacked[k] = v
}
}
return unpacked, true, actualDepth
}
return raw, false, 0
}
// dotifyMap mutates top, adding all elements of inner to it by using
// topkey+innerkey as the key for top. topkey should not be empty. Will only go
// the configured number of levels deep into a map before refusing to unpack
// anymore. Returns the actual depth it went before returning.
func dotifyMap(topkey string, top, inner map[string]interface{}, maxDepth int) int {
// only go max curDepth before just including the nested json as a thing (that
// will get stringified later)
if maxDepth <= 0 {
top[topkey] = inner
return 0
}
depth := 0
for k, v := range inner {
newkey := topkey + "." + k
switch v := v.(type) {
case map[string]interface{}:
if newDepth := dotifyMap(newkey, top, v, maxDepth-1); newDepth > depth {
depth = newDepth
}
default:
top[newkey] = v
}
}
return depth + 1
}
func TestUnpackDeepEvent(t *testing.T) {
testObj := map[string]interface{}{
"one": 1,
"two": "tttoooowwwwoooo",
"deeptop": map[string]interface{}{
"subone": "one",
},
"deeplist": map[string]interface{}{
"sublist": []string{"foo", "bar"},
},
"deeper": map[string]interface{}{
"subfield": 3.145,
"subdeep": map[string]interface{}{
"subdeeper": "two",
},
},
"deepest": map[string]interface{}{
"subdeep": map[string]interface{}{
"subdeeper": map[string]interface{}{
"subdeepest": "three",
},
},
},
"slice": []string{"one", "two", "three"},
}
expectedRes := map[string]interface{}{
"one": 1,
"two": "tttoooowwwwoooo",
"deeptop.subone": "one",
"deeplist.sublist": []string{"foo", "bar"},
"deeper.subfield": 3.145,
"deeper.subdeep.subdeeper": "two",
// should only unpack 3 levels deep
"deepest.subdeep.subdeeper": map[string]interface{}{
"subdeepest": "three",
},
"slice": []string{"one", "two", "three"},
}
res, triggered, depth := unpackDeepEvent(testObj, 2)
test.Equals(t, res, expectedRes)
test.Equals(t, triggered, true)
test.Equals(t, depth, 2)
// if we don't restrict the depth, it should go 3 deep
res, triggered, depth = unpackDeepEvent(testObj, 5)
test.Equals(t, depth, 3)
// verify the testObj was not mutated
test.Equals(t, len(testObj["deeptop"].(map[string]interface{})), 1)
test.Equals(t, len(testObj), 7)
// verify non-nested json returns false, returned value matches input
noNest := map[string]interface{}{"one": 1, "two": "baz"}
res, triggered, depth = unpackDeepEvent(noNest, 3)
test.Equals(t, res, noNest)
test.Equals(t, triggered, false)
test.Equals(t, depth, 0)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment