194 lines
4.9 KiB
Go
194 lines
4.9 KiB
Go
// 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 mapval
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/elastic/beats/libbeat/common"
|
|
)
|
|
|
|
// PathComponentType indicates the type of PathComponent.
|
|
type PathComponentType int
|
|
|
|
const (
|
|
// PCMapKey is the Type for map keys.
|
|
PCMapKey PathComponentType = 1 + iota
|
|
// PCSliceIdx is the Type for slice indices.
|
|
PCSliceIdx
|
|
)
|
|
|
|
func (pct PathComponentType) String() string {
|
|
if pct == PCMapKey {
|
|
return "map"
|
|
} else if pct == PCSliceIdx {
|
|
return "slice"
|
|
} else {
|
|
// This should never happen, but we don't want to return an
|
|
// error since that would unnecessarily complicate the fluid API
|
|
return "<unknown>"
|
|
}
|
|
}
|
|
|
|
// PathComponent structs represent one breadcrumb in a Path.
|
|
type PathComponent struct {
|
|
Type PathComponentType // One of PCMapKey or PCSliceIdx
|
|
Key string // Populated for maps
|
|
Index int // Populated for slices
|
|
}
|
|
|
|
func (pc PathComponent) String() string {
|
|
if pc.Type == PCSliceIdx {
|
|
return fmt.Sprintf("[%d]", pc.Index)
|
|
}
|
|
return pc.Key
|
|
}
|
|
|
|
// Path represents the path within a nested set of maps.
|
|
type Path []PathComponent
|
|
|
|
// ExtendSlice is used to add a new PathComponent of the PCSliceIdx type.
|
|
func (p Path) ExtendSlice(index int) Path {
|
|
return p.extend(
|
|
PathComponent{PCSliceIdx, "", index},
|
|
)
|
|
}
|
|
|
|
// ExtendMap adds a new PathComponent of the PCMapKey type.
|
|
func (p Path) ExtendMap(key string) Path {
|
|
return p.extend(
|
|
PathComponent{PCMapKey, key, -1},
|
|
)
|
|
}
|
|
|
|
func (p Path) extend(pc PathComponent) Path {
|
|
out := make(Path, len(p)+1)
|
|
copy(out, p)
|
|
out[len(p)] = pc
|
|
return out
|
|
}
|
|
|
|
// Concat combines two paths into a new path without modifying any existing paths.
|
|
func (p Path) Concat(other Path) Path {
|
|
out := make(Path, 0, len(p)+len(other))
|
|
out = append(out, p...)
|
|
return append(out, other...)
|
|
}
|
|
|
|
func (p Path) String() string {
|
|
out := make([]string, len(p))
|
|
for idx, pc := range p {
|
|
out[idx] = pc.String()
|
|
}
|
|
return strings.Join(out, ".")
|
|
}
|
|
|
|
// Last returns a pointer to the last PathComponent in this path. If the path empty,
|
|
// a nil pointer is returned.
|
|
func (p Path) Last() *PathComponent {
|
|
idx := len(p) - 1
|
|
if idx < 0 {
|
|
return nil
|
|
}
|
|
return &p[len(p)-1]
|
|
}
|
|
|
|
// GetFrom takes a map and fetches the given path from it.
|
|
func (p Path) GetFrom(m common.MapStr) (value interface{}, exists bool) {
|
|
value = m
|
|
exists = true
|
|
for _, pc := range p {
|
|
rt := reflect.TypeOf(value)
|
|
switch rt.Kind() {
|
|
case reflect.Map:
|
|
converted := interfaceToMapStr(value)
|
|
value, exists = converted[pc.Key]
|
|
case reflect.Slice:
|
|
converted := sliceToSliceOfInterfaces(value)
|
|
if pc.Index < len(converted) {
|
|
exists = true
|
|
value = converted[pc.Index]
|
|
} else {
|
|
exists = false
|
|
value = nil
|
|
}
|
|
default:
|
|
// If this case has been reached this means the expected type, say a map,
|
|
// is actually something else, like a string or an array. In this case we
|
|
// simply say the value doesn't exist. From a practical perspective this is
|
|
// the right behavior since it will cause validation to fail.
|
|
return nil, false
|
|
}
|
|
|
|
if exists == false {
|
|
return nil, exists
|
|
}
|
|
}
|
|
|
|
return value, exists
|
|
}
|
|
|
|
var arrMatcher = regexp.MustCompile("\\[(\\d+)\\]")
|
|
|
|
// InvalidPathString is the error type returned from unparseable paths.
|
|
type InvalidPathString string
|
|
|
|
func (ps InvalidPathString) Error() string {
|
|
return fmt.Sprintf("Invalid path Path: %#v", ps)
|
|
}
|
|
|
|
// ParsePath parses a path of form key.[0].otherKey.[1] into a Path object.
|
|
func ParsePath(in string) (p Path, err error) {
|
|
keyParts := strings.Split(in, ".")
|
|
|
|
p = make(Path, len(keyParts))
|
|
for idx, part := range keyParts {
|
|
r := arrMatcher.FindStringSubmatch(part)
|
|
pc := PathComponent{Index: -1}
|
|
if len(r) > 0 {
|
|
pc.Type = PCSliceIdx
|
|
// Cannot fail, validated by regexp already
|
|
pc.Index, err = strconv.Atoi(r[1])
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
} else if len(part) > 0 {
|
|
pc.Type = PCMapKey
|
|
pc.Key = part
|
|
} else {
|
|
return p, InvalidPathString(in)
|
|
}
|
|
|
|
p[idx] = pc
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// MustParsePath is a convenience method for parsing paths that have been previously validated
|
|
func MustParsePath(in string) Path {
|
|
out, err := ParsePath(in)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return out
|
|
}
|