From 3039f7846cb16c6f2e9f452c1ea98516fe085031 Mon Sep 17 00:00:00 2001 From: Mike Danese Date: Sat, 14 Feb 2015 19:05:00 -0800 Subject: [PATCH] implement RFC7386 JSON Merge Patch --- pkg/util/merge/merge.go | 57 ++++++++++++++++++++++++++++++ pkg/util/merge/merge_test.go | 68 ++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 pkg/util/merge/merge.go create mode 100644 pkg/util/merge/merge_test.go diff --git a/pkg/util/merge/merge.go b/pkg/util/merge/merge.go new file mode 100644 index 00000000000..8b1cbf3c1f3 --- /dev/null +++ b/pkg/util/merge/merge.go @@ -0,0 +1,57 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package merge + +import ( + "encoding/json" +) + +// MergeJSON merges JSON according to RFC7386 +// (see https://tools.ietf.org/html/rfc7386) +func MergeJSON(dst, src []byte) ([]byte, error) { + var target interface{} + if err := json.Unmarshal(dst, &target); err != nil { + return nil, err + } + var patch interface{} + if err := json.Unmarshal(src, &patch); err != nil { + return nil, err + } + return json.Marshal(MergePatch(target, patch)) +} + +// MergePatch is an implementation of MergePatch described in RFC7386 that operates on +// json marshalled into empty interface{} by encoding/json.Unmarshal() +// (see https://tools.ietf.org/html/rfc7386#section-2) +func MergePatch(target, patch interface{}) interface{} { + if patchObject, isPatchObject := patch.(map[string]interface{}); isPatchObject { + targetObject := make(map[string]interface{}) + if m, isTargetObject := target.(map[string]interface{}); isTargetObject { + targetObject = m + } + for name, value := range patchObject { + if _, found := targetObject[name]; value == nil && found { + delete(targetObject, name) + } else { + targetObject[name] = MergePatch(targetObject[name], value) + } + } + return targetObject + } else { + return patch + } +} diff --git a/pkg/util/merge/merge_test.go b/pkg/util/merge/merge_test.go new file mode 100644 index 00000000000..eb3118bcabf --- /dev/null +++ b/pkg/util/merge/merge_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package merge + +import ( + "testing" +) + +func TestMergeJSON(t *testing.T) { + tests := []struct { + target string + patch string + expected string + }{ + {target: `{}`, patch: `{}`, expected: `{}`}, + {target: `{"a":"b","c":{"d":"e","f":"g"}}`, patch: `{}`, expected: `{"a":"b","c":{"d":"e","f":"g"}}`}, + //update + {target: `{"a":"b","c":"d"}`, patch: `{"a":"z"}`, expected: `{"a":"z","c":"d"}`}, + //remove key + {target: `{"f":"g"}`, patch: `{"f":null}`, expected: `{}`}, + //inner update + {target: `{"c":{"d":"e","f":"g"}}`, patch: `{"c":{"f":"z"}}`, expected: `{"c":{"d":"e","f":"z"}}`}, + //inner remove + {target: `{"c":{"d":"e","f":"g"}}`, patch: `{"c":{"f":null}}`, expected: `{"c":{"d":"e"}}`}, + //complex update and remove + {target: `{"a":"b","c":{"d":"e","f":"g"}}`, patch: `{"a":"z","c":{"f":null}}`, expected: `{"a":"z","c":{"d":"e"}}`}, + // test cases from https://tools.ietf.org/html/rfc7386#appendix-A slightly adapted to correspond to go's + // encoding/json conventions + {target: `{"a":"b"}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`}, + {target: `{"a":"b"}`, patch: `{"b":"c"}`, expected: `{"a":"b","b":"c"}`}, + {target: `{"a":"b"}`, patch: `{"a":null}`, expected: `{}`}, + {target: `{"a":"b","b":"c"}`, patch: `{"a":null}`, expected: `{"b":"c"}`}, + {target: `{"a":["b"]}`, patch: `{"a":"c"}`, expected: `{"a":"c"}`}, + {target: `{"a":"c"}`, patch: `{"a":["b"]}`, expected: `{"a":["b"]}`}, + {target: `{"a":{"b": "c"}}`, patch: `{"a": {"b": "d","c": null}}`, expected: `{"a":{"b":"d","c":null}}`}, + {target: `{"a":[{"b":"c"}]}`, patch: `{"a":[1]}`, expected: `{"a":[1]}`}, + {target: `["a","b"]`, patch: `["c","d"]`, expected: `["c","d"]`}, + {target: `{"a":"b"}`, patch: `["c"]`, expected: `["c"]`}, + {target: `{"a":"foo"}`, patch: `null`, expected: `null`}, + {target: `{"a":"foo"}`, patch: `"bar"`, expected: `"bar"`}, + {target: `{"e":null}`, patch: `{"a":1}`, expected: `{"a":1,"e":null}`}, + {target: `[1,2]`, patch: `{"a":"b","c":null}`, expected: `{"a":"b","c":null}`}, + {target: `{}`, patch: `{"a":{"bb":{"ccc":null}}}`, expected: `{"a":{"bb":{"ccc":null}}}`}, + } + for i, test := range tests { + out, err := MergeJSON([]byte(test.target), []byte(test.patch)) + if err != nil { + t.Errorf("case %v, unexpected error: %v", i, err) + } + if string(out) != test.expected { + t.Errorf("case %v, expected:\n%v\nsaw:\n%v\n", i, test.expected, string(out)) + } + } +}