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  }