mapstr/mapstr.go (459 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package mapstr
import (
"encoding/json"
"errors"
"fmt"
"io"
"sort"
"strings"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/elastic/elastic-agent-libs/config"
)
// Event metadata constants. These keys are used within libbeat to identify
// metadata stored in an event.
const (
FieldsKey = "fields"
TagsKey = "tags"
)
var (
// ErrKeyNotFound indicates that the specified key was not found.
ErrKeyNotFound = errors.New("key not found")
// ErrKeyCollision indicates that during the case-insensitive search multiple keys matched
ErrKeyCollision = errors.New("key collision")
// ErrNotMapType indicates that the given value is not map type
ErrNotMapType = errors.New("value is not a map")
)
// EventMetadata contains fields and tags that can be added to an event via
// configuration.
type EventMetadata struct {
Fields M
FieldsUnderRoot bool `config:"fields_under_root"`
Tags []string
}
// M is a map[string]interface{} wrapper with utility methods for common
// map operations like converting to JSON.
type M map[string]interface{}
// Update copies all the key-value pairs from d to this map. If the key
// already exists then it is overwritten. This method does not merge nested
// maps.
func (m M) Update(d M) {
for k, v := range d {
m[k] = v
}
}
// 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 M) DeepUpdate(d M) {
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 M) DeepUpdateNoOverwrite(d M) {
m.deepUpdateMap(d, false)
}
func (m M) deepUpdateMap(d M, overwrite bool) {
for k, v := range d {
switch val := v.(type) {
case map[string]interface{}:
m[k] = deepUpdateValue(m[k], M(val), overwrite)
case M:
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 M, overwrite bool) interface{} {
switch sub := old.(type) {
case M:
if sub == nil {
return val
}
sub.deepUpdateMap(val, overwrite)
return sub
case map[string]interface{}:
if sub == nil {
return val
}
tmp := M(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 M) 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
}
// CopyFieldsTo copies the field specified by key to the given map. It will
// overwrite the key if it exists. An error is returned if the key does not
// exist in the source map.
func (m M) CopyFieldsTo(to M, key string) error {
v, err := m.GetValue(key)
if err != nil {
return err
}
_, err = to.Put(key, v)
return err
}
// Clone returns a copy of the M. It recursively makes copies of inner
// maps. Nested arrays and non-map types are not cloned.
func (m M) Clone() M {
result := make(M, len(m))
cloneMap(result, m)
return result
}
func cloneMap(dst, src M) {
for k, v := range src {
switch v := v.(type) {
case M:
d := make(M, len(v))
dst[k] = d
cloneMap(d, v)
case map[string]interface{}:
d := make(M, len(v))
dst[k] = d
cloneMap(d, v)
case []M:
a := make([]M, 0, len(v))
for _, m := range v {
d := make(M, len(m))
cloneMap(d, m)
a = append(a, d)
}
dst[k] = a
case []map[string]interface{}:
a := make([]M, 0, len(v))
for _, m := range v {
d := make(M, len(m))
cloneMap(d, m)
a = append(a, d)
}
dst[k] = a
default:
dst[k] = v
}
}
}
// HasKey returns true if the key exist. If an error occurs then false is
// returned with a non-nil error.
func (m M) HasKey(key string) (bool, error) {
_, _, _, hasKey, err := mapFind(key, m, false)
return hasKey, err
}
// FindFold accepts a key and traverses the map trying to match every key segment
// using `strings.FindFold` (case-insensitive match) and returns the actual
// key of the map that matched the given key and the value stored under this key.
// Returns `ErrKeyCollision` if multiple keys match the same request.
// Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
// Returns `ErrKeyNotFound` when the path does not exist
func (m M) FindFold(path string) (matchedKey string, value interface{}, err error) {
segmentCount := strings.Count(path, ".") + 1
err = m.Traverse(path, CaseInsensitiveMode, func(level M, key string) error {
segmentCount--
matchedKey += key
if segmentCount != 0 {
matchedKey += "."
return nil
}
value = level[key]
return nil
})
if err != nil {
return "", nil, err
}
return matchedKey, value, nil
}
type AlterFunc func(string) (string, error)
// AlterPath walks the given `path` and replaces matching keys using the value returned by `alterFunc`.
// `mode` sets the behavior how the given path is matched throughout the levels.
// Returns `ErrKeyCollision` if multiple keys match the same request (when `mode` is `CaseInsensitiveMode`).
// Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
// Returns `ErrKeyNotFound` when the path does not exist
func (m M) AlterPath(path string, mode TraversalMode, alterFunc AlterFunc) (err error) {
return m.Traverse(path, mode, func(level M, key string) error {
val := level[key]
newKey, err := alterFunc(key)
if err != nil {
return fmt.Errorf("failed to apply a change to %q: %w", key, err)
}
if newKey == "" {
return fmt.Errorf("replacement key for %q cannot be empty", key)
}
// if altered key is equal to the original key, skip below delete/put func
if newKey == key {
return nil
}
_, exists := level[newKey]
if exists {
return fmt.Errorf("replacement key %q already exists: %w", newKey, ErrKeyCollision)
}
delete(level, key)
level[newKey] = val
return nil
})
}
// TraversalMode used for traversing the map through multiple levels.
type TraversalMode int
const (
// The key match is strictly case-sensitive
CaseSensitiveMode = iota
// The key match is performed with `strings.EqualFold`
CaseInsensitiveMode = iota
)
type TraversalVisitor func(M, string) error
// Traverse walks the given nested `path` in the map and invokes the `visitor` function on each level passing
// the current-level map and the current key.
// `mode` sets the behavior how the given path is matched throughout the levels.
// The `visitor` function is allowed to make changes in the level or collect data.
// Returns `ErrKeyCollision` if multiple keys match the same request (when `mode` is `CaseInsensitiveMode`).
// Returns `ErrNotMapType` when one of the values on the path is not a map and cannot be traversed.
// Returns `ErrKeyNotFound` when the path does not exist
func (m M) Traverse(path string, mode TraversalMode, visitor TraversalVisitor) (err error) {
segments := strings.Split(path, ".")
var match func(string, string) bool
switch mode {
case CaseInsensitiveMode:
match = strings.EqualFold
case CaseSensitiveMode:
match = func(a, b string) bool { return a == b }
}
// the initial value must be `true` for the first iteration to work
found := true
// start with the root
current := m
// allocate only once
var (
mapType bool
next interface{}
)
for i, segment := range segments {
if !found {
return fmt.Errorf("could not fetch value for key: %s, Error: %w ", path, ErrKeyNotFound)
}
found = false
// we have to go through the list of all key on each level to detect case-insensitive collisions
for k := range current {
if !match(segment, k) {
continue
}
// if already found on this level, it's a collision
if found {
return fmt.Errorf("multiple keys match %q on the same level of the path %q: %w", k, path, ErrKeyCollision)
}
// mark for collision detection
found = true
// we need to save this in case the visitor makes changes in keys
next = current[k]
err = visitor(current, k)
if err != nil {
return fmt.Errorf("error visiting key %q of the path %q: %w", k, path, err)
}
// if it's the last segment, we don't need to go deeper, skipping...
if i == len(segments)-1 {
continue
}
// try to go one level deeper
current, mapType = tryToMapStr(next)
if !mapType {
return fmt.Errorf("cannot continue path %q, next value %q is not a map: %w", path, k, ErrNotMapType)
}
// if it's a case-sensitive key match, we don't have to care about collision detection
// and we can simply stop iterating here.
if mode == CaseSensitiveMode {
break
}
}
}
if !found {
return fmt.Errorf("could not fetch value for key: %s, Error: %w", path, ErrKeyNotFound)
}
return nil
}
// GetValue gets a value from the map. If the key does not exist then an error
// is returned.
func (m M) 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 M) 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
}
// StringToPrint returns the M as pretty JSON.
func (m M) StringToPrint() string {
json, err := json.MarshalIndent(m, "", " ")
if err != nil {
return fmt.Sprintf("Not valid json: %v", err)
}
return string(json)
}
// String returns the M as JSON.
func (m M) String() string {
bytes, err := json.Marshal(m)
if err != nil {
return fmt.Sprintf("Not valid json: %v", err)
}
return string(bytes)
}
// MarshalLogObject implements the zapcore.ObjectMarshaler interface and allows
// for more efficient marshaling of mapstr.M in structured logging.
func (m M) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if len(m) == 0 {
return nil
}
debugM := m.Clone()
config.ApplyLoggingMask(map[string]interface{}(debugM))
keys := make([]string, 0, len(debugM))
for k := range debugM {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := debugM[k]
if inner, ok := tryToMapStr(v); ok {
err := enc.AddObject(k, inner)
if err != nil {
return fmt.Errorf("failed to add object: %w", err)
}
continue
}
zap.Any(k, v).AddTo(enc)
}
return nil
}
// Format implements fmt.Formatter
func (m M) Format(f fmt.State, c rune) {
if f.Flag('+') || f.Flag('#') {
_, _ = io.WriteString(f, m.String())
return
}
debugM := m.Clone()
config.ApplyLoggingMask(map[string]interface{}(debugM))
_, _ = io.WriteString(f, debugM.String())
}
// Flatten flattens the given M and returns a flat M.
//
// Example:
//
// "hello": M{"world": "test" }
//
// This is converted to:
//
// "hello.world": "test"
//
// This can be useful for testing or logging.
func (m M) Flatten() M {
return flatten("", m, M{})
}
// flatten is a helper for Flatten. See docs for Flatten. For convenience the
// out parameter is returned.
func flatten(prefix string, in, out M) M {
for k, v := range in {
var fullKey string
if prefix == "" {
fullKey = k
} else {
fullKey = prefix + "." + k
}
if m, ok := tryToMapStr(v); ok {
flatten(fullKey, m, out)
} else {
out[fullKey] = v
}
}
return out
}
// FlattenKeys flattens given MapStr keys and returns a containing array pointer
//
// Example:
//
// "hello": MapStr{"world": "test" }
//
// This is converted to:
//
// ["hello.world"]
func (m M) FlattenKeys() *[]string {
out := make([]string, 0)
flattenKeys("", m, &out)
return &out
}
func flattenKeys(prefix string, in M, out *[]string) {
for k, v := range in {
var fullKey string
if prefix == "" {
fullKey = k
} else {
fullKey = prefix + "." + k
}
if m, ok := tryToMapStr(v); ok {
flattenKeys(fullKey, m, out)
}
*out = append(*out, fullKey)
}
}
// Union creates a new M containing the union of the
// key-value pairs of the two maps. If the same key is present in
// both, the key-value pairs from dict2 overwrite the ones from dict1.
func Union(dict1 M, dict2 M) M {
dict := M{}
for k, v := range dict1 {
dict[k] = v
}
for k, v := range dict2 {
dict[k] = v
}
return dict
}
// MergeFields merges the top-level keys and values in each source map (it does
// not perform a deep merge). If the same key exists in both, the value in
// fields takes precedence. If underRoot is true then the contents of the fields
// M is merged with the value of the 'fields' key in target.
//
// An error is returned if underRoot is true and the value of ms.fields is not a
// M.
func MergeFields(target, from M, underRoot bool) error {
if target == nil || len(from) == 0 {
return nil
}
destMap, err := mergeFieldsGetDestMap(target, from, underRoot)
if err != nil {
return err
}
// Add fields and override.
for k, v := range from {
destMap[k] = v
}
return nil
}
// MergeFieldsDeep recursively merges the keys and values from `from` into `target`, either
// into ms itself (if underRoot == true) or into ms["fields"] (if underRoot == false). If
// the same key exists in `from` and the destination map, the value in fields takes precedence.
//
// An error is returned if underRoot is true and the value of ms["fields"] is not a
// M.
func MergeFieldsDeep(target, from M, underRoot bool) error {
if target == nil || len(from) == 0 {
return nil
}
destMap, err := mergeFieldsGetDestMap(target, from, underRoot)
if err != nil {
return err
}
destMap.DeepUpdate(from)
return nil
}
func mergeFieldsGetDestMap(target, from M, underRoot bool) (M, error) {
destMap := target
if !underRoot {
f, ok := target[FieldsKey]
if !ok {
destMap = make(M, len(from))
target[FieldsKey] = destMap
} else {
// Use existing 'fields' value.
var err error
destMap, err = toMapStr(f)
if err != nil {
return nil, err
}
}
}
return destMap, nil
}
// AddTags appends a tag to the tags field of ms. If the tags field does not
// exist then it will be created. If the tags field exists and is not a []string
// then an error will be returned. It does not deduplicate the list of tags.
func AddTags(ms M, tags []string) error {
return AddTagsWithKey(ms, TagsKey, tags)
}
// AddTagsWithKey appends a tag to the key field of ms. If the field does not
// exist then it will be created. If the field exists and is not a []string
// then an error will be returned. It does not deduplicate the list.
func AddTagsWithKey(ms M, key string, tags []string) error {
if ms == nil || len(tags) == 0 {
return nil
}
k, subMap, oldTags, present, err := mapFind(key, ms, true)
if err != nil {
return err
}
if !present {
subMap[k] = tags
return nil
}
switch arr := oldTags.(type) {
case []string:
subMap[k] = append(arr, tags...)
case []interface{}:
for _, tag := range tags {
arr = append(arr, tag)
}
subMap[k] = arr
default:
return fmt.Errorf("expected string array by type is %T", oldTags)
}
return 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{}) (M, 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{}) (M, bool) {
switch m := v.(type) {
case M:
return m, true
case map[string]interface{}:
return M(m), true
default:
return nil, false
}
}
// mapFind iterates a M 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 M,
createMissing bool,
) (subKey string, subMap M, 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 = M{}
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
}
}