refactored array merge flags into a strategy

This commit is contained in:
Mike Farah 2020-07-17 13:26:20 +10:00
parent 2fc39b3865
commit d66a709213
7 changed files with 48 additions and 30 deletions

View File

@ -26,8 +26,7 @@ var defaultValue = ""
var indent = 2 var indent = 2
var overwriteFlag = false var overwriteFlag = false
var autoCreateFlag = true var autoCreateFlag = true
var appendFlag = false var arrayMergeStrategyFlag = "update"
var overwriteArrays = false
var verbose = false var verbose = false
var version = false var version = false
var docIndex = "0" var docIndex = "0"

View File

@ -11,14 +11,14 @@ func createMergeCmd() *cobra.Command {
var cmdMerge = &cobra.Command{ var cmdMerge = &cobra.Command{
Use: "merge [initial_yaml_file] [additional_yaml_file]...", Use: "merge [initial_yaml_file] [additional_yaml_file]...",
Aliases: []string{"m"}, Aliases: []string{"m"},
Short: "yq m [--inplace/-i] [--doc/-d index] [--overwrite/-x] [--append/-a] sample.yaml sample2.yaml", Short: "yq m [--inplace/-i] [--doc/-d index] [--overwrite/-x] [--arrayMerge/-a strategy] sample.yaml sample2.yaml",
Example: ` Example: `
yq merge things.yaml other.yaml yq merge things.yaml other.yaml
yq merge --inplace things.yaml other.yaml yq merge --inplace things.yaml other.yaml
yq m -i things.yaml other.yaml yq m -i things.yaml other.yaml
yq m --overwrite things.yaml other.yaml yq m --overwrite things.yaml other.yaml
yq m -i -x things.yaml other.yaml yq m -i -x things.yaml other.yaml
yq m -i -a things.yaml other.yaml yq m -i -a=append things.yaml other.yaml
yq m -i --autocreate=false things.yaml other.yaml yq m -i --autocreate=false things.yaml other.yaml
`, `,
Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s). Long: `Updates the yaml file by adding/updating the path(s) and value(s) from additional yaml file(s).
@ -32,9 +32,8 @@ If append flag is set then existing arrays will be merged with the arrays from e
cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace") cmdMerge.PersistentFlags().BoolVarP(&writeInplace, "inplace", "i", false, "update the yaml file inplace")
cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values") cmdMerge.PersistentFlags().BoolVarP(&overwriteFlag, "overwrite", "x", false, "update the yaml file by overwriting existing values")
cmdMerge.PersistentFlags().BoolVarP(&autoCreateFlag, "autocreate", "c", true, "automatically create any missing entries") cmdMerge.PersistentFlags().BoolVarP(&autoCreateFlag, "autocreate", "c", true, "automatically create any missing entries")
cmdMerge.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "update the yaml file by appending array values") cmdMerge.PersistentFlags().StringVarP(&arrayMergeStrategyFlag, "array", "a", "update", "array merge strategy (update/append/overwrite)")
cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)") cmdMerge.PersistentFlags().StringVarP(&docIndex, "doc", "d", "0", "process document index number (0 based, * for all documents)")
cmdMerge.PersistentFlags().BoolVarP(&overwriteArrays, "overwriteArrays", "", false, "overwrite arrays rather than update recursively")
return cmdMerge return cmdMerge
} }
@ -42,9 +41,9 @@ If append flag is set then existing arrays will be merged with the arrays from e
* We don't deeply traverse arrays when appending a merge, instead we want to * We don't deeply traverse arrays when appending a merge, instead we want to
* append the entire array element. * append the entire array element.
*/ */
func createReadFunctionForMerge() func(*yaml.Node) ([]*yqlib.NodeContext, error) { func createReadFunctionForMerge(arrayMergeStrategy yqlib.ArrayMergeStrategy) func(*yaml.Node) ([]*yqlib.NodeContext, error) {
return func(dataBucket *yaml.Node) ([]*yqlib.NodeContext, error) { return func(dataBucket *yaml.Node) ([]*yqlib.NodeContext, error) {
return lib.GetForMerge(dataBucket, "**", !appendFlag, overwriteArrays) return lib.GetForMerge(dataBucket, "**", arrayMergeStrategy)
} }
} }
@ -54,13 +53,25 @@ func mergeProperties(cmd *cobra.Command, args []string) error {
if len(args) < 1 { if len(args) < 1 {
return errors.New("Must provide at least 1 yaml file") return errors.New("Must provide at least 1 yaml file")
} }
var arrayMergeStrategy yqlib.ArrayMergeStrategy
switch arrayMergeStrategyFlag {
case "update":
arrayMergeStrategy = yqlib.UpdateArrayMergeStrategy
case "append":
arrayMergeStrategy = yqlib.AppendArrayMergeStrategy
case "overwrite":
arrayMergeStrategy = yqlib.OverwriteArrayMergeStrategy
default:
return errors.New("Array merge strategy must be one of: update/append/overwrite")
}
if len(args) > 1 { if len(args) > 1 {
// first generate update commands from the file // first generate update commands from the file
var filesToMerge = args[1:] var filesToMerge = args[1:]
for _, fileToMerge := range filesToMerge { for _, fileToMerge := range filesToMerge {
matchingNodes, errorProcessingFile := doReadYamlFile(fileToMerge, createReadFunctionForMerge(), false, 0) matchingNodes, errorProcessingFile := doReadYamlFile(fileToMerge, createReadFunctionForMerge(arrayMergeStrategy), false, 0)
if errorProcessingFile != nil { if errorProcessingFile != nil {
return errorProcessingFile return errorProcessingFile
} }
@ -70,14 +81,14 @@ func mergeProperties(cmd *cobra.Command, args []string) error {
yqlib.DebugNode(matchingNode.Node) yqlib.DebugNode(matchingNode.Node)
} }
for _, matchingNode := range matchingNodes { for _, matchingNode := range matchingNodes {
mergePath := lib.MergePathStackToString(matchingNode.PathStack, appendFlag) mergePath := lib.MergePathStackToString(matchingNode.PathStack, arrayMergeStrategy)
updateCommands = append(updateCommands, yqlib.UpdateCommand{ updateCommands = append(updateCommands, yqlib.UpdateCommand{
Command: "merge", Command: "merge",
Path: mergePath, Path: mergePath,
Value: matchingNode.Node, Value: matchingNode.Node,
Overwrite: overwriteFlag, Overwrite: overwriteFlag,
// dont update the content for nodes midway, only leaf nodes // dont update the content for nodes midway, only leaf nodes
DontUpdateNodeContent: matchingNode.IsMiddleNode && (!overwriteArrays || matchingNode.Node.Kind != yaml.SequenceNode), DontUpdateNodeContent: matchingNode.IsMiddleNode && (arrayMergeStrategy != yqlib.OverwriteArrayMergeStrategy || matchingNode.Node.Kind != yaml.SequenceNode),
}) })
} }
} }

View File

@ -97,7 +97,7 @@ func TestMergeOverwriteDeepExampleCmd(t *testing.T) {
func TestMergeAppendCmd(t *testing.T) { func TestMergeAppendCmd(t *testing.T) {
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, "merge --autocreate=false --append ../examples/data1.yaml ../examples/data2.yaml") result := test.RunCmd(cmd, "merge --autocreate=false --array=append ../examples/data1.yaml ../examples/data2.yaml")
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }
@ -123,7 +123,7 @@ func TestMergeAppendArraysCmd(t *testing.T) {
defer test.RemoveTempYamlFile(mergeFilename) defer test.RemoveTempYamlFile(mergeFilename)
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, fmt.Sprintf("merge --append -d* %s %s", filename, mergeFilename)) result := test.RunCmd(cmd, fmt.Sprintf("merge --array=append -d* %s %s", filename, mergeFilename))
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }
@ -181,7 +181,7 @@ usage:
func TestMergeOverwriteAndAppendCmd(t *testing.T) { func TestMergeOverwriteAndAppendCmd(t *testing.T) {
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, "merge --autocreate=false --append --overwrite ../examples/data1.yaml ../examples/data2.yaml") result := test.RunCmd(cmd, "merge --autocreate=false --array=append --overwrite ../examples/data1.yaml ../examples/data2.yaml")
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }
@ -208,7 +208,7 @@ b: [6]`
defer test.RemoveTempYamlFile(mergeFilename) defer test.RemoveTempYamlFile(mergeFilename)
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, fmt.Sprintf("merge --autocreate=false --overwriteArrays --overwrite %s %s", filename, mergeFilename)) result := test.RunCmd(cmd, fmt.Sprintf("merge --autocreate=false --array=overwrite --overwrite %s %s", filename, mergeFilename))
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }
@ -223,7 +223,7 @@ c:
func TestMergeRootArraysCmd(t *testing.T) { func TestMergeRootArraysCmd(t *testing.T) {
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, "merge --append ../examples/sample_array.yaml ../examples/sample_array_2.yaml") result := test.RunCmd(cmd, "merge --array=append ../examples/sample_array.yaml ../examples/sample_array_2.yaml")
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }
@ -238,7 +238,7 @@ func TestMergeRootArraysCmd(t *testing.T) {
func TestMergeOverwriteArraysCmd(t *testing.T) { func TestMergeOverwriteArraysCmd(t *testing.T) {
cmd := getRootCommand() cmd := getRootCommand()
result := test.RunCmd(cmd, "merge --overwriteArrays ../examples/sample_array.yaml ../examples/sample_array_2.yaml") result := test.RunCmd(cmd, "merge --array=overwrite ../examples/sample_array.yaml ../examples/sample_array_2.yaml")
if result.Error != nil { if result.Error != nil {
t.Error(result.Error) t.Error(result.Error)
} }

View File

@ -58,15 +58,15 @@ func DebugNode(value *yaml.Node) {
} }
func pathStackToString(pathStack []interface{}) string { func pathStackToString(pathStack []interface{}) string {
return mergePathStackToString(pathStack, false) return mergePathStackToString(pathStack, UpdateArrayMergeStrategy)
} }
func mergePathStackToString(pathStack []interface{}, appendArrays bool) string { func mergePathStackToString(pathStack []interface{}, arrayMergeStrategy ArrayMergeStrategy) string {
var sb strings.Builder var sb strings.Builder
for index, path := range pathStack { for index, path := range pathStack {
switch path.(type) { switch path.(type) {
case int, int64: case int, int64:
if appendArrays { if arrayMergeStrategy == AppendArrayMergeStrategy {
sb.WriteString("[+]") sb.WriteString("[+]")
} else { } else {
sb.WriteString(fmt.Sprintf("[%v]", path)) sb.WriteString(fmt.Sprintf("[%v]", path))
@ -136,12 +136,12 @@ func guessKind(head interface{}, tail []interface{}, guess yaml.Kind) yaml.Kind
type YqLib interface { type YqLib interface {
Get(rootNode *yaml.Node, path string) ([]*NodeContext, error) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error)
GetForMerge(rootNode *yaml.Node, path string, deeplyTraverseArrays bool, overwriteArray bool) ([]*NodeContext, error) GetForMerge(rootNode *yaml.Node, path string, arrayMergeStrategy ArrayMergeStrategy) ([]*NodeContext, error)
Update(rootNode *yaml.Node, updateCommand UpdateCommand, autoCreate bool) error Update(rootNode *yaml.Node, updateCommand UpdateCommand, autoCreate bool) error
New(path string) yaml.Node New(path string) yaml.Node
PathStackToString(pathStack []interface{}) string PathStackToString(pathStack []interface{}) string
MergePathStackToString(pathStack []interface{}, appendArrays bool) string MergePathStackToString(pathStack []interface{}, arrayMergeStrategy ArrayMergeStrategy) string
} }
type lib struct { type lib struct {
@ -162,9 +162,9 @@ func (l *lib) Get(rootNode *yaml.Node, path string) ([]*NodeContext, error) {
return navigationStrategy.GetVisitedNodes(), error return navigationStrategy.GetVisitedNodes(), error
} }
func (l *lib) GetForMerge(rootNode *yaml.Node, path string, deeplyTraverseArrays bool, overwriteArray bool) ([]*NodeContext, error) { func (l *lib) GetForMerge(rootNode *yaml.Node, path string, arrayMergeStrategy ArrayMergeStrategy) ([]*NodeContext, error) {
var paths = l.parser.ParsePath(path) var paths = l.parser.ParsePath(path)
navigationStrategy := ReadForMergeNavigationStrategy(deeplyTraverseArrays, overwriteArray) navigationStrategy := ReadForMergeNavigationStrategy(arrayMergeStrategy)
navigator := NewDataNavigator(navigationStrategy) navigator := NewDataNavigator(navigationStrategy)
error := navigator.Traverse(rootNode, paths) error := navigator.Traverse(rootNode, paths)
return navigationStrategy.GetVisitedNodes(), error return navigationStrategy.GetVisitedNodes(), error
@ -174,8 +174,8 @@ func (l *lib) PathStackToString(pathStack []interface{}) string {
return pathStackToString(pathStack) return pathStackToString(pathStack)
} }
func (l *lib) MergePathStackToString(pathStack []interface{}, appendArrays bool) string { func (l *lib) MergePathStackToString(pathStack []interface{}, arrayMergeStrategy ArrayMergeStrategy) string {
return mergePathStackToString(pathStack, appendArrays) return mergePathStackToString(pathStack, arrayMergeStrategy)
} }
func (l *lib) New(path string) yaml.Node { func (l *lib) New(path string) yaml.Node {

View File

@ -30,7 +30,7 @@ func TestLib(t *testing.T) {
array[0] = "a" array[0] = "a"
array[1] = 0 array[1] = 0
array[2] = "b" array[2] = "b"
got := subject.MergePathStackToString(array, true) got := subject.MergePathStackToString(array, AppendArrayMergeStrategy)
test.AssertResult(t, `a.[+].b`, got) test.AssertResult(t, `a.[+].b`, got)
}) })

View File

@ -2,6 +2,14 @@ package yqlib
import "gopkg.in/yaml.v3" import "gopkg.in/yaml.v3"
type ArrayMergeStrategy uint32
const (
UpdateArrayMergeStrategy ArrayMergeStrategy = 1 << iota
OverwriteArrayMergeStrategy
AppendArrayMergeStrategy
)
func MergeNavigationStrategy(updateCommand UpdateCommand, autoCreate bool) NavigationStrategy { func MergeNavigationStrategy(updateCommand UpdateCommand, autoCreate bool) NavigationStrategy {
return &NavigationStrategyImpl{ return &NavigationStrategyImpl{
visitedNodes: []*NodeContext{}, visitedNodes: []*NodeContext{},

View File

@ -2,7 +2,7 @@ package yqlib
import "gopkg.in/yaml.v3" import "gopkg.in/yaml.v3"
func ReadForMergeNavigationStrategy(deeplyTraverseArrays bool, overwriteArray bool) NavigationStrategy { func ReadForMergeNavigationStrategy(arrayMergeStrategy ArrayMergeStrategy) NavigationStrategy {
return &NavigationStrategyImpl{ return &NavigationStrategyImpl{
visitedNodes: []*NodeContext{}, visitedNodes: []*NodeContext{},
pathParser: NewPathParser(), pathParser: NewPathParser(),
@ -16,7 +16,7 @@ func ReadForMergeNavigationStrategy(deeplyTraverseArrays bool, overwriteArray bo
return nil return nil
}, },
shouldDeeplyTraverse: func(nodeContext NodeContext) bool { shouldDeeplyTraverse: func(nodeContext NodeContext) bool {
if nodeContext.Node.Kind == yaml.SequenceNode && overwriteArray { if nodeContext.Node.Kind == yaml.SequenceNode && arrayMergeStrategy == OverwriteArrayMergeStrategy {
nodeContext.IsMiddleNode = false nodeContext.IsMiddleNode = false
return false return false
} }
@ -31,7 +31,7 @@ func ReadForMergeNavigationStrategy(deeplyTraverseArrays bool, overwriteArray bo
isInArray = false isInArray = false
} }
} }
return deeplyTraverseArrays || !isInArray return arrayMergeStrategy == UpdateArrayMergeStrategy || !isInArray
}, },
} }
} }