Add method to create strategic merge patch.

pull/6/head
jackgr 2015-08-31 12:43:46 -07:00
parent 92f21b3fe3
commit 3ecb1bac83
2 changed files with 991 additions and 338 deletions

View File

@ -31,28 +31,357 @@ import (
// lists should be merged or replaced.
//
// For more information, see the PATCH section of docs/api-conventions.md.
func StrategicMergePatchData(original, patch []byte, dataStruct interface{}) ([]byte, error) {
var o map[string]interface{}
err := json.Unmarshal(original, &o)
//
// Some of the content of this package was borrowed with minor adaptations from
// evanphx/json-patch and openshift/origin.
const specialKey = "$patch"
const specialValue = "delete"
var errBadJSONDoc = fmt.Errorf("Invalid JSON document")
var errNoListOfLists = fmt.Errorf("Lists of lists are not supported")
// CreateStrategicMergePatch creates a patch that can be passed to StrategicMergePatch.
// The original and modified documents must be passed to the method as json encoded content.
// It will return a mergeable json document with differences from original to modified, or an error
// if either of the two documents is invalid.
func CreateStrategicMergePatch(original, modified []byte, dataStruct interface{}) ([]byte, error) {
originalMap := map[string]interface{}{}
err := json.Unmarshal(original, &originalMap)
if err != nil {
return nil, errBadJSONDoc
}
modifiedMap := map[string]interface{}{}
err = json.Unmarshal(modified, &modifiedMap)
if err != nil {
return nil, errBadJSONDoc
}
t, err := getTagStructType(dataStruct)
if err != nil {
return nil, err
}
var p map[string]interface{}
err = json.Unmarshal(patch, &p)
patchMap, err := diffMaps(originalMap, modifiedMap, t, false, false)
if err != nil {
return nil, err
}
t := reflect.TypeOf(dataStruct)
return json.Marshal(patchMap)
}
// Returns a (recursive) strategic merge patch that yields modified when applied to original.
func diffMaps(original, modified map[string]interface{}, t reflect.Type, ignoreAdditions, ignoreChangesAndDeletions bool) (map[string]interface{}, error) {
patch := map[string]interface{}{}
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf("strategic merge patch needs a struct, %s received instead", t.Kind().String())
for key, modifiedValue := range modified {
originalValue, ok := original[key]
// value was added
if !ok {
if !ignoreAdditions {
patch[key] = modifiedValue
}
continue
}
if key == specialKey {
originalString, ok := originalValue.(string)
if !ok {
return nil, fmt.Errorf("invalid value for special key: %s", specialKey)
}
modifiedString, ok := modifiedValue.(string)
if !ok {
return nil, fmt.Errorf("invalid value for special key: %s", specialKey)
}
if modifiedString != originalString {
patch[key] = modifiedValue
}
continue
}
if !ignoreChangesAndDeletions {
// If types have changed, replace completely
if reflect.TypeOf(originalValue) != reflect.TypeOf(modifiedValue) {
patch[key] = modifiedValue
continue
}
}
// Types are the same, compare values
switch originalValueTyped := originalValue.(type) {
case map[string]interface{}:
modifiedValueTyped := modifiedValue.(map[string]interface{})
fieldType, _, _, err := forkedjson.LookupPatchMetadata(t, key)
if err != nil {
return nil, err
}
patchValue, err := diffMaps(originalValueTyped, modifiedValueTyped, fieldType, ignoreAdditions, ignoreChangesAndDeletions)
if err != nil {
return nil, err
}
if len(patchValue) > 0 {
patch[key] = patchValue
}
continue
case []interface{}:
modifiedValueTyped := modifiedValue.([]interface{})
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, key)
if err != nil {
return nil, err
}
if fieldPatchStrategy == "merge" {
patchValue, err := diffLists(originalValueTyped, modifiedValueTyped, fieldType.Elem(), fieldPatchMergeKey, ignoreAdditions, ignoreChangesAndDeletions)
if err != nil {
return nil, err
}
if len(patchValue) > 0 {
patch[key] = patchValue
}
continue
}
}
if !ignoreChangesAndDeletions {
if !reflect.DeepEqual(originalValue, modifiedValue) {
patch[key] = modifiedValue
}
}
}
result, err := mergeMap(o, p, t)
if !ignoreChangesAndDeletions {
// Now add all deleted values as nil
for key := range original {
_, found := modified[key]
if !found {
patch[key] = nil
}
}
}
return patch, nil
}
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists with merge semantics.
func diffLists(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreAdditions, ignoreChangesAndDeletions bool) ([]interface{}, error) {
if len(original) == 0 {
if len(modified) == 0 || ignoreAdditions {
return nil, nil
}
return modified, nil
}
elementType, err := sliceElementType(original, modified)
if err != nil {
return nil, err
}
var patch []interface{}
// If the elements are not maps...
if elementType.Kind() == reflect.Map {
patch, err = diffListsOfMaps(original, modified, t, mergeKey, ignoreAdditions, ignoreChangesAndDeletions)
} else {
patch, err = diffListsOfScalars(original, modified, ignoreAdditions)
}
if err != nil {
return nil, err
}
return patch, nil
}
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists of scalars with merge semantics.
func diffListsOfScalars(original, modified []interface{}, ignoreAdditions bool) ([]interface{}, error) {
if len(modified) == 0 {
// There is no need to check the length of original because there is no way to create
// a patch that deletes a scalar from a list of scalars with merge semantics.
return nil, nil
}
patch := []interface{}{}
originalScalars := uniqifyAndSortScalars(original)
modifiedScalars := uniqifyAndSortScalars(modified)
originalIndex, modifiedIndex := 0, 0
loopB:
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
for ; originalIndex < len(originalScalars); originalIndex++ {
originalString := fmt.Sprintf("%v", original[originalIndex])
modifiedString := fmt.Sprintf("%v", modified[modifiedIndex])
if originalString >= modifiedString {
if originalString != modifiedString {
if !ignoreAdditions {
patch = append(patch, modified[modifiedIndex])
}
}
continue loopB
}
// There is no else clause because there is no way to create a patch that deletes
// a scalar from a list of scalars with merge semantics.
}
break
}
if !ignoreAdditions {
// Add any remaining items found only in modified
for ; modifiedIndex < len(modifiedScalars); modifiedIndex++ {
patch = append(patch, modified[modifiedIndex])
}
}
return patch, nil
}
var errNoMergeKeyFmt = "map: %v does not contain declared merge key: %s"
var errBadArgTypeFmt = "expected a %s, but received a %t"
// Returns a (recursive) strategic merge patch that yields modified when applied to original,
// for a pair of lists of maps with merge semantics.
func diffListsOfMaps(original, modified []interface{}, t reflect.Type, mergeKey string, ignoreAdditions, ignoreChangesAndDeletions bool) ([]interface{}, error) {
patch := make([]interface{}, 0)
originalSorted, err := sortMergeListsByNameArray(original, t, mergeKey, false)
if err != nil {
return nil, err
}
modifiedSorted, err := sortMergeListsByNameArray(modified, t, mergeKey, false)
if err != nil {
return nil, err
}
originalIndex, modifiedIndex := 0, 0
loopB:
for ; modifiedIndex < len(modifiedSorted); modifiedIndex++ {
modifiedMap, ok := modifiedSorted[modifiedIndex].(map[string]interface{})
if !ok {
return nil, fmt.Errorf(errBadArgTypeFmt, "map[string]interface{}", modifiedSorted[modifiedIndex])
}
modifiedValue, ok := modifiedMap[mergeKey]
if !ok {
return nil, fmt.Errorf(errNoMergeKeyFmt, modifiedMap, mergeKey)
}
for ; originalIndex < len(originalSorted); originalIndex++ {
originalMap, ok := originalSorted[originalIndex].(map[string]interface{})
if !ok {
return nil, fmt.Errorf(errBadArgTypeFmt, "map[string]interface{}", originalSorted[originalIndex])
}
originalValue, ok := originalMap[mergeKey]
if !ok {
return nil, fmt.Errorf(errNoMergeKeyFmt, originalMap, mergeKey)
}
// Assume that the merge key values are comparable strings
originalString := fmt.Sprintf("%v", originalValue)
modifiedString := fmt.Sprintf("%v", modifiedValue)
if originalString >= modifiedString {
if originalString == modifiedString {
patchValue, err := diffMaps(originalMap, modifiedMap, t, ignoreAdditions, ignoreChangesAndDeletions)
if err != nil {
return nil, err
}
originalIndex++
if len(patchValue) > 0 {
patchValue[mergeKey] = modifiedValue
patch = append(patch, patchValue)
}
} else if !ignoreAdditions {
patch = append(patch, modifiedMap)
}
continue loopB
}
if !ignoreChangesAndDeletions {
patch = append(patch, map[string]interface{}{mergeKey: originalValue, specialKey: specialValue})
}
}
break
}
if !ignoreChangesAndDeletions {
// Delete any remaining items found only in original
for ; originalIndex < len(originalSorted); originalIndex++ {
originalMap, ok := originalSorted[originalIndex].(map[string]interface{})
if !ok {
return nil, fmt.Errorf(errBadArgTypeFmt, "map[string]interface{}", originalSorted[originalIndex])
}
originalValue, ok := originalMap[mergeKey]
if !ok {
return nil, fmt.Errorf(errNoMergeKeyFmt, originalMap, mergeKey)
}
patch = append(patch, map[string]interface{}{mergeKey: originalValue, specialKey: specialValue})
}
}
if !ignoreAdditions {
// Add any remaining items found only in modified
for ; modifiedIndex < len(modifiedSorted); modifiedIndex++ {
patch = append(patch, modified[modifiedIndex])
}
}
return patch, nil
}
// StrategicMergePatchData applies a patch using strategic merge patch semantics.
// Deprecated: StrategicMergePatchData is deprecated. Use the synonym StrategicMergePatch,
// instead, which follows the naming convention of evanphx/json-patch.
func StrategicMergePatchData(original, patch []byte, dataStruct interface{}) ([]byte, error) {
return StrategicMergePatch(original, patch, dataStruct)
}
// StrategicMergePatch applies a strategic merge patch. The patch and the original document
// must be json encoded content. A patch can be created from an original and a modified document
// by calling CreateStrategicMergePatch.
func StrategicMergePatch(original, patch []byte, dataStruct interface{}) ([]byte, error) {
originalMap := map[string]interface{}{}
err := json.Unmarshal(original, &originalMap)
if err != nil {
return nil, errBadJSONDoc
}
patchMap := map[string]interface{}{}
err = json.Unmarshal(patch, &patchMap)
if err != nil {
return nil, errBadJSONDoc
}
t, err := getTagStructType(dataStruct)
if err != nil {
return nil, err
}
result, err := mergeMap(originalMap, patchMap, t)
if err != nil {
return nil, err
}
@ -60,7 +389,20 @@ func StrategicMergePatchData(original, patch []byte, dataStruct interface{}) ([]
return json.Marshal(result)
}
const specialKey = "$patch"
func getTagStructType(dataStruct interface{}) (reflect.Type, error) {
t := reflect.TypeOf(dataStruct)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return nil, fmt.Errorf(errBadArgTypeFmt, "struct", t.Kind().String())
}
return t, nil
}
var errBadPatchTypeFmt = "unknown patch type: %s in map: %v"
// Merge fields from a patch map into the original map. Note: This may modify
// both the original map and the patch because getting a deep copy of a map in
@ -74,7 +416,8 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
delete(patch, specialKey)
return patch, nil
}
return nil, fmt.Errorf("unknown patch type found: %s", v)
return nil, fmt.Errorf(errBadPatchTypeFmt, v, patch)
}
// nil is an accepted value for original to simplify logic in other places.
@ -92,29 +435,32 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
if _, ok := original[k]; ok {
delete(original, k)
}
continue
}
_, ok := original[k]
if !ok {
// If it's not in the original document, just take the patch value.
original[k] = patchV
continue
}
// If the data type is a pointer, resolve the element.
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
// If they're both maps or lists, recurse into the value.
// First find the fieldPatchStrategy and fieldPatchMergeKey.
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
originalType := reflect.TypeOf(original[k])
patchType := reflect.TypeOf(patchV)
if originalType == patchType {
// First find the fieldPatchStrategy and fieldPatchMergeKey.
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
if originalType.Kind() == reflect.Map && fieldPatchStrategy != "replace" {
typedOriginal := original[k].(map[string]interface{})
typedPatch := patchV.(map[string]interface{})
@ -123,8 +469,10 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
if err != nil {
return nil, err
}
continue
}
if originalType.Kind() == reflect.Slice && fieldPatchStrategy == "merge" {
elemType := fieldType.Elem()
typedOriginal := original[k].([]interface{})
@ -134,6 +482,7 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type) (map[strin
if err != nil {
return nil, err
}
continue
}
}
@ -154,15 +503,13 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
if len(original) == 0 && len(patch) == 0 {
return original, nil
}
// All the values must be of the same type, but not a list.
t, err := sliceElementType(original, patch)
if err != nil {
return nil, fmt.Errorf("types of list elements need to be the same, type: %s: %v",
elemType.Kind().String(), err)
}
if t.Kind() == reflect.Slice {
return nil, fmt.Errorf("not supporting merging lists of lists yet")
return nil, err
}
// If the elements are not maps, merge the slices of scalars.
if t.Kind() != reflect.Map {
// Maybe in the future add a "concat" mode that doesn't
@ -182,10 +529,14 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
typedV := v.(map[string]interface{})
patchType, ok := typedV[specialKey]
if ok {
if patchType == "delete" {
if patchType == specialValue {
mergeValue, ok := typedV[mergeKey]
if ok {
_, originalKey, found := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
_, originalKey, found, err := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
if err != nil {
return nil, err
}
if found {
// Delete the element at originalKey.
original = append(original[:originalKey], original[originalKey+1:]...)
@ -199,7 +550,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
} else if patchType == "merge" {
return nil, fmt.Errorf("merging lists cannot yet be specified in the patch")
} else {
return nil, fmt.Errorf("unknown patch type found: %s", patchType)
return nil, fmt.Errorf(errBadPatchTypeFmt, patchType, typedV)
}
} else {
patchWithoutSpecialElements = append(patchWithoutSpecialElements, v)
@ -218,11 +569,16 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
typedV := v.(map[string]interface{})
mergeValue, ok := typedV[mergeKey]
if !ok {
return nil, fmt.Errorf("all list elements need the merge key %s", mergeKey)
return nil, fmt.Errorf(errNoMergeKeyFmt, typedV, mergeKey)
}
// If we find a value with this merge key value in original, merge the
// maps. Otherwise append onto original.
originalMap, originalKey, found := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
originalMap, originalKey, found, err := findMapInSliceBasedOnKeyValue(original, mergeKey, mergeValue)
if err != nil {
return nil, err
}
if found {
var mergedMaps interface{}
var err error
@ -231,6 +587,7 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
if err != nil {
return nil, err
}
original[originalKey] = mergedMaps
} else {
original = append(original, v)
@ -240,16 +597,21 @@ func mergeSlice(original, patch []interface{}, elemType reflect.Type, mergeKey s
return original, nil
}
// This panics if any element of the slice is not a map.
func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{}) (map[string]interface{}, int, bool) {
// This method no longer panics if any element of the slice is not a map.
func findMapInSliceBasedOnKeyValue(m []interface{}, key string, value interface{}) (map[string]interface{}, int, bool, error) {
for k, v := range m {
typedV := v.(map[string]interface{})
typedV, ok := v.(map[string]interface{})
if !ok {
return nil, 0, false, fmt.Errorf("value for key %v is not a map.", k)
}
valueToMatch, ok := typedV[key]
if ok && valueToMatch == value {
return typedV, k, true
return typedV, k, true, nil
}
}
return nil, 0, false
return nil, 0, false, nil
}
// This function takes a JSON map and sorts all the lists that should be merged
@ -274,43 +636,46 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
func sortMergeListsByNameMap(s map[string]interface{}, t reflect.Type) (map[string]interface{}, error) {
newS := map[string]interface{}{}
for k, v := range s {
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
// If v is a map or a merge slice, recurse.
if typedV, ok := v.(map[string]interface{}); ok {
var err error
v, err = sortMergeListsByNameMap(typedV, fieldType)
if k != specialKey {
fieldType, fieldPatchStrategy, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k)
if err != nil {
return nil, err
}
} else if typedV, ok := v.([]interface{}); ok {
if fieldPatchStrategy == "merge" {
// If v is a map or a merge slice, recurse.
if typedV, ok := v.(map[string]interface{}); ok {
var err error
v, err = sortMergeListsByNameArray(typedV, fieldType.Elem(), fieldPatchMergeKey)
v, err = sortMergeListsByNameMap(typedV, fieldType)
if err != nil {
return nil, err
}
} else if typedV, ok := v.([]interface{}); ok {
if fieldPatchStrategy == "merge" {
var err error
v, err = sortMergeListsByNameArray(typedV, fieldType.Elem(), fieldPatchMergeKey, true)
if err != nil {
return nil, err
}
}
}
}
newS[k] = v
}
return newS, nil
}
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string) ([]interface{}, error) {
func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey string, recurse bool) ([]interface{}, error) {
if len(s) == 0 {
return s, nil
}
// We don't support lists of lists yet.
t, err := sliceElementType(s)
if err != nil {
return nil, err
}
if t.Kind() == reflect.Slice {
return nil, fmt.Errorf("not supporting lists of lists yet")
}
// If the elements are not maps...
if t.Kind() != reflect.Map {
@ -319,15 +684,20 @@ func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey
}
// Elements are maps - if one of the keys of the map is a map or a
// list, we need to recurse into it.
// list, we may need to recurse into it.
newS := []interface{}{}
for _, elem := range s {
typedElem := elem.(map[string]interface{})
newElem, err := sortMergeListsByNameMap(typedElem, elemType)
if err != nil {
return nil, err
if recurse {
typedElem := elem.(map[string]interface{})
newElem, err := sortMergeListsByNameMap(typedElem, elemType)
if err != nil {
return nil, err
}
newS = append(newS, newElem)
} else {
newS = append(newS, elem)
}
newS = append(newS, newElem)
}
// Sort the maps.
@ -336,19 +706,32 @@ func sortMergeListsByNameArray(s []interface{}, elemType reflect.Type, mergeKey
}
func sortMapsBasedOnField(m []interface{}, fieldName string) []interface{} {
mapM := []map[string]interface{}{}
for _, v := range m {
mapM = append(mapM, v.(map[string]interface{}))
}
mapM := mapSliceFromSlice(m)
ss := SortableSliceOfMaps{mapM, fieldName}
sort.Sort(ss)
newM := []interface{}{}
for _, v := range ss.s {
newM = append(newM, v)
newS := sliceFromMapSlice(ss.s)
return newS
}
func mapSliceFromSlice(m []interface{}) []map[string]interface{} {
newM := []map[string]interface{}{}
for _, v := range m {
vt := v.(map[string]interface{})
newM = append(newM, vt)
}
return newM
}
func sliceFromMapSlice(s []map[string]interface{}) []interface{} {
newS := []interface{}{}
for _, v := range s {
newS = append(newS, v)
}
return newS
}
type SortableSliceOfMaps struct {
s []map[string]interface{}
k string // key to sort on
@ -391,6 +774,7 @@ func uniqifyScalars(s []interface{}) []interface{} {
}
}
}
return s
}
@ -415,7 +799,7 @@ func (ss SortableSliceOfScalars) Swap(i, j int) {
}
// Returns the type of the elements of N slice(s). If the type is different,
// returns an error.
// another slice or undefined, returns an error.
func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
var prevType reflect.Type
for _, s := range slices {
@ -424,16 +808,22 @@ func sliceElementType(slices ...[]interface{}) (reflect.Type, error) {
currentType := reflect.TypeOf(v)
if prevType == nil {
prevType = currentType
// We don't support lists of lists yet.
if prevType.Kind() == reflect.Slice {
return nil, errNoListOfLists
}
} else {
if prevType != currentType {
return nil, fmt.Errorf("at least two types found: %s and %s", prevType, currentType)
return nil, fmt.Errorf("list element types are not identical: %v", fmt.Sprint(slices))
}
prevType = currentType
}
}
}
if prevType == nil {
return nil, fmt.Errorf("no elements in any given slices")
return nil, fmt.Errorf("no elements in any of the given slices")
}
return prevType, nil
}

View File

@ -26,19 +26,26 @@ import (
"github.com/ghodss/yaml"
)
type TestCases struct {
StrategicMergePatchCases []StrategicMergePatchCase
SortMergeListTestCases []SortMergeListCase
type StrategicMergePatchTestCases struct {
TestCases []StrategicMergePatchTestCase
}
type StrategicMergePatchCase struct {
type SortMergeListTestCases struct {
TestCases []SortMergeListTestCase
}
type StrategicMergePatchTestCaseData struct {
Original map[string]interface{}
Patch map[string]interface{}
Modified map[string]interface{}
}
type StrategicMergePatchTestCase struct {
Description string
Patch map[string]interface{}
Original map[string]interface{}
Result map[string]interface{}
StrategicMergePatchTestCaseData
}
type SortMergeListCase struct {
type SortMergeListTestCase struct {
Description string
Original map[string]interface{}
Sorted map[string]interface{}
@ -47,6 +54,7 @@ type SortMergeListCase struct {
type MergeItem struct {
Name string
Value string
Other string
MergingList []MergeItem `patchStrategy:"merge" patchMergeKey:"name"`
NonMergingList []MergeItem
MergingIntList []int `patchStrategy:"merge"`
@ -55,220 +63,10 @@ type MergeItem struct {
SimpleMap map[string]string
}
var testCaseData = []byte(`
strategicMergePatchCases:
- description: add new field
original:
name: 1
patch:
value: 1
result:
name: 1
value: 1
- description: remove field and add new field
original:
name: 1
patch:
name: null
value: 1
result:
value: 1
- description: merge arrays of scalars
original:
mergingIntList:
- 1
- 2
patch:
mergingIntList:
- 2
- 3
result:
mergingIntList:
- 1
- 2
- 3
- description: replace arrays of scalars
original:
nonMergingIntList:
- 1
- 2
patch:
nonMergingIntList:
- 2
- 3
result:
nonMergingIntList:
- 2
- 3
- description: update param of list that should be merged but had element added serverside
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
patch:
mergingList:
- name: 1
value: a
result:
mergingList:
- name: 1
value: a
- name: 2
value: 2
- description: delete field when field is nested in a map
original:
simpleMap:
key1: 1
key2: 1
patch:
simpleMap:
key2: null
result:
simpleMap:
key1: 1
- description: update nested list when nested list should not be merged
original:
mergingList:
- name: 1
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
- name: 2
- description: update nested list when nested list should be merged
original:
mergingList:
- name: 1
mergingList:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
result:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
- description: update map when map should be replaced
original:
name: 1
value: 1
patch:
value: 1
$patch: replace
result:
value: 1
- description: merge empty merge lists
original:
mergingList: []
patch:
mergingList: []
result:
mergingList: []
- description: delete others in a map
original:
name: 1
value: 1
patch:
$patch: replace
result: {}
- description: delete item from a merge list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- $patch: delete
name: 1
result:
mergingList:
- name: 2
- description: add and delete item from a merge list
original:
merginglist:
- name: 1
- name: 2
patch:
merginglist:
- name: 3
- $patch: delete
name: 1
result:
merginglist:
- name: 2
- name: 3
- description: delete all items from a merge list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- $patch: replace
result:
mergingList: []
- description: add new field inside pointers
original:
mergeItemPtr:
- name: 1
patch:
mergeItemPtr:
- name: 2
result:
mergeItemPtr:
- name: 1
- name: 2
- description: update nested pointers
original:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
value: 1
result:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
sortMergeListTestCases:
// These are test cases for SortMergeList, used to assert that it (recursively)
// sorts both merging and non merging lists correctly.
var sortMergeListTestCaseData = []byte(`
testCases:
- description: sort one list of maps
original:
mergingList:
@ -327,7 +125,7 @@ sortMergeListTestCases:
- name: 1
- name: 2
- name: 3
- description: merging list should NOT sort when nested in a non merging list
- description: merging list should NOT sort when nested in non merging list
original:
nonMergingList:
- name: 2
@ -350,7 +148,7 @@ sortMergeListTestCases:
mergingList:
- name: 2
- name: 1
- description: sort a very nested list of maps
- description: sort very nested list of maps
fieldTypes:
original:
mergingList:
@ -410,7 +208,7 @@ sortMergeListTestCases:
- 1
- 2
- 3
- description: sort one pointer of maps
- description: sort merging list by pointer
original:
mergeItemPtr:
- name: 1
@ -423,81 +221,546 @@ sortMergeListTestCases:
- name: 3
`)
func TestStrategicMergePatch(t *testing.T) {
tc := TestCases{}
err := yaml.Unmarshal(testCaseData, &tc)
if err != nil {
t.Errorf("can't unmarshal test cases: %v", err)
return
}
var e MergeItem
for _, c := range tc.StrategicMergePatchCases {
result, err := StrategicMergePatchData(toJSON(c.Original), toJSON(c.Patch), e)
if err != nil {
t.Errorf("error patching: %v:\noriginal:\n%s\npatch:\n%s",
err, toYAML(c.Original), toYAML(c.Patch))
}
// Sort the lists that have merged maps, since order is not significant.
result, err = sortMergeListsByName(result, e)
if err != nil {
t.Errorf("error sorting result object: %v", err)
}
cResult, err := sortMergeListsByName(toJSON(c.Result), e)
if err != nil {
t.Errorf("error sorting result object: %v", err)
}
if !reflect.DeepEqual(result, cResult) {
t.Errorf("patching failed: %s\noriginal:\n%s\npatch:\n%s\nexpected result:\n%s\ngot result:\n%s",
c.Description, toYAML(c.Original), toYAML(c.Patch), jsonToYAML(cResult), jsonToYAML(result))
}
}
}
func TestSortMergeLists(t *testing.T) {
tc := TestCases{}
err := yaml.Unmarshal(testCaseData, &tc)
tc := SortMergeListTestCases{}
err := yaml.Unmarshal(sortMergeListTestCaseData, &tc)
if err != nil {
t.Errorf("can't unmarshal test cases: %v", err)
return
}
var e MergeItem
for _, c := range tc.SortMergeListTestCases {
sorted, err := sortMergeListsByName(toJSON(c.Original), e)
for _, c := range tc.TestCases {
sorted, err := sortMergeListsByName(toJSONOrFail(c.Original, t), e)
if err != nil {
t.Errorf("sort arrays returned error: %v", err)
}
if !reflect.DeepEqual(sorted, toJSON(c.Sorted)) {
t.Errorf("sorting failed: %s\ntried to sort:\n%s\nexpected:\n%s\ngot:\n%s",
c.Description, toYAML(c.Original), toYAML(c.Sorted), jsonToYAML(sorted))
if !reflect.DeepEqual(sorted, toJSONOrFail(c.Sorted, t)) {
t.Errorf("sorting failed: %v\ntried to sort:\n%vexpected:\n%vgot:\n%v",
c.Description, toYAMLOrError(c.Original), toYAMLOrError(c.Sorted), jsonToYAMLOrError(sorted))
}
}
}
func toYAML(v interface{}) string {
y, err := yaml.Marshal(v)
// These are test cases for StrategicMergePatch that cannot be generated using
// CreateStrategicMergePatch because it doesn't use the replace directive, generate
// duplicate integers for a merging list patch, or generate empty merging lists.
var customStrategicMergePatchTestCaseData = []byte(`
testCases:
- description: unique scalars when merging lists
original:
mergingIntList:
- 1
- 2
patch:
mergingIntList:
- 2
- 3
modified:
mergingIntList:
- 1
- 2
- 3
- description: delete all items from merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- $patch: replace
modified:
mergingList: []
- description: merge empty merging lists
original:
mergingList: []
patch:
mergingList: []
modified:
mergingList: []
- description: delete all keys from map
original:
name: 1
value: 1
patch:
$patch: replace
modified: {}
- description: add key and delete all keys from map
original:
name: 1
value: 1
patch:
other: a
$patch: replace
modified:
other: a
`)
func TestCustomStrategicMergePatch(t *testing.T) {
tc := StrategicMergePatchTestCases{}
err := yaml.Unmarshal(customStrategicMergePatchTestCaseData, &tc)
if err != nil {
panic(fmt.Sprintf("yaml marshal failed: %v", err))
t.Errorf("can't unmarshal test cases: %v", err)
return
}
for _, c := range tc.TestCases {
cOriginal, cPatch, cModified := testCaseToJSONOrFail(t, c)
testPatchApplication(t, cOriginal, cPatch, cModified, c.Description)
}
}
func testCaseToJSONOrFail(t *testing.T, c StrategicMergePatchTestCase) ([]byte, []byte, []byte) {
var e MergeItem
cOriginal := toJSONOrFail(c.Original, t)
cPatch, err := sortMergeListsByName(toJSONOrFail(c.Patch, t), e)
if err != nil {
t.Errorf("error:%v sorting patch object:\n%v", err, c.Patch)
}
cModified, err := sortMergeListsByName(toJSONOrFail(c.Modified, t), e)
if err != nil {
t.Errorf("error: %v sorting modified object:\n%v", err, c.Modified)
}
return cOriginal, cPatch, cModified
}
func testPatchApplication(t *testing.T, cOriginal, cPatch, cModified []byte, description string) {
var e MergeItem
modified, err := StrategicMergePatch(cOriginal, cPatch, e)
if err != nil {
t.Errorf("error applying patch: %v:\noriginal:\n%vpatch:\n%v",
err, jsonToYAMLOrError(cOriginal), jsonToYAMLOrError(cPatch))
}
// Sort the lists that have merged maps, since order is not significant.
modified, err = sortMergeListsByName(modified, e)
if err != nil {
t.Errorf("error: %v sorting modified object:\n%v", err, modified)
}
if !reflect.DeepEqual(modified, cModified) {
t.Errorf("patch application failed: %v\noriginal:\n%vpatch:\n%vexpected modified:\n%vgot modified:\n%v",
description, jsonToYAMLOrError(cOriginal), jsonToYAMLOrError(cPatch),
jsonToYAMLOrError(cModified), jsonToYAMLOrError(modified))
}
}
// These are test cases for CreateStrategicMergePatch, used to assert that it
// generates the correct patch for a given outcome. They are also test cases for
// StrategicMergePatch, used to assert that applying a patch yields the correct
// outcome.
var createStrategicMergePatchTestCaseData = []byte(`
testCases:
- description: add field to map
original:
name: 1
patch:
value: 1
modified:
name: 1
value: 1
- description: add field and delete field from map
original:
name: 1
patch:
name: null
value: 1
modified:
value: 1
- description: delete field from nested map
original:
simpleMap:
key1: 1
key2: 1
patch:
simpleMap:
key2: null
modified:
simpleMap:
key1: 1
- description: delete all fields from map
original:
name: 1
value: 1
patch:
name: null
value: null
modified: {}
- description: add field and delete all fields from map
original:
name: 1
value: 1
patch:
other: a
name: null
value: null
modified:
other: a
- description: replace list of scalars
original:
nonMergingIntList:
- 1
- 2
patch:
nonMergingIntList:
- 2
- 3
modified:
nonMergingIntList:
- 2
- 3
- description: merge lists of scalars
original:
mergingIntList:
- 1
- 2
patch:
mergingIntList:
- 3
modified:
mergingIntList:
- 1
- 2
- 3
- description: merge lists of maps
original:
mergingList:
- name: 1
- name: 2
value: 2
patch:
mergingList:
- name: 3
value: 3
modified:
mergingList:
- name: 1
- name: 2
value: 2
- name: 3
value: 3
- description: add field to map in merging list
original:
mergingList:
- name: 1
- name: 2
value: 2
patch:
mergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- description: add duplicate field to map in merging list
original:
mergingList:
- name: 1
- name: 2
value: 2
patch:
mergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- description: replace map field value in merging list
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
patch:
mergingList:
- name: 1
value: a
modified:
mergingList:
- name: 1
value: a
- name: 2
value: 2
- description: delete map from merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- name: 1
$patch: delete
modified:
mergingList:
- name: 2
- description: delete missing map from merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- name: 1
$patch: delete
modified:
mergingList:
- name: 2
- description: add map and delete map from merging list
original:
merginglist:
- name: 1
- name: 2
patch:
merginglist:
- name: 1
$patch: delete
- name: 3
modified:
merginglist:
- name: 2
- name: 3
- description: delete all maps from merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
- description: delete all maps from partially empty merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
- description: delete all maps from empty merging list
original:
mergingList:
- name: 1
- name: 2
patch:
mergingList:
- name: 1
$patch: delete
- name: 2
$patch: delete
modified:
mergingList: []
- description: delete field from map in merging list
original:
mergingList:
- name: 1
value: 1
- name: 2
value: 2
patch:
mergingList:
- name: 1
value: null
modified:
mergingList:
- name: 1
- name: 2
value: 2
- description: replace non merging list nested in merging list
original:
mergingList:
- name: 1
nonMergingList:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
nonMergingList:
- name: 1
value: 1
- name: 2
- description: add field to map in merging list nested in merging list
original:
mergingList:
- name: 1
mergingList:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
modified:
mergingList:
- name: 1
mergingList:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
- description: merge empty merging lists
original:
mergingList: []
patch:
{}
modified:
mergingList: []
current:
mergingList: []
result:
mergingList: []
- description: add map to merging list by pointer
original:
mergeItemPtr:
- name: 1
patch:
mergeItemPtr:
- name: 2
modified:
mergeItemPtr:
- name: 1
- name: 2
- description: add field to map in merging list by pointer
original:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
- name: 2
value: 2
- name: 2
patch:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
value: 1
modified:
mergeItemPtr:
- name: 1
mergeItemPtr:
- name: 1
value: 1
- name: 2
value: 2
- name: 2
`)
func TestStrategicMergePatch(t *testing.T) {
tc := StrategicMergePatchTestCases{}
err := yaml.Unmarshal(createStrategicMergePatchTestCaseData, &tc)
if err != nil {
t.Errorf("can't unmarshal test cases: %v", err)
return
}
var e MergeItem
for _, c := range tc.TestCases {
cOriginal, cPatch, cModified := testCaseToJSONOrFail(t, c)
// Test patch generation
patch, err := CreateStrategicMergePatch(cOriginal, cModified, e)
if err != nil {
t.Errorf("error generating patch: %s:\n%v", err, toYAMLOrError(c.StrategicMergePatchTestCaseData))
}
// Sort the lists that have merged maps, since order is not significant.
patch, err = sortMergeListsByName(patch, e)
if err != nil {
t.Errorf("error: %s sorting patch object:\n%v", err, patch)
}
if !reflect.DeepEqual(patch, cPatch) {
t.Errorf("patch generation failed:\n%vgot patch:\n%v", toYAMLOrError(c.StrategicMergePatchTestCaseData), jsonToYAMLOrError(patch))
}
// Test patch application
testPatchApplication(t, cOriginal, cPatch, cModified, c.Description)
}
}
func toYAMLOrError(v interface{}) string {
y, err := toYAML(v)
if err != nil {
return err.Error()
}
return y
}
func toJSONOrFail(v interface{}, t *testing.T) []byte {
theJSON, err := toJSON(v)
if err != nil {
t.Error(err)
}
return theJSON
}
func jsonToYAMLOrError(j []byte) string {
y, err := jsonToYAML(j)
if err != nil {
return err.Error()
}
return string(y)
}
func toJSON(v interface{}) []byte {
j, err := json.Marshal(v)
func toYAML(v interface{}) (string, error) {
y, err := yaml.Marshal(v)
if err != nil {
panic(fmt.Sprintf("json marshal failed: %s", spew.Sdump(v)))
return "", fmt.Errorf("yaml marshal failed: %v\n%v", err, spew.Sdump(v))
}
return j
return string(y), nil
}
func jsonToYAML(j []byte) []byte {
func toJSON(v interface{}) ([]byte, error) {
j, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("json marshal failed: %v\n%v", err, spew.Sdump(v))
}
return j, nil
}
func jsonToYAML(j []byte) ([]byte, error) {
y, err := yaml.JSONToYAML(j)
if err != nil {
panic(fmt.Sprintf("json to yaml failed: %v", err))
return nil, fmt.Errorf("json to yaml failed: %v\n%v", err, j)
}
return y
return y, nil
}