github.com/LukasHeimann/cloudfoundrycli/v8@v8.4.4/command/v7/shared/manifest_diff_displayer.go (about) 1 package shared 2 3 import ( 4 "path" 5 "reflect" 6 "strconv" 7 "strings" 8 9 "github.com/LukasHeimann/cloudfoundrycli/v8/command" 10 "github.com/LukasHeimann/cloudfoundrycli/v8/resources" 11 "github.com/pkg/errors" 12 "gopkg.in/yaml.v2" 13 ) 14 15 type ManifestDiffDisplayer struct { 16 UI command.UI 17 } 18 19 func NewManifestDiffDisplayer(ui command.UI) *ManifestDiffDisplayer { 20 return &ManifestDiffDisplayer{ 21 UI: ui, 22 } 23 } 24 25 func (display *ManifestDiffDisplayer) DisplayDiff(rawManifest []byte, diff resources.ManifestDiff) error { 26 // If there are no diffs, just print the manifest 27 if len(diff.Diffs) == 0 { 28 display.UI.DisplayDiffUnchanged(string(rawManifest), 0, false) 29 return nil 30 } 31 32 // We unmarshal into a MapSlice vs a map[string]interface{} here to preserve the order of map keys 33 var yamlManifest yaml.MapSlice 34 err := yaml.Unmarshal(rawManifest, &yamlManifest) 35 if err != nil { 36 return errors.New("Unable to process manifest diff because its format is invalid.") 37 } 38 39 // Distinguish between add/remove and replace diffs 40 // Replace diffs have the key in the given manifest so we can navigate to that path and display the changed value 41 // Add/Remove diffs will not, so we need to know their parent's path to insert the diff as a child 42 pathAddReplaceMap := make(map[string]resources.Diff) 43 pathRemoveMap := make(map[string]resources.Diff) 44 for _, diff := range diff.Diffs { 45 switch diff.Op { 46 case resources.AddOperation, resources.ReplaceOperation: 47 pathAddReplaceMap[diff.Path] = diff 48 case resources.RemoveOperation: 49 pathRemoveMap[path.Dir(diff.Path)] = diff 50 } 51 } 52 53 // Always display the yaml header line 54 display.UI.DisplayDiffUnchanged("---", 0, false) 55 56 // For each entry in the provided manifest, process any diffs at or below that entry 57 for _, entry := range yamlManifest { 58 display.processDiffsRecursively("/"+interfaceToString(entry.Key), entry.Value, 0, &pathAddReplaceMap, &pathRemoveMap, false) 59 } 60 61 return nil 62 } 63 64 func (display *ManifestDiffDisplayer) processDiffsRecursively( 65 currentManifestPath string, 66 value interface{}, 67 depth int, 68 pathAddReplaceMap, pathRemoveMap *map[string]resources.Diff, 69 addHyphen bool, 70 ) { 71 72 field := path.Base(currentManifestPath) 73 // If there is a diff at the current path, print it 74 if diff, ok := diffExistsAtTheCurrentPath(currentManifestPath, pathAddReplaceMap); ok { 75 display.formatDiff(field, diff, depth, addHyphen) 76 return 77 } 78 // If the value is a slice type (i.e. a yaml.MapSlice or a slice), recurse into it 79 if isSliceType(value) { 80 addHyphen = isInt(field) 81 if !isInt(field) { 82 display.UI.DisplayDiffUnchanged(field+":", depth, addHyphen) 83 depth += 1 84 } 85 86 if mapSlice, ok := value.(yaml.MapSlice); ok { 87 // If a map, recursively process each entry 88 for i, entry := range mapSlice { 89 if i > 0 { 90 addHyphen = false 91 } 92 display.processDiffsRecursively( 93 currentManifestPath+"/"+interfaceToString(entry.Key), 94 entry.Value, 95 depth, 96 pathAddReplaceMap, pathRemoveMap, addHyphen, 97 ) 98 } 99 } else if asSlice, ok := value.([]interface{}); ok { 100 // If a slice, recursively process each element 101 for index, sliceValue := range asSlice { 102 display.processDiffsRecursively( 103 currentManifestPath+"/"+strconv.Itoa(index), 104 sliceValue, 105 depth, 106 pathAddReplaceMap, pathRemoveMap, false, 107 ) 108 } 109 } 110 111 // Print add/remove diffs after printing the rest of the map or slice 112 if diff, ok := diffExistsAtTheCurrentPath(currentManifestPath, pathRemoveMap); ok { 113 display.formatDiff(path.Base(diff.Path), diff, depth, addHyphen) 114 } 115 116 return 117 } 118 119 // Otherwise, print the unchanged field and value 120 display.UI.DisplayDiffUnchanged(formatKeyValue(field, value), depth, addHyphen) 121 } 122 123 func (display *ManifestDiffDisplayer) formatDiff(field string, diff resources.Diff, depth int, addHyphen bool) { 124 addHyphen = isInt(field) || addHyphen 125 switch diff.Op { 126 case resources.AddOperation: 127 display.UI.DisplayDiffAddition(formatKeyValue(field, diff.Value), depth, addHyphen) 128 129 case resources.ReplaceOperation: 130 display.UI.DisplayDiffRemoval(formatKeyValue(field, diff.Was), depth, addHyphen) 131 display.UI.DisplayDiffAddition(formatKeyValue(field, diff.Value), depth, addHyphen) 132 133 case resources.RemoveOperation: 134 display.UI.DisplayDiffRemoval(formatKeyValue(field, diff.Was), depth, addHyphen) 135 } 136 } 137 138 func diffExistsAtTheCurrentPath(currentManifestPath string, pathDiffMap *map[string]resources.Diff) (resources.Diff, bool) { 139 diff, ok := (*pathDiffMap)[currentManifestPath] 140 return diff, ok 141 } 142 143 func formatKeyValue(key string, value interface{}) string { 144 if isInt(key) { 145 return interfaceToString(value) 146 } 147 if isSliceType(value) { 148 return key + ":\n" + interfaceToString(value) 149 } 150 if isMapType(value) { 151 return key + ":\n" + indentOneLevelDeeper(interfaceToString(value)) 152 } 153 return key + ": " + interfaceToString(value) 154 } 155 156 func interfaceToString(value interface{}) string { 157 val, _ := yaml.Marshal(value) 158 return strings.TrimSpace(string(val)) 159 } 160 161 func indentOneLevelDeeper(input string) string { 162 inputLines := strings.Split(input, "\n") 163 outputLines := make([]string, len(inputLines)) 164 165 for i, line := range inputLines { 166 outputLines[i] = " " + line 167 } 168 169 return strings.Join(outputLines, "\n") 170 } 171 172 func isSliceType(value interface{}) bool { 173 valueType := reflect.TypeOf(value) 174 return valueType != nil && valueType.Kind() == reflect.Slice 175 } 176 177 func isMapType(value interface{}) bool { 178 valueType := reflect.TypeOf(value) 179 return valueType != nil && valueType.Kind() == reflect.Map 180 } 181 182 func isInt(field string) bool { 183 _, err := strconv.Atoi(field) 184 return err == nil 185 }