Select Git revision
audit.go 10.33 KiB
package audit
import (
"encoding/json"
"gitlab.com/uafrica/go-utils/errors"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/r3labs/diff/v2"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/reflection"
"gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils"
)
type FieldChange struct {
From interface{} `json:"change_from"`
To interface{} `json:"change_to"`
}
func VerifyAuditEvents(original interface{}, new interface{}) error {
if original != nil {
structValue := reflect.ValueOf(original)
if structValue.Kind() != reflect.Struct {
return errors.New("original object is not of type struct")
}
}
if new != nil {
structValue = reflect.ValueOf(new)
if structValue.Kind() != reflect.Struct {
return errors.New("new object is not of type struct")
}
}
return nil
}
func GetChanges(original interface{}, new interface{}) (map[string]interface{}, error) {
changes := map[string]interface{}{}
changelog, err := diff.Diff(original, new)
if err != nil {
return changes, err
}
for _, change := range changelog {
if len(change.Path) == 1 {
// Root object change
field := ToSnakeCase(change.Path[0])
changes[field] = FieldChange{
From: change.From,
To: change.To,
}
} else if len(change.Path) == 2 {
// Child object changed
// ["Account", "ID"]
// 0 = Object
// 1 = field
ChildObjectChanges(changes, change.Path[0], change.Path[1], change.From, change.To)
} else if len(change.Path) >= 3 {
// Array of objects
// ["Parcel", "0", "ActualWeight"]
// 0 = Object
// 1 = Index of object
// 2 = field
objectKey := ToSnakeCase(change.Path[0])
indexString := change.Path[1]
if !string_utils.IsNumericString(indexString) {
// Not an array, but a deeper nested object
ChildObjectChanges(changes, change.Path[len(change.Path)-2], change.Path[len(change.Path)-1], change.From, change.To)
continue
}
index, _ := string_utils.StringToInt64(indexString)
field := ToSnakeCase(change.Path[2])
arrayObject, present := changes[objectKey]
if present {
if arrayOfObjects, ok := arrayObject.([]map[string]interface{}); ok {
arrayIndex := ArrayIndexForObjectIndex(arrayOfObjects, index)
if arrayIndex != -1 {
// Add field to existing object in array
object := arrayOfObjects[arrayIndex]
object[field] = FieldChange{
From: change.From,
To: change.To,
}
} else {
// new object, append to existing array
fieldChange := map[string]interface{}{
"index": index,
field: FieldChange{
From: change.From,
To: change.To,
},
}
changes[objectKey] = append(arrayOfObjects, fieldChange)
}
}
} else {
// Create array of objects
fieldChange := map[string]interface{}{
"index": index,
field: FieldChange{
From: change.From,
To: change.To,
},
}
changes[objectKey] = []map[string]interface{}{
fieldChange,
}
}
}
}
return changes, nil
}
func ChildObjectChanges(changes map[string]interface{}, objectPath string, fieldPath string, changeFrom interface{}, changeTo interface{}) {
objectKey := ToSnakeCase(objectPath)
field := ToSnakeCase(fieldPath)
existingObject, present := changes[objectKey]
if present {
if object, ok := existingObject.(map[string]interface{}); ok {
object[field] = FieldChange{
From: changeFrom,
To: changeTo,
}
changes[objectKey] = object
}
} else {
fieldChange := map[string]interface{}{
field: FieldChange{
From: changeFrom,
To: changeTo,
},
}
changes[objectKey] = fieldChange
}
}
// ArrayIndexForObjectIndex gets the index of arrayOfObjects where the object's index field is equal to objectIndex.
func ArrayIndexForObjectIndex(arrayOfObjects []map[string]interface{}, objectIndex int64) int {
for arrayIndex, object := range arrayOfObjects {
index, present := object["index"]
if present {
if index == objectIndex {
return arrayIndex
}
}
}
return -1
}
// 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 _, convErr := strconv.Atoi(change.Path[pathIndex]); convErr == 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 {
// Prevent finding nested keys as opposed to our "change" keys
opener := changesJson[index+len(split)+2 : index+len(split)+3]
// Prevent a "base level" key from matching a nested key
baseLevel := false
if index > 1 {
containedBy := changesJson[index-1 : index]
if levelsDeep == 0 && (containedBy == "{" || containedBy == "[") {
baseLevel = true
}
}
if (opener == "{" || opener == "[") && !baseLevel {
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])")
func ToSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}
func GetIntValue(object interface{}, key string) int64 {
structValue := reflect.ValueOf(object)
if structValue.Kind() == reflect.Struct {
field := structValue.FieldByName(key)
id := reflection.GetInt64Value(field)
return id
}
return 0
}
func GetStringValue(object interface{}, key string) string {
structValue := reflect.ValueOf(object)
if structValue.Kind() == reflect.Struct {
field := structValue.FieldByName(key)
id := reflection.GetStringValue(field)
return id
}
return ""
}