diff --git a/audit/audit.go b/audit/audit.go index 6146f093cf995d81a78bb134a07b1ee7c03f07d9..901ec195a93e3b59bf08b31628f0653269f58dcb 100644 --- a/audit/audit.go +++ b/audit/audit.go @@ -1,13 +1,14 @@ package audit import ( - "reflect" - "regexp" - "strings" - + "encoding/json" "github.com/r3labs/diff/v2" "gitlab.com/uafrica/go-utils/reflection" "gitlab.com/uafrica/go-utils/string_utils" + "reflect" + "regexp" + "strconv" + "strings" ) type FieldChange struct { @@ -111,6 +112,151 @@ func GetChanges(original interface{}, new interface{}) (map[string]interface{}, return changes, nil } +// GetAllChanges Returns the diff, structured in json, recursively +// Be warned, here be dragons. Debug this first to understand how it works +func GetAllChanges(original interface{}, new interface{}) (map[string]interface{}, error) { + changes := map[string]interface{}{} + changelog, err := diff.Diff(original, new) + if err != nil { + return changes, err + } + + changesJson := "{" + subArrays := map[string]string{} + for _, change := range changelog { + var value string // Keep track of the core value + var key string // Keep track of the key/s for this value + var closing string // Keep track of the correct number of closing characters + + for pathIndex := len(change.Path) - 1; pathIndex >= 0; pathIndex-- { + // If this is the first value, it's the "deepest" value of this change set, otherwise leave this var alone + // If this is set at the wrong point, other behaviour changes + if pathIndex+1 == len(change.Path) { + changedField := FieldChange{ + From: change.From, + To: change.To, + } + request, err := json.Marshal(changedField) + if err != nil { + return nil, err + } + value = string(request) + value = value[1 : len(value)-1] + } + + // If this "key" is integer-like, we handle it a little differently to ensure related data ends up + // together and formatted as you would expect JSON arrays to look + if _, err = strconv.Atoi(change.Path[pathIndex]); err == nil { + positionOfSubArray := "" + for sub := 0; sub <= pathIndex; sub++ { + positionOfSubArray = positionOfSubArray + change.Path[sub] + } + + // Add it to the placeholder data map used later on + subArrays[positionOfSubArray] = subArrays[positionOfSubArray] + key + value + closing + "," + + // Don't insert the same placeholder into the json multiple times + if !strings.Contains(changesJson, positionOfSubArray) { + value = positionOfSubArray + } else { + // Make sure this value doesn't end up populated explicitly + value = "" + } + } else { + // Safety net so we don't compare to a non existant value + if pathIndex < len(change.Path)-1 { + // If the value this one "contains" has a integer-like key, this is probably an array + if _, err = strconv.Atoi(change.Path[pathIndex+1]); err == nil { + key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": [" + closing = "]" + } else { + key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": {" + key + closing = closing + "}" + } + } else { + key = "\"" + ToSnakeCase(change.Path[pathIndex]) + "\": {" + key + closing = closing + "}" + } + } + } + + // Don't insert empty values (happens when multiple values within an array are set, see "placeholder" behavior) + if value != "" { + // Duplicate key prevention + keySplit := strings.Split(key, ": ") + lastOccurrence := 0 + lastMatched := "" + levelsDeep := 0 + for sIndex, split := range keySplit { + // Trim leading character off split + if split[0:1] == "{" || split[0:1] == "[" { + split = split[1:] + } + + // The final value of keySplit might be empty ("") + if split != "" { + if index := strings.Index(changesJson, split); index > -1 && changesJson != "{" { + // Prevent reverse traversal + if index > lastOccurrence { + lastOccurrence = index + lastMatched = split + levelsDeep = sIndex + } + } + } + } + + // If the "key" is already present, handle it differently + if lastOccurrence > 0 { + // Strip parent keys that are already present + key = key[strings.Index(key, lastMatched)+len(lastMatched):] + if key[0:2] == ": " { + key = key[2:] + } + // Strip the correct amount of closing tags + closing = closing[:len(closing)-levelsDeep] + + // Find the position of this key in the master string + position := strings.Index(changesJson, lastMatched) + len(lastMatched) + 3 + + // We're appending to an existing object, so strip the outermost wrapping layer + key = key[1:] + closing = closing[:len(closing)-1] + + // Place the value within the existing key + changesJson = changesJson[:position] + key + value + closing + ", " + changesJson[position:] + } else { + // No value found, append it to the end + changesJson = changesJson + key + value + closing + ", " + } + } + } + // Trim whitespace and strip trailing comma since we are done inserting at the back + changesJson = strings.TrimSpace(changesJson) + if changesJson[len(changesJson)-1:] == "," { + changesJson = changesJson[:len(changesJson)-1] + } + changesJson = changesJson + "}" + + // Now we can go make sure the placeholders are populated with the data in + for placeholderKey, placeholderValue := range subArrays { + // Trim the trailing comma since we won't be adding any more stuff + placeholderValue = strings.TrimSpace(placeholderValue) + if placeholderValue[len(placeholderValue)-1:] == "," { + placeholderValue = placeholderValue[:len(placeholderValue)-1] + } + changesJson = strings.ReplaceAll(changesJson, placeholderKey, "{"+placeholderValue+"}") + } + + // Now, hopefully, the json parsing will pass and we have a nicely formatted dataset + err = json.Unmarshal([]byte(changesJson), &changes) + if err != nil { + return nil, err + } + + return changes, nil +} + var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")