├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── mergemap.go └── mergemap_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Peter Bourgon, SoundCloud Ltd. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mergemap 2 | 3 | mergemap is a Go library to recursively merge JSON maps. 4 | 5 | [![Build Status](https://drone.io/github.com/peterbourgon/mergemap/status.png)](https://drone.io/github.com/peterbourgon/mergemap/latest) 6 | 7 | 8 | ## Behavior 9 | 10 | mergemap performs a simple merge of the **src** map into the **dst** map. That 11 | is, it takes the **src** value when there is a key conflict. 12 | 13 | The only special behavior is when the conflicting key represents a map in both 14 | src and dst. Then, mergemap recursively descends into both maps, repeating the 15 | same logic. The max recursion depth is set by **mergemap.MaxDepth**. 16 | 17 | 18 | ## Usage 19 | 20 | ```go 21 | var m1, m2 map[string]interface{} 22 | json.Unmarshal(buf1, &m1) 23 | json.Unmarshal(buf2, &m2) 24 | 25 | merged := mergemap.Merge(m1, m2) 26 | ``` 27 | 28 | See the test file for some pretty straightforward examples. 29 | 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/peterbourgon/mergemap 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /mergemap.go: -------------------------------------------------------------------------------- 1 | package mergemap 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | var ( 8 | MaxDepth = 32 9 | ) 10 | 11 | // Merge recursively merges the src and dst maps. Key conflicts are resolved by 12 | // preferring src, or recursively descending, if both src and dst are maps. 13 | func Merge(dst, src map[string]interface{}) map[string]interface{} { 14 | return merge(dst, src, 0) 15 | } 16 | 17 | func merge(dst, src map[string]interface{}, depth int) map[string]interface{} { 18 | if depth > MaxDepth { 19 | panic("too deep!") 20 | } 21 | for key, srcVal := range src { 22 | if dstVal, ok := dst[key]; ok { 23 | srcMap, srcMapOk := mapify(srcVal) 24 | dstMap, dstMapOk := mapify(dstVal) 25 | if srcMapOk && dstMapOk { 26 | srcVal = merge(dstMap, srcMap, depth+1) 27 | } 28 | } 29 | dst[key] = srcVal 30 | } 31 | return dst 32 | } 33 | 34 | func mapify(i interface{}) (map[string]interface{}, bool) { 35 | value := reflect.ValueOf(i) 36 | if value.Kind() == reflect.Map { 37 | m := map[string]interface{}{} 38 | for _, k := range value.MapKeys() { 39 | m[k.String()] = value.MapIndex(k).Interface() 40 | } 41 | return m, true 42 | } 43 | return map[string]interface{}{}, false 44 | } 45 | -------------------------------------------------------------------------------- /mergemap_test.go: -------------------------------------------------------------------------------- 1 | package mergemap 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestMerge(t *testing.T) { 10 | for _, tuple := range []struct { 11 | src string 12 | dst string 13 | expected string 14 | }{ 15 | { 16 | src: `{}`, 17 | dst: `{}`, 18 | expected: `{}`, 19 | }, 20 | { 21 | src: `{"b":2}`, 22 | dst: `{"a":1}`, 23 | expected: `{"a":1,"b":2}`, 24 | }, 25 | { 26 | src: `{"a":0}`, 27 | dst: `{"a":1}`, 28 | expected: `{"a":0}`, 29 | }, 30 | { 31 | src: `{"a":{ "y":2}}`, 32 | dst: `{"a":{"x":1 }}`, 33 | expected: `{"a":{"x":1, "y":2}}`, 34 | }, 35 | { 36 | src: `{"a":{"x":2}}`, 37 | dst: `{"a":{"x":1}}`, 38 | expected: `{"a":{"x":2}}`, 39 | }, 40 | { 41 | src: `{"a":{ "y":7, "z":8}}`, 42 | dst: `{"a":{"x":1, "y":2 }}`, 43 | expected: `{"a":{"x":1, "y":7, "z":8}}`, 44 | }, 45 | { 46 | src: `{"1": { "b":1, "2": { "3": { "b":3, "n":[1,2]} } }}`, 47 | dst: `{"1": { "2": { "3": {"a":"A", "n":"xxx"} }, "a":3 }}`, 48 | expected: `{"1": { "b":1, "2": { "3": {"a":"A", "b":3, "n":[1,2]} }, "a":3 }}`, 49 | }, 50 | } { 51 | var dst map[string]interface{} 52 | if err := json.Unmarshal([]byte(tuple.dst), &dst); err != nil { 53 | t.Error(err) 54 | continue 55 | } 56 | 57 | var src map[string]interface{} 58 | if err := json.Unmarshal([]byte(tuple.src), &src); err != nil { 59 | t.Error(err) 60 | continue 61 | } 62 | 63 | var expected map[string]interface{} 64 | if err := json.Unmarshal([]byte(tuple.expected), &expected); err != nil { 65 | t.Error(err) 66 | continue 67 | } 68 | 69 | got := Merge(dst, src) 70 | assert(t, expected, got) 71 | } 72 | } 73 | 74 | func assert(t *testing.T, expected, got map[string]interface{}) { 75 | expectedBuf, err := json.Marshal(expected) 76 | if err != nil { 77 | t.Error(err) 78 | return 79 | } 80 | gotBuf, err := json.Marshal(got) 81 | if err != nil { 82 | t.Error(err) 83 | return 84 | } 85 | if bytes.Compare(expectedBuf, gotBuf) != 0 { 86 | t.Errorf("expected %s, got %s", string(expectedBuf), string(gotBuf)) 87 | return 88 | } 89 | } 90 | --------------------------------------------------------------------------------