internal/common/mapstr.go (151 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package common
// WARNING: This code is copied from https://github.com/elastic/beats/blob/master/libbeat/common/mapstr.go
// This was done to not have to import the full common package and all its dependencies
// Not needed methods / variables were removed, but no changes made to the logic.
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
var (
// ErrKeyNotFound indicates that the specified key was not found.
ErrKeyNotFound = errors.New("key not found")
)
// MapStr is a map[string]interface{} wrapper with utility methods for common
// map operations like converting to JSON.
type MapStr map[string]interface{}
// GetValue gets a value from the map. If the key does not exist then an error
// is returned.
func (m MapStr) GetValue(key string) (interface{}, error) {
_, _, v, found, err := mapFind(key, m, false)
if err != nil {
return nil, err
}
if !found {
return nil, ErrKeyNotFound
}
return v, nil
}
// Put associates the specified value with the specified key. If the map
// previously contained a mapping for the key, the old value is replaced and
// returned. The key can be expressed in dot-notation (e.g. x.y) to put a value
// into a nested map.
//
// 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
k, d, old, _, err := mapFind(key, m, true)
if err != nil {
return nil, err
}
d[k] = value
return old, nil
}
// DeepUpdate recursively copies the key-value pairs from d to this map.
// If the key is present and a map as well, the sub-map will be updated recursively
// via DeepUpdate.
// DeepUpdateNoOverwrite is a version of this function that does not
// overwrite existing values.
func (m MapStr) DeepUpdate(d MapStr) {
m.deepUpdateMap(d, true)
}
// DeepUpdateNoOverwrite recursively copies the key-value pairs from d to this map.
// If a key is already present it will not be overwritten.
// DeepUpdate is a version of this function that overwrites existing values.
func (m MapStr) DeepUpdateNoOverwrite(d MapStr) {
m.deepUpdateMap(d, false)
}
func (m MapStr) deepUpdateMap(d MapStr, overwrite bool) {
for k, v := range d {
switch val := v.(type) {
case map[string]interface{}:
m[k] = deepUpdateValue(m[k], MapStr(val), overwrite)
case MapStr:
m[k] = deepUpdateValue(m[k], val, overwrite)
default:
if overwrite {
m[k] = v
} else if _, exists := m[k]; !exists {
m[k] = v
}
}
}
}
func deepUpdateValue(old interface{}, val MapStr, overwrite bool) interface{} {
switch sub := old.(type) {
case MapStr:
if sub == nil {
return val
}
sub.deepUpdateMap(val, overwrite)
return sub
case map[string]interface{}:
if sub == nil {
return val
}
tmp := MapStr(sub)
tmp.deepUpdateMap(val, overwrite)
return tmp
default:
// We reach the default branch if old is no map or if old == nil.
// In either case we return `val`, such that the old value is completely
// replaced when merging.
return val
}
}
// Delete deletes the given key from the map.
func (m MapStr) Delete(key string) error {
k, d, _, found, err := mapFind(key, m, false)
if err != nil {
return err
}
if !found {
return ErrKeyNotFound
}
delete(d, k)
return nil
}
// StringToPrint returns the MapStr as pretty JSON.
func (m MapStr) StringToPrint() string {
j, err := json.MarshalIndent(m, "", " ")
if err != nil {
return fmt.Sprintf("Not valid json: %v", err)
}
return string(j)
}
// ToMapStrSlice function tries to convert the interface into the slice of MapStrs.
func ToMapStrSlice(slice interface{}) ([]MapStr, error) {
sliceI, ok := slice.([]interface{})
if !ok {
return nil, fmt.Errorf("expected slice of interfaces but type is %T", slice)
}
var mapStrs []MapStr
for _, v := range sliceI {
m, err := toMapStr(v)
if err != nil {
return nil, fmt.Errorf("can't convert element to MapStr: %w", err)
}
mapStrs = append(mapStrs, m)
}
return mapStrs, nil
}
// toMapStr performs a type assertion on v and returns a MapStr. v can be either
// a MapStr or a map[string]interface{}. If it's any other type or nil then
// an error is returned.
func toMapStr(v interface{}) (MapStr, error) {
m, ok := tryToMapStr(v)
if !ok {
return nil, fmt.Errorf("expected map but type is %T", v)
}
return m, nil
}
func tryToMapStr(v interface{}) (MapStr, bool) {
switch m := v.(type) {
case MapStr:
return m, true
case map[string]interface{}:
return MapStr(m), true
default:
return nil, false
}
}
// mapFind iterates a MapStr based on a the given dotted key, finding the final
// subMap and subKey to operate on.
// An error is returned if some intermediate is no map or the key doesn't exist.
// If createMissing is set to true, intermediate maps are created.
// The final map and un-dotted key to run further operations on are returned in
// subKey and subMap. The subMap already contains a value for subKey, the
// present flag is set to true and the oldValue return will hold
// the original value.
func mapFind(
key string,
data MapStr,
createMissing bool,
) (subKey string, subMap MapStr, oldValue interface{}, present bool, err error) {
// XXX `safemapstr.mapFind` mimics this implementation, both should be updated to have similar behavior
for {
// Fast path, key is present as is.
if v, exists := data[key]; exists {
return key, data, v, true, nil
}
idx := strings.IndexRune(key, '.')
if idx < 0 {
return key, data, nil, false, nil
}
k := key[:idx]
d, exists := data[k]
if !exists {
if createMissing {
d = MapStr{}
data[k] = d
} else {
return "", nil, nil, false, ErrKeyNotFound
}
}
v, err := toMapStr(d)
if err != nil {
return "", nil, nil, false, err
}
// advance to sub-map
key = key[idx+1:]
data = v
}
}