mirror of
https://github.com/mikefarah/yq.git
synced 2024-11-15 07:38:14 +00:00
181 lines
5.2 KiB
Go
181 lines
5.2 KiB
Go
package yqlib
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
yaml "gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type NodeContext struct {
|
|
Node *yaml.Node
|
|
Head interface{}
|
|
Tail []interface{}
|
|
PathStack []interface{}
|
|
// middle nodes are nodes that match along the original path, but not a
|
|
// target match of the path. This is only relevant when ShouldOnlyDeeplyVisitLeaves is false.
|
|
IsMiddleNode bool
|
|
}
|
|
|
|
func NewNodeContext(node *yaml.Node, head interface{}, tail []interface{}, pathStack []interface{}) NodeContext {
|
|
newTail := make([]interface{}, len(tail))
|
|
copy(newTail, tail)
|
|
|
|
newPathStack := make([]interface{}, len(pathStack))
|
|
copy(newPathStack, pathStack)
|
|
return NodeContext{
|
|
Node: node,
|
|
Head: head,
|
|
Tail: newTail,
|
|
PathStack: newPathStack,
|
|
}
|
|
}
|
|
|
|
type NavigationStrategy interface {
|
|
FollowAlias(nodeContext NodeContext) bool
|
|
AutoCreateMap(nodeContext NodeContext) bool
|
|
Visit(nodeContext NodeContext) error
|
|
// node key is the string value of the last element in the path stack
|
|
// we use it to match against the pathExpression in head.
|
|
ShouldTraverse(nodeContext NodeContext, nodeKey string) bool
|
|
ShouldDeeplyTraverse(nodeContext NodeContext) bool
|
|
// when deeply traversing, should we visit all matching nodes, or just leaves?
|
|
ShouldOnlyDeeplyVisitLeaves(NodeContext) bool
|
|
GetVisitedNodes() []*NodeContext
|
|
DebugVisitedNodes()
|
|
GetPathParser() PathParser
|
|
}
|
|
|
|
type NavigationStrategyImpl struct {
|
|
followAlias func(nodeContext NodeContext) bool
|
|
autoCreateMap func(nodeContext NodeContext) bool
|
|
visit func(nodeContext NodeContext) error
|
|
shouldVisitExtraFn func(nodeContext NodeContext) bool
|
|
shouldDeeplyTraverse func(nodeContext NodeContext) bool
|
|
shouldOnlyDeeplyVisitLeaves func(nodeContext NodeContext) bool
|
|
visitedNodes []*NodeContext
|
|
pathParser PathParser
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) GetPathParser() PathParser {
|
|
return ns.pathParser
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) GetVisitedNodes() []*NodeContext {
|
|
return ns.visitedNodes
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) FollowAlias(nodeContext NodeContext) bool {
|
|
if ns.followAlias != nil {
|
|
return ns.followAlias(nodeContext)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) AutoCreateMap(nodeContext NodeContext) bool {
|
|
if ns.autoCreateMap != nil {
|
|
return ns.autoCreateMap(nodeContext)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) ShouldDeeplyTraverse(nodeContext NodeContext) bool {
|
|
if ns.shouldDeeplyTraverse != nil {
|
|
return ns.shouldDeeplyTraverse(nodeContext)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) ShouldOnlyDeeplyVisitLeaves(nodeContext NodeContext) bool {
|
|
if ns.shouldOnlyDeeplyVisitLeaves != nil {
|
|
return ns.shouldOnlyDeeplyVisitLeaves(nodeContext)
|
|
}
|
|
return true
|
|
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) ShouldTraverse(nodeContext NodeContext, nodeKey string) bool {
|
|
// we should traverse aliases (if enabled), but not visit them :/
|
|
if len(nodeContext.PathStack) == 0 {
|
|
return true
|
|
}
|
|
|
|
if ns.alreadyVisited(nodeContext.PathStack) {
|
|
return false
|
|
}
|
|
|
|
return (nodeKey == "<<" && ns.FollowAlias(nodeContext)) || (nodeKey != "<<" &&
|
|
ns.pathParser.MatchesNextPathElement(nodeContext, nodeKey))
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) shouldVisit(nodeContext NodeContext) bool {
|
|
pathStack := nodeContext.PathStack
|
|
if len(pathStack) == 0 {
|
|
return true
|
|
}
|
|
log.Debug("tail len %v", len(nodeContext.Tail))
|
|
|
|
if ns.alreadyVisited(pathStack) || len(nodeContext.Tail) != 0 {
|
|
return false
|
|
}
|
|
|
|
nodeKey := fmt.Sprintf("%v", pathStack[len(pathStack)-1])
|
|
log.Debug("nodeKey: %v, nodeContext.Head: %v", nodeKey, nodeContext.Head)
|
|
|
|
// only visit aliases if its an exact match
|
|
return ((nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" &&
|
|
ns.pathParser.MatchesNextPathElement(nodeContext, nodeKey))) && (ns.shouldVisitExtraFn == nil || ns.shouldVisitExtraFn(nodeContext))
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) Visit(nodeContext NodeContext) error {
|
|
log.Debug("Visit?, %v, %v", nodeContext.Head, pathStackToString(nodeContext.PathStack))
|
|
DebugNode(nodeContext.Node)
|
|
if ns.shouldVisit(nodeContext) {
|
|
log.Debug("yep, visiting")
|
|
// pathStack array must be
|
|
// copied, as append() may sometimes reuse and modify the array
|
|
ns.visitedNodes = append(ns.visitedNodes, &nodeContext)
|
|
ns.DebugVisitedNodes()
|
|
return ns.visit(nodeContext)
|
|
}
|
|
log.Debug("nope, skip it")
|
|
return nil
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) DebugVisitedNodes() {
|
|
log.Debug("Visited Nodes:")
|
|
for _, candidate := range ns.visitedNodes {
|
|
log.Debug(" - %v", pathStackToString(candidate.PathStack))
|
|
}
|
|
}
|
|
|
|
func (ns *NavigationStrategyImpl) alreadyVisited(pathStack []interface{}) bool {
|
|
log.Debug("checking already visited pathStack: %v", pathStackToString(pathStack))
|
|
for _, candidate := range ns.visitedNodes {
|
|
candidatePathStack := candidate.PathStack
|
|
if patchStacksMatch(candidatePathStack, pathStack) {
|
|
log.Debug("paths match, already seen it")
|
|
return true
|
|
}
|
|
|
|
}
|
|
log.Debug("never seen it before!")
|
|
return false
|
|
}
|
|
|
|
func patchStacksMatch(path1 []interface{}, path2 []interface{}) bool {
|
|
log.Debug("checking against path: %v", pathStackToString(path1))
|
|
|
|
if len(path1) != len(path2) {
|
|
return false
|
|
}
|
|
for index, p1Value := range path1 {
|
|
|
|
p2Value := path2[index]
|
|
if p1Value != p2Value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
|
|
}
|