diff --git a/libbeat/common/mapstr.go b/libbeat/common/mapstr.go index b84b3e57b2a..93404cdfe98 100644 --- a/libbeat/common/mapstr.go +++ b/libbeat/common/mapstr.go @@ -137,6 +137,7 @@ func (m MapStr) GetValue(key string) (interface{}, error) { // If you need insert keys containing dots then you must use bracket notation // to insert values (e.g. m[key] = value). func (m MapStr) Put(key string, value interface{}) (interface{}, error) { + // XXX `safemapstr.Put` mimics this implementation, both should be updated to have similar behavior return walkMap(key, m, mapStrOperation{putOperation{value}, true}) } diff --git a/libbeat/common/safemapstr/safemapstr.go b/libbeat/common/safemapstr/safemapstr.go new file mode 100644 index 00000000000..d81aa7b16c1 --- /dev/null +++ b/libbeat/common/safemapstr/safemapstr.go @@ -0,0 +1,69 @@ +package safemapstr + +import ( + "strings" + + "github.com/elastic/beats/libbeat/common" +) + +const alternativeKey = "value" + +// 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 { + // XXX This implementation mimics `common.MapStr.Put`, both should be updated to have similar behavior + keyParts := strings.SplitN(key, ".", 2) + + // If leaf node or key exists directly + if len(keyParts) == 1 { + oldValue, exists := data[key] + if exists { + oldMap, ok := tryToMapStr(oldValue) + if ok { + // This would replace a whole object, change its key to avoid that: + oldMap[alternativeKey] = 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{alternativeKey: 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) +}