diff --git a/libbeat/common/safemapstr/safemapstr.go b/libbeat/common/safemapstr/safemapstr.go new file mode 100644 index 00000000000..22e631c4bea --- /dev/null +++ b/libbeat/common/safemapstr/safemapstr.go @@ -0,0 +1,66 @@ +package safemapstr + +import ( + "strings" + + "github.com/elastic/beats/libbeat/common" +) + +// Put This method implements a way to put dotted keys into a MapStr while +// ensuring they don't override each other. For example: +// +// a := MapStr{} +// safemapstr.Put(a, "com.docker.swarm.task", "x") +// safemapstr.Put(a, "com.docker.swarm.task.id", 1) +// safemapstr.Put(a, "com.docker.swarm.task.name", "foobar") +// +// Will result in `{"com":{"docker":{"swarm":{"task":{"id":1,"name":"foobar","value":"x"}}}}}` +// +// Put detects this scenario and renames the common base key, by appending +// `.value` +func Put(data common.MapStr, key string, value interface{}) error { + keyParts := strings.SplitN(key, ".", 2) + + // If leaf node or key exists directly + if len(keyParts) == 1 { + oldValue, exists := data[key] + if exists { + switch oldValue.(type) { + case common.MapStr: + // This would replace a whole object, change its key to avoid that: + oldValue.(common.MapStr)["value"] = value + return nil + } + } + data[key] = value + return nil + } + + // Checks if first part of the key exists + k := keyParts[0] + d, exists := data[k] + if !exists { + d = common.MapStr{} + data[k] = d + } + + v, ok := tryToMapStr(d) + if !ok { + // This would replace a leaf with an object, change its key to avoid that: + v = common.MapStr{"value": d} + data[k] = v + } + + return Put(v, keyParts[1], value) +} + +func tryToMapStr(v interface{}) (common.MapStr, bool) { + switch m := v.(type) { + case common.MapStr: + return m, true + case map[string]interface{}: + return common.MapStr(m), true + default: + return nil, false + } +} diff --git a/libbeat/common/safemapstr/safemapstr_test.go b/libbeat/common/safemapstr/safemapstr_test.go new file mode 100644 index 00000000000..e7279cd981c --- /dev/null +++ b/libbeat/common/safemapstr/safemapstr_test.go @@ -0,0 +1,65 @@ +package safemapstr + +import ( + "testing" + + "github.com/elastic/beats/libbeat/common" + + "github.com/stretchr/testify/assert" +) + +func TestPut(t *testing.T) { + m := common.MapStr{ + "subMap": common.MapStr{ + "a": 1, + }, + } + + // Add new value to the top-level. + err := Put(m, "a", "ok") + assert.NoError(t, err) + assert.Equal(t, common.MapStr{"a": "ok", "subMap": common.MapStr{"a": 1}}, m) + + // Add new value to subMap. + err = Put(m, "subMap.b", 2) + assert.NoError(t, err) + assert.Equal(t, common.MapStr{"a": "ok", "subMap": common.MapStr{"a": 1, "b": 2}}, m) + + // Overwrite a value in subMap. + err = Put(m, "subMap.a", 2) + assert.NoError(t, err) + assert.Equal(t, common.MapStr{"a": "ok", "subMap": common.MapStr{"a": 2, "b": 2}}, m) + + // Add value to map that does not exist. + m = common.MapStr{} + err = Put(m, "subMap.newMap.a", 1) + assert.NoError(t, err) + assert.Equal(t, common.MapStr{"subMap": common.MapStr{"newMap": common.MapStr{"a": 1}}}, m) +} + +func TestPutRenames(t *testing.T) { + assert := assert.New(t) + + a := common.MapStr{} + Put(a, "com.docker.swarm.task", "x") + Put(a, "com.docker.swarm.task.id", 1) + Put(a, "com.docker.swarm.task.name", "foobar") + assert.Equal(common.MapStr{"com": common.MapStr{"docker": common.MapStr{"swarm": common.MapStr{ + "task": common.MapStr{ + "id": 1, + "name": "foobar", + "value": "x", + }}}}}, a) + + // order is not important: + b := common.MapStr{} + Put(b, "com.docker.swarm.task.id", 1) + Put(b, "com.docker.swarm.task.name", "foobar") + Put(b, "com.docker.swarm.task", "x") + assert.Equal(common.MapStr{"com": common.MapStr{"docker": common.MapStr{"swarm": common.MapStr{ + "task": common.MapStr{ + "id": 1, + "name": "foobar", + "value": "x", + }}}}}, b) +}