llpath/path.go (138 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 llpath import ( "fmt" "github.com/elastic/go-lookslike/internal/llreflect" "reflect" "regexp" "strconv" "strings" ) // 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 // pcInterface is the type for all other values pcInterface ) func (pct PathComponentType) String() string { if pct == pcMapKey { return "map" } else if pct == pcSliceIdx { return "slice" } else if pct == pcInterface { return "scalar" } 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}, ) } // Extend lengthens the given path with the given component. 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(source reflect.Value) (result reflect.Value, exists bool) { // nil values are handled specially. If we're fetching from a nil // there's one case where it exists, when comparing it to another nil. if (source.Kind() == reflect.Map || source.Kind() == reflect.Slice) && source.IsNil() { // since another nil would be scalar, we just check that the // path length is 0. return source, len(p) == 0 } result = source exists = true for _, pc := range p { switch result.Kind() { case reflect.Map: result = llreflect.ChaseValue(result.MapIndex(reflect.ValueOf(pc.Key))) exists = result != reflect.Value{} case reflect.Slice, reflect.Array: if pc.Index < result.Len() { result = llreflect.ChaseValue(result.Index(pc.Index)) exists = result != reflect.Value{} } else { result = reflect.ValueOf(nil) exists = false } 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 result doesn't exist. From a practical perspective this is // the right behavior since it will cause validation to fail. return reflect.ValueOf(nil), false } if exists == false { return reflect.ValueOf(nil), exists } } return result, 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: %#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, ".") // We return empty paths for empty strings // Empty paths are valid when working with scalar values if in == "" { return Path{}, nil } 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 nil, 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 }