Skip to content

Commit

Permalink
Implement change root support
Browse files Browse the repository at this point in the history
Add flags and implementation code to support changing the root level
of a given input file, or both. Depending on the actual object that
is referenced by the path that serves as the new root level, there
are use cases where a list should be translated into documents, or
where the list is used as the root level itself.
  • Loading branch information
HeavyWombat committed Apr 25, 2018
1 parent e7bf019 commit 36ffabb
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 5 deletions.
33 changes: 32 additions & 1 deletion internal/cmd/between.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ import (
var style string
var swap bool

var translateListToDocuments bool
var chroot string
var chrootFrom string
var chrootTo string

// betweenCmd represents the between command
var betweenCmd = &cobra.Command{
Use: "between",
Expand All @@ -57,6 +62,26 @@ document types are: YAML (http://yaml.org/) and JSON (http://json.org/).
exitWithError("Failed to load input files", err)
}

// If the main change root flag is set, this (re-)sets the individual change roots of the two input files
if chroot != "" {
chrootFrom = chroot
chrootTo = chroot
}

// Change root of from input file if change root flag for form is set
if chrootFrom != "" {
if err = dyff.ChangeRoot(&from, chrootFrom, translateListToDocuments); err != nil {
exitWithError(fmt.Sprintf("Failed to change root of %s to path %s", from.Location, chrootFrom), err)
}
}

// Change root of to input file if change root flag for to is set
if chrootTo != "" {
if err = dyff.ChangeRoot(&to, chrootTo, translateListToDocuments); err != nil {
exitWithError(fmt.Sprintf("Failed to change root of %s to path %s", to.Location, chrootTo), err)
}
}

report, err := dyff.CompareInputFiles(from, to)
if err != nil {
exitWithError("Failed to compare input files", err)
Expand Down Expand Up @@ -84,8 +109,14 @@ func init() {

// TODO Add flag for filter on path
betweenCmd.PersistentFlags().StringVarP(&style, "output", "o", "human", "Specify the output style, e.g. 'human' (more to come ...)")
betweenCmd.PersistentFlags().BoolVarP(&swap, "swap", "s", false, "Swap `from` and `to` for compare")
betweenCmd.PersistentFlags().BoolVarP(&swap, "swap", "s", false, "Swap 'from' and 'to' for comparison")

betweenCmd.PersistentFlags().BoolVarP(&dyff.NoTableStyle, "no-table-style", "t", false, "Disable the table output")
betweenCmd.PersistentFlags().BoolVarP(&dyff.DoNotInspectCerts, "no-cert-inspection", "c", false, "Disable certificate inspection (compare as raw text)")
betweenCmd.PersistentFlags().BoolVarP(&dyff.UseGoPatchPaths, "use-go-patch-style", "g", false, "Use Go-Patch style paths instead of Spruce Dot-Style")

betweenCmd.PersistentFlags().BoolVar(&translateListToDocuments, "chroot-list-to-documents", false, "usage chroot-list-to-documents")
betweenCmd.PersistentFlags().StringVar(&chroot, "chroot", "", "usage chroot")
betweenCmd.PersistentFlags().StringVar(&chrootFrom, "chroot-of-from", "", "usage chroot from")
betweenCmd.PersistentFlags().StringVar(&chrootTo, "chroot-of-to", "", "usage chroot ro")
}
35 changes: 35 additions & 0 deletions pkg/dyff/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,5 +637,40 @@ listY: [ Yo, Yo, Yo ]
}
})
})

Context("change root for comparison", func() {
It("should change the root of an input file", func() {
from := InputFile{Location: "/ginkgo/compare/test/from", Documents: loadTestDocuments(`---
a: foo
---
b: bar
`)}

to := InputFile{Location: "/ginkgo/compare/test/to", Documents: loadTestDocuments(`{
"items": [
{"a": "Foo"},
{"b": "Bar"}
]
}`)}

err := ChangeRoot(&to, "/items", true)
if err != nil {
Fail(err.Error())
}

results, err := CompareInputFiles(from, to)
Expect(err).To(BeNil())

expected := []Diff{
singleDiff("#0/a", MODIFICATION, "foo", "Foo"),
singleDiff("#1/b", MODIFICATION, "bar", "Bar"),
}

Expect(len(results.Diffs)).To(BeEquivalentTo(len(expected)))
for i, result := range results.Diffs {
Expect(result).To(BeEquivalentTo(expected[i]))
}
})
})
})
})
215 changes: 214 additions & 1 deletion pkg/dyff/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,11 @@ func getValueByKey(mapslice yaml.MapSlice, key string) (interface{}, error) {
}
}

return nil, fmt.Errorf("no map key %s found in %v", key, mapslice)
if names, err := ListStringKeys(mapslice); err == nil {
return nil, fmt.Errorf("no key '%s' found in map, available keys are: %s", key, strings.Join(names, ", "))
}

return nil, fmt.Errorf("no key '%s' found in map and also failed to get a list of key for this map", key)
}

// getEntryFromNamedList returns the entry that is identified by the identifier key and a name, for example: `name: one` where name is the identifier key and one the name. Function will return nil with bool false if there is no such entry.
Expand Down Expand Up @@ -667,6 +671,26 @@ func GetIdentifierFromNamedList(list []interface{}) string {
return ""
}

func listNamesOfNamedList(list []interface{}, identifier string) ([]string, error) {
result := make([]string, len(list))
for i, entry := range list {
switch entry.(type) {
case yaml.MapSlice:
value, err := getValueByKey(entry.(yaml.MapSlice), identifier)
if err != nil {
return nil, errors.Wrap(err, "unable to list names of a names list")
}

result[i] = value.(string)

default:
return nil, fmt.Errorf("unable to list names of a names list, because list entry #%d is not a YAML map but %s", i, typeToName(entry))
}
}

return result, nil
}

func createLookUpMap(list []interface{}) (map[uint64]int, error) {
result := make(map[uint64]int, len(list))
for idx, entry := range list {
Expand Down Expand Up @@ -740,3 +764,192 @@ func SimplifyList(input []yaml.MapSlice) []interface{} {

return result
}

// StringToPath creates a new Path using the provided serialized path string. In case of Spruce paths, we need the actual tree as a reference to create the correct path.
func StringToPath(path string, obj interface{}) (Path, error) {
elements := make([]PathElement, 0)

if strings.HasPrefix(path, "/") { // Go-path path in case it starts with a slash
for i, section := range strings.Split(path, "/") {
if i == 0 {
continue
}

keyNameSplit := strings.Split(section, "=")
switch len(keyNameSplit) {
case 1:
elements = append(elements, PathElement{Name: keyNameSplit[0]})

case 2:
elements = append(elements, PathElement{Key: keyNameSplit[0], Name: keyNameSplit[1]})

default:
return Path{}, fmt.Errorf("invalid Go-patch style path, element '%s' cannot contain more than one equal sign", section)
}
}

} else { // Spruce path
pointer := obj
for _, section := range strings.Split(path, ".") {
if isMapSlice(pointer) {
mapslice := pointer.(yaml.MapSlice)
value, err := getValueByKey(mapslice, section)
if err != nil {
return Path{}, errors.Wrap(err, "foobar #1")
}

pointer = value
elements = append(elements, PathElement{Name: section})

} else if isList(pointer) {
list := pointer.([]interface{})
if id, err := strconv.Atoi(section); err == nil {
if id < 0 || id >= len(list) {
return Path{}, fmt.Errorf("failed to parse path %s, provided list index %d is not in range: 0..%d", path, id, len(list)-1)
}

pointer = list[id]
elements = append(elements, PathElement{Name: section})

} else {
identifier := GetIdentifierFromNamedList(list)
value, ok := getEntryFromNamedList(list, identifier, section)
if !ok {
names, err := listNamesOfNamedList(list, identifier)
if err != nil {
return Path{}, fmt.Errorf("failed to parse path %s, provided named list entry '%s' cannot be found in list", path, section)
}

return Path{}, fmt.Errorf("failed to parse path %s, provided named list entry '%s' cannot be found in list, available names are: %s", path, section, strings.Join(names, ", "))
}

pointer = value
elements = append(elements, PathElement{Key: identifier, Name: section})
}
}
}
}

return Path{DocumentIdx: 0, PathElements: elements}, nil
}

func isList(obj interface{}) bool {
switch obj.(type) {
case []interface{}:
return true

default:
return false
}
}

func isMapSlice(obj interface{}) bool {
switch obj.(type) {
case yaml.MapSlice:
return true

default:
return false
}
}

// Grab get the value from the provided YAML tree using a path to traverse through the tree structure
func Grab(obj interface{}, pathString string) (interface{}, error) {
path, err := StringToPath(pathString, obj)
if err != nil {
return nil, err
}

pointer := obj
pointerPath := Path{DocumentIdx: path.DocumentIdx}

for _, element := range path.PathElements {
if element.Key != "" { // List
if !isList(pointer) {
return nil, fmt.Errorf("failed to traverse tree, expected a list but found type %s at %s", typeToName(pointer), ToGoPatchStyle(pointerPath, false))
}

entry, ok := getEntryFromNamedList(pointer.([]interface{}), element.Key, element.Name)
if !ok {
return nil, fmt.Errorf("there is no entry %s: %s in the list", element.Key, element.Name)
}

pointer = entry

} else if id, err := strconv.Atoi(element.Name); err == nil { // List (entry referenced by its index)
if !isList(pointer) {
return nil, fmt.Errorf("failed to traverse tree, expected a list but found type %s at %s", typeToName(pointer), ToGoPatchStyle(pointerPath, false))
}

list := pointer.([]interface{})
if id < 0 || id >= len(list) {
return nil, fmt.Errorf("failed to traverse tree, provided list index %d is not in range: 0..%d", id, len(list)-1)
}

pointer = list[id]

} else { // Map
if !isMapSlice(pointer) {
return nil, fmt.Errorf("failed to traverse tree, expected a YAML map but found type %s at %s", typeToName(pointer), ToGoPatchStyle(pointerPath, false))
}

entry, err := getValueByKey(pointer.(yaml.MapSlice), element.Name)
if err != nil {
return nil, err
}

pointer = entry
}

// Update the path that the current pointer has (only used in error case to point to the right position)
pointerPath.PathElements = append(pointerPath.PathElements, element)
}

return pointer, nil
}

// ChangeRoot changes the root of an input file to a position inside its document based on the given path. Input files with more than one document are not supported, since they could have multiple elements with that path.
func ChangeRoot(inputFile *InputFile, path string, translateListToDocuments bool) error {
if len(inputFile.Documents) != 1 {
return fmt.Errorf("change root for an input file is only possible if there is only one document, but %s contains %s",
inputFile.Location,
Plural(len(inputFile.Documents), "document"))
}

// Find the object at the given path
obj, err := Grab(inputFile.Documents[0], path)
if err != nil {
return err
}

if translateListToDocuments && isList(obj) {
// Change root of input file main document to a new list of documents based on the the list that was found
inputFile.Documents = obj.([]interface{})

} else {
// Change root of input file main document to the object that was found
inputFile.Documents = []interface{}{obj}
}

// Parse path string and create nicely formatted output path
if resolvedPath, err := StringToPath(path, obj); err == nil {
path = PathToString(resolvedPath, false)
}

inputFile.Note = fmt.Sprintf("YAML root was changed to %s", path)

return nil
}

func typeToName(obj interface{}) string {
switch obj.(type) {
case yaml.MapSlice:
return "YAML map"

case []yaml.MapSlice, []interface{}:
return "YAML list"

default:
return reflect.TypeOf(obj).Kind().String()
}
}
32 changes: 32 additions & 0 deletions pkg/dyff/core_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,35 @@ func compare(from interface{}, to interface{}) ([]Diff, error) {

return report.Diffs, nil
}

func loadTestDocuments(input string) []interface{} {
documents, err := LoadDocuments([]byte(input))
if err != nil {
Fail(err.Error())
}

return documents
}

func grab(obj interface{}, path string) interface{} {
value, err := Grab(obj, path)
if err != nil {
out, _ := ToYAMLString(obj)
Fail(fmt.Sprintf("Failed to grab by path %s from %s", path, out))
}

return value
}

func grabError(obj interface{}, path string) string {
value, err := Grab(obj, path)
Expect(value).To(BeNil())
return err.Error()
}

func pathFromString(path string, obj interface{}) Path {
result, err := StringToPath(path, obj)
Expect(err).To(BeNil())

return result
}
Loading

0 comments on commit 36ffabb

Please sign in to comment.