Skip to content

Commit

Permalink
Add safemapstr.Put (#6434)
Browse files Browse the repository at this point in the history
* Add `safemapstr` to provide `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{}
  a.Put("com.docker.swarm.task", "x")
  a.Put("com.docker.swarm.task.id", 1)
  a.Put("com.docker.swarm.task.name", "foobar")
```

Will result in `{"com":{"docker":{"swarm":{"task":"x"}}}}`

DeDotPut detects this scenario and renames the common base key, by appending
`.value`:

```
 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"}}}}}`

The plan is to use this method to convert all unstructured external data, like labels, annotations and so

Related to #6421

* Apply some review comments

* Use tryToMapStr when checking leaf node
  • Loading branch information
exekias authored and andrewkroh committed Mar 12, 2018
1 parent 6bec4b1 commit bb6d3d8
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
1 change: 1 addition & 0 deletions libbeat/common/mapstr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}

Expand Down
69 changes: 69 additions & 0 deletions libbeat/common/safemapstr/safemapstr.go
Original file line number Diff line number Diff line change
@@ -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
}
}
65 changes: 65 additions & 0 deletions libbeat/common/safemapstr/safemapstr_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit bb6d3d8

Please sign in to comment.