Refactored!

This commit is contained in:
Mike Farah 2019-12-28 20:19:37 +13:00
parent df52383ffb
commit 0652f67a91
9 changed files with 177 additions and 178 deletions

View File

@ -12,12 +12,12 @@ type DataNavigator interface {
} }
type navigator struct { type navigator struct {
navigationSettings NavigationSettings navigationStrategy NavigationStrategy
} }
func NewDataNavigator(navigationSettings NavigationSettings) DataNavigator { func NewDataNavigator(NavigationStrategy NavigationStrategy) DataNavigator {
return &navigator{ return &navigator{
navigationSettings: navigationSettings, navigationStrategy: NavigationStrategy,
} }
} }
@ -38,7 +38,7 @@ func (n *navigator) doTraverse(value *yaml.Node, head string, path []string, pat
return n.recurse(value, path[0], path[1:], pathStack) return n.recurse(value, path[0], path[1:], pathStack)
} }
log.Debug("should I visit?") log.Debug("should I visit?")
return n.navigationSettings.Visit(value, head, path, pathStack) return n.navigationStrategy.Visit(NodeContext{value, head, path, pathStack})
} }
func (n *navigator) getOrReplace(original *yaml.Node, expectedKind yaml.Kind) *yaml.Node { func (n *navigator) getOrReplace(original *yaml.Node, expectedKind yaml.Kind) *yaml.Node {
@ -66,7 +66,7 @@ func (n *navigator) recurse(value *yaml.Node, head string, tail []string, pathSt
case yaml.AliasNode: case yaml.AliasNode:
log.Debug("its an alias!") log.Debug("its an alias!")
DebugNode(value.Alias) DebugNode(value.Alias)
if n.navigationSettings.FollowAlias(value, head, tail, pathStack) == true { if n.navigationStrategy.FollowAlias(NodeContext{value, head, tail, pathStack}) == true {
log.Debug("following the alias") log.Debug("following the alias")
return n.recurse(value.Alias, head, tail, pathStack) return n.recurse(value.Alias, head, tail, pathStack)
} }
@ -83,7 +83,7 @@ func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, pat
log.Debug("should I traverse? %v", head) log.Debug("should I traverse? %v", head)
DebugNode(value) DebugNode(value)
newPathStack := append(pathStack, contents[indexInMap].Value) newPathStack := append(pathStack, contents[indexInMap].Value)
if n.navigationSettings.ShouldTraverse(contents[indexInMap+1], head, tail, newPathStack, contents[indexInMap].Value) == true { if n.navigationStrategy.ShouldTraverse(NodeContext{contents[indexInMap+1], head, tail, newPathStack}, contents[indexInMap].Value) == true {
log.Debug("yep!") log.Debug("yep!")
traversedEntry = true traversedEntry = true
contents[indexInMap+1] = n.getOrReplace(contents[indexInMap+1], guessKind(tail, contents[indexInMap+1].Kind)) contents[indexInMap+1] = n.getOrReplace(contents[indexInMap+1], guessKind(tail, contents[indexInMap+1].Kind))
@ -98,7 +98,7 @@ func (n *navigator) recurseMap(value *yaml.Node, head string, tail []string, pat
return errorVisiting return errorVisiting
} }
if traversedEntry == true || head == "*" || n.navigationSettings.AutoCreateMap(value, head, tail, pathStack) == false { if traversedEntry == true || head == "*" || n.navigationStrategy.AutoCreateMap(NodeContext{value, head, tail, pathStack}) == false {
return nil return nil
} }
@ -136,7 +136,7 @@ func (n *navigator) visitMatchingEntries(node *yaml.Node, head string, tail []st
// if we don't find a match directly on this node first. // if we don't find a match directly on this node first.
errorVisitedDirectEntries := n.visitDirectMatchingEntries(node, head, tail, pathStack, visit) errorVisitedDirectEntries := n.visitDirectMatchingEntries(node, head, tail, pathStack, visit)
if errorVisitedDirectEntries != nil || n.navigationSettings.FollowAlias(node, head, tail, pathStack) == false { if errorVisitedDirectEntries != nil || n.navigationStrategy.FollowAlias(NodeContext{node, head, tail, pathStack}) == false {
return errorVisitedDirectEntries return errorVisitedDirectEntries
} }
return n.visitAliases(contents, head, tail, pathStack, visit) return n.visitAliases(contents, head, tail, pathStack, visit)

View File

@ -6,38 +6,39 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func DeleteNavigationSettings(lastBit string) NavigationSettings { func DeleteNavigationStrategy(pathElementToDelete string) NavigationStrategy {
parser := NewPathParser() parser := NewPathParser()
return &NavigationSettingsImpl{ return &NavigationStrategyImpl{
visitedNodes: []*VisitedNode{}, visitedNodes: []*NodeContext{},
followAlias: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool { followAlias: func(nodeContext NodeContext) bool {
return false return false
}, },
autoCreateMap: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool { autoCreateMap: func(nodeContext NodeContext) bool {
return true return true
}, },
visit: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) error { visit: func(nodeContext NodeContext) error {
log.Debug("need to find and delete %v in here", lastBit) node := nodeContext.Node
log.Debug("need to find and delete %v in here", pathElementToDelete)
DebugNode(node) DebugNode(node)
if node.Kind == yaml.SequenceNode { if node.Kind == yaml.SequenceNode {
newContent, errorDeleting := deleteFromArray(node.Content, lastBit) newContent, errorDeleting := deleteFromArray(node.Content, pathElementToDelete)
if errorDeleting != nil { if errorDeleting != nil {
return errorDeleting return errorDeleting
} }
node.Content = newContent node.Content = newContent
} else if node.Kind == yaml.MappingNode { } else if node.Kind == yaml.MappingNode {
node.Content = deleteFromMap(parser, node.Content, pathStack, lastBit) node.Content = deleteFromMap(parser, node.Content, nodeContext.PathStack, pathElementToDelete)
} }
return nil return nil
}, },
} }
} }
func deleteFromMap(pathParser PathParser, contents []*yaml.Node, pathStack []interface{}, lastBit string) []*yaml.Node { func deleteFromMap(pathParser PathParser, contents []*yaml.Node, pathStack []interface{}, pathElementToDelete string) []*yaml.Node {
newContents := make([]*yaml.Node, 0) newContents := make([]*yaml.Node, 0)
for index := 0; index < len(contents); index = index + 2 { for index := 0; index < len(contents); index = index + 2 {
keyNode := contents[index] keyNode := contents[index]
valueNode := contents[index+1] valueNode := contents[index+1]
if pathParser.MatchesNextPathElement(keyNode, lastBit, []string{}, pathStack, keyNode.Value) == false { if pathParser.MatchesNextPathElement(NodeContext{keyNode, pathElementToDelete, []string{}, pathStack}, keyNode.Value) == false {
log.Debug("adding node %v", keyNode.Value) log.Debug("adding node %v", keyNode.Value)
newContents = append(newContents, keyNode, valueNode) newContents = append(newContents, keyNode, valueNode)
} else { } else {
@ -47,13 +48,13 @@ func deleteFromMap(pathParser PathParser, contents []*yaml.Node, pathStack []int
return newContents return newContents
} }
func deleteFromArray(content []*yaml.Node, lastBit string) ([]*yaml.Node, error) { func deleteFromArray(content []*yaml.Node, pathElementToDelete string) ([]*yaml.Node, error) {
if lastBit == "*" { if pathElementToDelete == "*" {
return make([]*yaml.Node, 0), nil return make([]*yaml.Node, 0), nil
} }
var index, err = strconv.ParseInt(lastBit, 10, 64) // nolint var index, err = strconv.ParseInt(pathElementToDelete, 10, 64) // nolint
if err != nil { if err != nil {
return content, err return content, err
} }

View File

@ -57,7 +57,7 @@ func guessKind(tail []string, guess yaml.Kind) yaml.Kind {
} }
type YqLib interface { type YqLib interface {
Get(rootNode *yaml.Node, path string) ([]*VisitedNode, error) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error)
Update(rootNode *yaml.Node, updateCommand UpdateCommand) error Update(rootNode *yaml.Node, updateCommand UpdateCommand) error
New(path string) yaml.Node New(path string) yaml.Node
} }
@ -73,12 +73,12 @@ func NewYqLib(l *logging.Logger) YqLib {
} }
} }
func (l *lib) Get(rootNode *yaml.Node, path string) ([]*VisitedNode, error) { func (l *lib) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error) {
var paths = l.parser.ParsePath(path) var paths = l.parser.ParsePath(path)
navigationSettings := ReadNavigationSettings() NavigationStrategy := ReadNavigationStrategy()
navigator := NewDataNavigator(navigationSettings) navigator := NewDataNavigator(NavigationStrategy)
error := navigator.Traverse(rootNode, paths) error := navigator.Traverse(rootNode, paths)
return navigationSettings.GetVisitedNodes(), error return NavigationStrategy.GetVisitedNodes(), error
} }
@ -93,12 +93,12 @@ func (l *lib) Update(rootNode *yaml.Node, updateCommand UpdateCommand) error {
switch updateCommand.Command { switch updateCommand.Command {
case "update": case "update":
var paths = l.parser.ParsePath(updateCommand.Path) var paths = l.parser.ParsePath(updateCommand.Path)
navigator := NewDataNavigator(UpdateNavigationSettings(updateCommand.Value)) navigator := NewDataNavigator(UpdateNavigationStrategy(updateCommand.Value))
return navigator.Traverse(rootNode, paths) return navigator.Traverse(rootNode, paths)
case "delete": case "delete":
var paths = l.parser.ParsePath(updateCommand.Path) var paths = l.parser.ParsePath(updateCommand.Path)
lastBit, newTail := paths[len(paths)-1], paths[:len(paths)-1] lastBit, newTail := paths[len(paths)-1], paths[:len(paths)-1]
navigator := NewDataNavigator(DeleteNavigationSettings(lastBit)) navigator := NewDataNavigator(DeleteNavigationStrategy(lastBit))
return navigator.Traverse(rootNode, newTail) return navigator.Traverse(rootNode, newTail)
default: default:
return fmt.Errorf("Unknown command %v", updateCommand.Command) return fmt.Errorf("Unknown command %v", updateCommand.Command)

View File

@ -1,122 +0,0 @@
package yqlib
import (
"fmt"
yaml "gopkg.in/yaml.v3"
)
type VisitedNode struct {
Node *yaml.Node
Head string
Tail []string
PathStack []interface{}
}
type NavigationSettings interface {
FollowAlias(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool
AutoCreateMap(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool
Visit(node *yaml.Node, head string, tail []string, pathStack []interface{}) error
ShouldTraverse(node *yaml.Node, head string, tail []string, pathStack []interface{}, lastBit string) bool
GetVisitedNodes() []*VisitedNode
}
type NavigationSettingsImpl struct {
followAlias func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool
autoCreateMap func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool
visit func(node *yaml.Node, head string, tail []string, pathStack []interface{}) error
visitedNodes []*VisitedNode
}
func (ns *NavigationSettingsImpl) GetVisitedNodes() []*VisitedNode {
return ns.visitedNodes
}
func (ns *NavigationSettingsImpl) FollowAlias(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool {
return ns.followAlias(node, head, tail, pathStack)
}
func (ns *NavigationSettingsImpl) AutoCreateMap(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool {
return ns.autoCreateMap(node, head, tail, pathStack)
}
func (ns *NavigationSettingsImpl) ShouldTraverse(node *yaml.Node, head string, tail []string, pathStack []interface{}, lastBit string) bool {
// we should traverse aliases (if enabled), but not visit them :/
if len(pathStack) == 0 {
return true
}
if ns.alreadyVisited(pathStack) {
return false
}
parser := NewPathParser()
return (lastBit == "<<" && ns.FollowAlias(node, head, tail, pathStack)) || (lastBit != "<<" &&
parser.MatchesNextPathElement(node, head, tail, pathStack, lastBit))
}
func (ns *NavigationSettingsImpl) shouldVisit(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool {
// we should traverse aliases (if enabled), but not visit them :/
if len(pathStack) == 0 {
return true
}
if ns.alreadyVisited(pathStack) {
return false
}
lastBit := fmt.Sprintf("%v", pathStack[len(pathStack)-1])
parser := NewPathParser()
// only visit aliases if its an exact match
return (lastBit == "<<" && head == "<<") || (lastBit != "<<" &&
parser.MatchesNextPathElement(node, head, tail, pathStack, lastBit))
}
func (ns *NavigationSettingsImpl) Visit(node *yaml.Node, head string, tail []string, pathStack []interface{}) error {
if ns.shouldVisit(node, head, tail, pathStack) {
ns.visitedNodes = append(ns.visitedNodes, &VisitedNode{node, head, tail, pathStack})
log.Debug("adding to visited nodes, %v", head)
return ns.visit(node, head, tail, pathStack)
}
return nil
}
func (ns *NavigationSettingsImpl) alreadyVisited(pathStack []interface{}) bool {
log.Debug("looking for pathStack")
for _, val := range pathStack {
log.Debug("\t %v", val)
}
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")
for _, val := range path1 {
log.Debug("\t %v", val)
}
if len(path1) != len(path2) {
return false
}
for index, p1Value := range path1 {
p2Value := path2[index]
if p1Value != p2Value {
return false
}
}
return true
}

View File

@ -0,0 +1,124 @@
package yqlib
import (
"fmt"
yaml "gopkg.in/yaml.v3"
)
type NodeContext struct {
Node *yaml.Node
Head string
Tail []string
PathStack []interface{}
}
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
GetVisitedNodes() []*NodeContext
}
type NavigationStrategyImpl struct {
followAlias func(nodeContext NodeContext) bool
autoCreateMap func(nodeContext NodeContext) bool
visit func(nodeContext NodeContext) error
visitedNodes []*NodeContext
}
func (ns *NavigationStrategyImpl) GetVisitedNodes() []*NodeContext {
return ns.visitedNodes
}
func (ns *NavigationStrategyImpl) FollowAlias(nodeContext NodeContext) bool {
return ns.followAlias(nodeContext)
}
func (ns *NavigationStrategyImpl) AutoCreateMap(nodeContext NodeContext) bool {
return ns.autoCreateMap(nodeContext)
}
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
}
parser := NewPathParser()
return (nodeKey == "<<" && ns.FollowAlias(nodeContext)) || (nodeKey != "<<" &&
parser.MatchesNextPathElement(nodeContext, nodeKey))
}
func (ns *NavigationStrategyImpl) shouldVisit(nodeContext NodeContext) bool {
// we should traverse aliases (if enabled), but not visit them :/
pathStack := nodeContext.PathStack
if len(pathStack) == 0 {
return true
}
if ns.alreadyVisited(pathStack) {
return false
}
nodeKey := fmt.Sprintf("%v", pathStack[len(pathStack)-1])
parser := NewPathParser()
// only visit aliases if its an exact match
return (nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" &&
parser.MatchesNextPathElement(nodeContext, nodeKey))
}
func (ns *NavigationStrategyImpl) Visit(nodeContext NodeContext) error {
if ns.shouldVisit(nodeContext) {
ns.visitedNodes = append(ns.visitedNodes, &nodeContext)
log.Debug("adding to visited nodes, %v", nodeContext.Head)
return ns.visit(nodeContext)
}
return nil
}
func (ns *NavigationStrategyImpl) alreadyVisited(pathStack []interface{}) bool {
log.Debug("looking for pathStack")
for _, val := range pathStack {
log.Debug("\t %v", val)
}
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")
for _, val := range path1 {
log.Debug("\t %v", val)
}
if len(path1) != len(path2) {
return false
}
for index, p1Value := range path1 {
p2Value := path2[index]
if p1Value != p2Value {
return false
}
}
return true
}

View File

@ -2,13 +2,11 @@ package yqlib
import ( import (
"strings" "strings"
yaml "gopkg.in/yaml.v3"
) )
type PathParser interface { type PathParser interface {
ParsePath(path string) []string ParsePath(path string) []string
MatchesNextPathElement(node *yaml.Node, head string, tail []string, pathStack []interface{}, lastBit string) bool MatchesNextPathElement(nodeContext NodeContext, nodeKey string) bool
} }
type pathParser struct{} type pathParser struct{}
@ -22,15 +20,16 @@ func NewPathParser() PathParser {
* head: path element expression to match against * head: path element expression to match against
* tail: remaining path element expressions * tail: remaining path element expressions
* pathStack: stack of actual paths we've matched to get to node * pathStack: stack of actual paths we've matched to get to node
* lastBit: actual value of this nodes 'key' or index. * nodeKey: actual value of this nodes 'key' or index.
*/ */
func (p *pathParser) MatchesNextPathElement(node *yaml.Node, head string, tail []string, pathStack []interface{}, lastBit string) bool { func (p *pathParser) MatchesNextPathElement(nodeContext NodeContext, nodeKey string) bool {
head := nodeContext.Head
var prefixMatch = strings.TrimSuffix(head, "*") var prefixMatch = strings.TrimSuffix(head, "*")
if prefixMatch != head { if prefixMatch != head {
log.Debug("prefix match, %v", strings.HasPrefix(lastBit, prefixMatch)) log.Debug("prefix match, %v", strings.HasPrefix(nodeKey, prefixMatch))
return strings.HasPrefix(lastBit, prefixMatch) return strings.HasPrefix(nodeKey, prefixMatch)
} }
return lastBit == head return nodeKey == head
} }
func (p *pathParser) ParsePath(path string) []string { func (p *pathParser) ParsePath(path string) []string {

View File

@ -1,19 +1,15 @@
package yqlib package yqlib
import ( func ReadNavigationStrategy() NavigationStrategy {
yaml "gopkg.in/yaml.v3" return &NavigationStrategyImpl{
) visitedNodes: []*NodeContext{},
followAlias: func(nodeContext NodeContext) bool {
func ReadNavigationSettings() NavigationSettings {
return &NavigationSettingsImpl{
visitedNodes: []*VisitedNode{},
followAlias: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool {
return true return true
}, },
autoCreateMap: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool { autoCreateMap: func(nodeContext NodeContext) bool {
return false return false
}, },
visit: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) error { visit: func(nodeContext NodeContext) error {
return nil return nil
}, },
} }

View File

@ -4,16 +4,17 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func UpdateNavigationSettings(changesToApply *yaml.Node) NavigationSettings { func UpdateNavigationStrategy(changesToApply *yaml.Node) NavigationStrategy {
return &NavigationSettingsImpl{ return &NavigationStrategyImpl{
visitedNodes: []*VisitedNode{}, visitedNodes: []*NodeContext{},
followAlias: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool { followAlias: func(nodeContext NodeContext) bool {
return false return false
}, },
autoCreateMap: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) bool { autoCreateMap: func(nodeContext NodeContext) bool {
return true return true
}, },
visit: func(node *yaml.Node, head string, tail []string, pathStack []interface{}) error { visit: func(nodeContext NodeContext) error {
node := nodeContext.Node
log.Debug("going to update") log.Debug("going to update")
DebugNode(node) DebugNode(node)
log.Debug("with") log.Debug("with")

6
yq.go
View File

@ -257,7 +257,7 @@ func readProperty(cmd *cobra.Command, args []string) error {
return errorParsingDocIndex return errorParsingDocIndex
} }
var matchingNodes []*yqlib.VisitedNode var matchingNodes []*yqlib.NodeContext
var currentIndex = 0 var currentIndex = 0
var errorReadingStream = readStream(args[0], func(decoder *yaml.Decoder) error { var errorReadingStream = readStream(args[0], func(decoder *yaml.Decoder) error {
@ -292,7 +292,7 @@ func handleEOF(updateAll bool, docIndexInt int, currentIndex int) error {
return nil return nil
} }
func appendDocument(originalMatchingNodes []*yqlib.VisitedNode, dataBucket yaml.Node, path string, updateAll bool, docIndexInt int, currentIndex int) ([]*yqlib.VisitedNode, error) { func appendDocument(originalMatchingNodes []*yqlib.NodeContext, dataBucket yaml.Node, path string, updateAll bool, docIndexInt int, currentIndex int) ([]*yqlib.NodeContext, error) {
log.Debugf("processing document %v - requested index %v", currentIndex, docIndexInt) log.Debugf("processing document %v - requested index %v", currentIndex, docIndexInt)
yqlib.DebugNode(&dataBucket) yqlib.DebugNode(&dataBucket)
if !updateAll && currentIndex != docIndexInt { if !updateAll && currentIndex != docIndexInt {
@ -337,7 +337,7 @@ func printValue(node *yaml.Node, cmd *cobra.Command) error {
return nil return nil
} }
func printResults(matchingNodes []*yqlib.VisitedNode, cmd *cobra.Command) error { func printResults(matchingNodes []*yqlib.NodeContext, cmd *cobra.Command) error {
if len(matchingNodes) == 0 { if len(matchingNodes) == 0 {
log.Debug("no matching results, nothing to print") log.Debug("no matching results, nothing to print")
return nil return nil