utils/json.go (304 lines of code) (raw):
package utils
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"strings"
jmes "github.com/jmespath/go-jmespath"
)
func NormalizeJson(jsonString interface{}) string {
if jsonString == nil || jsonString == "" {
return ""
}
var j interface{}
if err := json.Unmarshal([]byte(jsonString.(string)), &j); err != nil {
return fmt.Sprintf("Error parsing JSON: %+v", err)
}
b, _ := json.Marshal(j)
return string(b)
}
// MergeObject is used to merge object old and new, if overlaps, use new value
func MergeObject(old interface{}, new interface{}) interface{} {
if new == nil {
return new
}
switch oldValue := old.(type) {
case map[string]interface{}:
if newMap, ok := new.(map[string]interface{}); ok {
res := make(map[string]interface{})
for key, value := range oldValue {
if _, ok := newMap[key]; ok {
res[key] = MergeObject(value, newMap[key])
} else {
res[key] = value
}
}
for key, newValue := range newMap {
if res[key] == nil {
res[key] = newValue
}
}
return res
}
case []interface{}:
if newArr, ok := new.([]interface{}); ok {
if len(oldValue) != len(newArr) {
return newArr
}
res := make([]interface{}, 0)
for index := range oldValue {
res = append(res, MergeObject(oldValue[index], newArr[index]))
}
return res
}
}
return new
}
type UpdateJsonOption struct {
IgnoreCasing bool
IgnoreMissingProperty bool
}
// UpdateObject is used to get an updated object which has same schema as old, but with new value
func UpdateObject(old interface{}, new interface{}, option UpdateJsonOption) interface{} {
if reflect.DeepEqual(old, new) {
return old
}
switch oldValue := old.(type) {
case map[string]interface{}:
if newMap, ok := new.(map[string]interface{}); ok {
res := make(map[string]interface{})
for key, value := range oldValue {
switch {
case newMap[key] != nil:
res[key] = UpdateObject(value, newMap[key], option)
case option.IgnoreMissingProperty || isZeroValue(value):
res[key] = value
}
}
return res
}
case []interface{}:
if newArr, ok := new.([]interface{}); ok {
if len(oldValue) == 0 {
return new
}
hasIdentifier := identifierOfArrayItem(oldValue[0]) != ""
if !hasIdentifier {
if len(oldValue) != len(newArr) {
return newArr
}
res := make([]interface{}, 0)
for index := range oldValue {
res = append(res, UpdateObject(oldValue[index], newArr[index], option))
}
return res
}
res := make([]interface{}, 0)
used := make([]bool, len(newArr))
for _, oldItem := range oldValue {
found := false
for index, newItem := range newArr {
if reflect.DeepEqual(oldItem, newItem) && !used[index] {
res = append(res, UpdateObject(oldItem, newItem, option))
used[index] = true
found = true
break
}
}
if found {
continue
}
for index, newItem := range newArr {
if areSameArrayItems(oldItem, newItem) && !used[index] {
res = append(res, UpdateObject(oldItem, newItem, option))
used[index] = true
break
}
}
}
for index, newItem := range newArr {
if !used[index] {
res = append(res, newItem)
}
}
return res
}
case string:
if newStr, ok := new.(string); ok {
if option.IgnoreCasing && strings.EqualFold(oldValue, newStr) {
return oldValue
}
if option.IgnoreMissingProperty && (regexp.MustCompile(`^\*+$`).MatchString(newStr) || "<redacted>" == newStr || "" == newStr) {
return oldValue
}
}
}
return new
}
func areSameArrayItems(a, b interface{}) bool {
aId := identifierOfArrayItem(a)
bId := identifierOfArrayItem(b)
if aId == "" || bId == "" {
return false
}
return aId == bId
}
func identifierOfArrayItem(input interface{}) string {
inputMap, ok := input.(map[string]interface{})
if !ok {
return ""
}
name := inputMap["name"]
if name == nil {
return ""
}
nameValue, ok := name.(string)
if !ok {
return ""
}
return nameValue
}
// ExtractObject is used to extract object from old for a json path
func ExtractObject(old interface{}, path string) interface{} {
if len(path) == 0 {
return old
}
if oldMap, ok := old.(map[string]interface{}); ok {
index := strings.Index(path, ".")
if index != -1 {
key := path[0:index]
result := make(map[string]interface{}, 1)
value := ExtractObject(oldMap[key], path[index+1:])
if value == nil {
return nil
} else {
result[key] = value
}
return result
} else {
if oldMap[path] != nil {
result := make(map[string]interface{}, 1)
result[path] = oldMap[path]
return result
} else {
return nil
}
}
}
return nil
}
// ExtractObjectJMES is used to extract object from old using JMES path
func ExtractObjectJMES(old interface{}, pathKey, path string) interface{} {
result := make(map[string]interface{}, 1)
value, err := jmes.Search(path, old)
if err != nil {
return nil
}
result[pathKey] = value
return result
}
// OverrideWithPaths is used to override old object with new object for specific paths
func OverrideWithPaths(old interface{}, new interface{}, path string, pathSet map[string]bool) (interface{}, error) {
if len(pathSet) == 0 || old == nil {
return old, nil
}
if _, ok := pathSet[path]; ok {
return new, nil
}
switch oldValue := old.(type) {
case map[string]interface{}:
if newMap, ok := new.(map[string]interface{}); ok {
outMap := make(map[string]interface{})
for key, value := range oldValue {
if newValue, ok := newMap[key]; ok {
nestedPath := strings.TrimPrefix(path+"."+key, ".")
out, err := OverrideWithPaths(value, newValue, nestedPath, pathSet)
if err != nil {
return nil, err
}
outMap[key] = out
} else {
outMap[key] = value
}
}
return outMap, nil
}
case []interface{}:
// Does not support override specific item in list
for v := range pathSet {
if strings.HasPrefix(v, path+".") {
return nil, fmt.Errorf("ignoring specific item in list is not supported")
}
}
if newArr, ok := new.([]interface{}); ok && pathSet[path] {
return mergeArray(oldValue, newArr), nil
}
default:
}
return old, nil
}
// mergeArray is used to merge two array, if overlaps, use old value. `name` is used as key to compare
func mergeArray(old []interface{}, new []interface{}) []interface{} {
oldMap := make(map[string]interface{})
for _, v := range old {
if vMap, ok := v.(map[string]interface{}); ok {
if name, ok := vMap["name"]; ok {
oldMap[name.(string)] = v
}
}
}
out := make([]interface{}, 0)
for _, v := range new {
if vMap, ok := v.(map[string]interface{}); ok {
if name, ok := vMap["name"]; ok {
if oldV, ok := oldMap[name.(string)]; ok {
out = append(out, oldV)
continue
}
}
}
out = append(out, v)
}
return out
}
// NormalizeObject is used to remove customized type and replaced with builtin type
func NormalizeObject(input interface{}) interface{} {
jsonString, _ := json.Marshal(input)
var output interface{}
_ = json.Unmarshal(jsonString, &output)
return output
}
// RemoveFields is used to remove fields from input
func RemoveFields(input interface{}, fields []string) interface{} {
if input == nil {
return input
}
switch v := input.(type) {
case map[string]interface{}:
for _, field := range fields {
delete(v, field)
}
for key, value := range v {
v[key] = RemoveFields(value, fields)
}
return v
case []interface{}:
res := make([]interface{}, 0)
for _, item := range v {
res = append(res, RemoveFields(item, fields))
}
return res
default:
return input
}
}
func isZeroValue(value interface{}) bool {
if value == nil {
return true
}
switch v := value.(type) {
case map[string]interface{}:
return len(v) == 0
case []interface{}:
return len(v) == 0
case string:
return len(v) == 0
case int, int32, int64, float32, float64:
return v == 0
case bool:
return !v
}
return false
}