Skip to content

Commit c51e3ef

Browse files
committed
deepmerge function
1 parent c6c4399 commit c51e3ef

File tree

5 files changed

+217
-24
lines changed

5 files changed

+217
-24
lines changed

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ Contents:
125125
- [(( makemap(fieldlist) ))](#-makemapfieldlist-)
126126
- [(( makemap(key, value) ))](#-makemapkey-value-)
127127
- [(( merge(map1, map2) ))](#-mergemap1-map2-)
128+
- [(( deepmerge(map1, map2) ))](#-deepmergemap1-map2-)
128129
- [(( intersect(list1, list2) ))](#-intersectlist1-list2-)
129130
- [(( reverse(list) ))](#-reverselist-)
130131
- [(( parse(yamlorjson) ))](#-parseyamlorjson-)
@@ -781,6 +782,7 @@ yields the list `[ 1, 2, 3, "alice" ]` for `bar`.
781782
### `(( map1 map2 ))`
782783

783784
Concatenation of maps as expression. Any sequences of maps can be concatenated, given by any dynaml expression. Thereby entries will be merged. Entries with the same key are overwritten from left to right.
785+
This is a flat map merge, only top level entries are merged. If you want a deep merge you can use [deepmerge(map1, map2)](#-deepmergemap1-map2-)
784786

785787
e.g.:
786788

@@ -2826,6 +2828,54 @@ merged:
28262828
sum: 49
28272829
```
28282830

2831+
### `(( deepmerge(map1, map2) ))`
2832+
2833+
The `merge` function provides a spiff-like merge used for merging templates and stubs. Hereby, only existing entries can be overwritten.
2834+
If a formal deep merge of map entries is required, the `deepmerge` function can be used.
2835+
2836+
If an entry in both maps is a again a map, those maps are merged recursively, again.
2837+
It is possible to give any number of maps as arguments to `deepmerge`.
2838+
Alternatively a single list of maps can be given.
2839+
Merging is done from left to right, where values in later maps replace left values.
2840+
2841+
e.g.:
2842+
2843+
```yaml
2844+
map1:
2845+
a: a
2846+
b: b
2847+
c:
2848+
a: ca
2849+
b: cb
2850+
d: d
2851+
2852+
map2:
2853+
a: 2a
2854+
c:
2855+
a: 2ca
2856+
c: 2cc
2857+
d:
2858+
a: 2da
2859+
e: 2e
2860+
2861+
merged: (( deepmerge(map1,map2) ))
2862+
```
2863+
2864+
resolves `merged` to
2865+
2866+
```yaml
2867+
merged:
2868+
a: 2a
2869+
b: b
2870+
c:
2871+
a: 2ca
2872+
b: cb
2873+
c: 2cc
2874+
d:
2875+
a: 2da
2876+
e: 2e
2877+
```
2878+
28292879
### `(( intersect(list1, list2) ))`
28302880

28312881
The function `intersect` intersects multiple lists. A list may contain entries

dynaml/call.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ func (e CallExpr) Evaluate(binding Binding, locally bool) (interface{}, Evaluati
275275
case "merge":
276276
result, sub, ok = func_merge(values, binding)
277277

278+
case "deepmerge":
279+
result, sub, ok = func_deepmerge(values, binding)
280+
278281
case "base64":
279282
result, sub, ok = func_base64(values, binding)
280283
case "base64_decode":

dynaml/deepmerge.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dynaml
2+
3+
import (
4+
"github.com/mandelsoft/spiff/yaml"
5+
)
6+
7+
func func_deepmerge(arguments []interface{}, binding Binding) (interface{}, EvaluationInfo, bool) {
8+
info := DefaultInfo()
9+
10+
if len(arguments) < 1 {
11+
return nil, info, false
12+
}
13+
14+
maps, msg := getMapList(arguments)
15+
if maps == nil {
16+
return info.Error("deepmerge: %s", msg)
17+
}
18+
result := make(map[string]yaml.Node)
19+
for _, v := range maps {
20+
deepmerge(result, v)
21+
}
22+
return result, info, true
23+
}
24+
25+
func deepmerge(result map[string]yaml.Node, m map[string]yaml.Node) {
26+
for k, v := range m {
27+
if isMap(result[k]) && isMap(v) {
28+
r := make(map[string]yaml.Node)
29+
concatenateMap(r, result[k].Value().(map[string]yaml.Node))
30+
deepmerge(r, v.Value().(map[string]yaml.Node))
31+
v = yaml.NewNode(r, "<deepmerge>")
32+
}
33+
result[k] = v
34+
}
35+
}

dynaml/mapmerge.go

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dynaml
22

33
import (
44
"fmt"
5+
56
"github.com/mandelsoft/spiff/yaml"
67
)
78

@@ -12,31 +13,14 @@ func func_merge(arguments []interface{}, binding Binding) (interface{}, Evaluati
1213
return info.Error("at least one argument required for merge function")
1314
}
1415

15-
args := []yaml.Node{}
16-
17-
if len(arguments) == 1 {
18-
l, ok := arguments[0].([]yaml.Node)
19-
if ok {
20-
for i, e := range l {
21-
m, err := getMap(i, e.Value())
22-
if err != nil {
23-
return info.Error("merge: entry of list argument: %s", err)
24-
}
25-
args = append(args, yaml.NewNode(m, "dynaml"))
26-
}
27-
if len(args) == 0 {
28-
return info.Error("merge: no map found for merge")
29-
}
30-
}
16+
maps, msg := getMapList(arguments)
17+
if maps == nil {
18+
return info.Error("merge: %s", msg)
3119
}
32-
if len(args) == 0 {
33-
for i, arg := range arguments {
34-
m, err := getMap(i, arg)
35-
if err != nil {
36-
return info.Error("merge: argument %s", err)
37-
}
38-
args = append(args, yaml.NewNode(m, "dynaml"))
39-
}
20+
21+
args := make([]yaml.Node, len(maps))
22+
for i, m := range maps {
23+
args[i] = yaml.NewNode(m, "dynaml")
4024
}
4125
result, err := binding.Cascade(binding, args[0], false, args[1:]...)
4226
if err != nil {
@@ -62,3 +46,33 @@ func getMap(n int, arg interface{}) (map[string]yaml.Node, error) {
6246
}
6347
return nil, fmt.Errorf("%d: no map or map template, but %s", n+1, ExpressionType(arg))
6448
}
49+
50+
func getMapList(arguments []interface{}) ([]map[string]yaml.Node, string) {
51+
args := []map[string]yaml.Node{}
52+
53+
if len(arguments) == 1 {
54+
l, ok := arguments[0].([]yaml.Node)
55+
if ok {
56+
for i, e := range l {
57+
m, err := getMap(i, e.Value())
58+
if err != nil {
59+
return nil, fmt.Sprintf("entry of list argument: %s", err)
60+
}
61+
args = append(args, m)
62+
}
63+
if len(args) == 0 {
64+
return nil, "no map found for merge"
65+
}
66+
}
67+
}
68+
if len(args) == 0 {
69+
for i, arg := range arguments {
70+
m, err := getMap(i, arg)
71+
if err != nil {
72+
return nil, fmt.Sprintf("argument %s", err)
73+
}
74+
args = append(args, m)
75+
}
76+
}
77+
return args, ""
78+
}

flow/flow_deepmerge_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package flow
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("Flowing YAML with deepmerge functipn", func() {
9+
It("handles multiple maps", func() {
10+
source := parseYAML(`
11+
---
12+
in:
13+
<<: (( &temporary ))
14+
map1:
15+
a: a
16+
b: b
17+
c:
18+
a: ca
19+
b: cb
20+
d: d
21+
22+
map2:
23+
a: 2a
24+
c:
25+
a: 2ca
26+
c: 2cc
27+
d:
28+
a: 2da
29+
e: 2e
30+
31+
merge: (( deepmerge(in.map1,in.map2) ))
32+
`)
33+
resolved := parseYAML(`
34+
---
35+
merge:
36+
a: 2a
37+
b: b
38+
c:
39+
a: 2ca
40+
b: cb
41+
c: 2cc
42+
d:
43+
a: 2da
44+
e: 2e
45+
`)
46+
Expect(source).To(FlowAs(resolved))
47+
})
48+
49+
It("handles multiple map list", func() {
50+
source := parseYAML(`
51+
---
52+
in:
53+
<<: (( &temporary ))
54+
map1:
55+
a: a
56+
b: b
57+
c:
58+
a: ca
59+
b: cb
60+
d: d
61+
62+
map2:
63+
a: 2a
64+
c:
65+
a: 2ca
66+
c: 2cc
67+
d:
68+
a: 2da
69+
e: 2e
70+
list:
71+
- (( map1 ))
72+
- (( map2 ))
73+
74+
merge: (( deepmerge(in.list) ))
75+
`)
76+
resolved := parseYAML(`
77+
---
78+
merge:
79+
a: 2a
80+
b: b
81+
c:
82+
a: 2ca
83+
b: cb
84+
c: 2cc
85+
d:
86+
a: 2da
87+
e: 2e
88+
`)
89+
Expect(source).To(FlowAs(resolved))
90+
})
91+
})

0 commit comments

Comments
 (0)