Skip to content

Commit c87d02d

Browse files
authored
Function to merge two instances of config.Value (#938)
## Changes Semantics for merging two instances of `config.Value`: * Merging x with nil or nil with x always yields x * Merging maps a and b means entries from map b take precedence * Merging sequences a and b means concatenating them These are the same semantics that we use today when calling into mergo in `bundle/config`. ## Tests Unit tests pass.
1 parent 14d1675 commit c87d02d

2 files changed

Lines changed: 305 additions & 0 deletions

File tree

libs/config/merge/merge.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package merge
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/databricks/cli/libs/config"
7+
)
8+
9+
// Merge recursively merges the specified values.
10+
//
11+
// Semantics are as follows:
12+
// * Merging x with nil or nil with x always yields x.
13+
// * Merging maps a and b means entries from map b take precedence.
14+
// * Merging sequences a and b means concatenating them.
15+
func Merge(a, b config.Value) (config.Value, error) {
16+
return merge(a, b)
17+
}
18+
19+
func merge(a, b config.Value) (config.Value, error) {
20+
ak := a.Kind()
21+
bk := b.Kind()
22+
23+
// If a is nil, return b.
24+
if ak == config.KindNil {
25+
return b, nil
26+
}
27+
28+
// If b is nil, return a.
29+
if bk == config.KindNil {
30+
return a, nil
31+
}
32+
33+
// Call the appropriate merge function based on the kind of a and b.
34+
switch ak {
35+
case config.KindMap:
36+
if bk != config.KindMap {
37+
return config.NilValue, fmt.Errorf("cannot merge map with %s", bk)
38+
}
39+
return mergeMap(a, b)
40+
case config.KindSequence:
41+
if bk != config.KindSequence {
42+
return config.NilValue, fmt.Errorf("cannot merge sequence with %s", bk)
43+
}
44+
return mergeSequence(a, b)
45+
default:
46+
if ak != bk {
47+
return config.NilValue, fmt.Errorf("cannot merge %s with %s", ak, bk)
48+
}
49+
return mergePrimitive(a, b)
50+
}
51+
}
52+
53+
func mergeMap(a, b config.Value) (config.Value, error) {
54+
out := make(map[string]config.Value)
55+
am := a.MustMap()
56+
bm := b.MustMap()
57+
58+
// Add the values from a into the output map.
59+
for k, v := range am {
60+
out[k] = v
61+
}
62+
63+
// Merge the values from b into the output map.
64+
for k, v := range bm {
65+
if _, ok := out[k]; ok {
66+
// If the key already exists, merge the values.
67+
merged, err := merge(out[k], v)
68+
if err != nil {
69+
return config.NilValue, err
70+
}
71+
out[k] = merged
72+
} else {
73+
// Otherwise, just set the value.
74+
out[k] = v
75+
}
76+
}
77+
78+
// Preserve the location of the first value.
79+
return config.NewValue(out, a.Location()), nil
80+
}
81+
82+
func mergeSequence(a, b config.Value) (config.Value, error) {
83+
as := a.MustSequence()
84+
bs := b.MustSequence()
85+
86+
// Merging sequences means concatenating them.
87+
out := make([]config.Value, len(as)+len(bs))
88+
copy(out[:], as)
89+
copy(out[len(as):], bs)
90+
91+
// Preserve the location of the first value.
92+
return config.NewValue(out, a.Location()), nil
93+
}
94+
95+
func mergePrimitive(a, b config.Value) (config.Value, error) {
96+
// Merging primitive values means using the incoming value.
97+
return b, nil
98+
}

libs/config/merge/merge_test.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package merge
2+
3+
import (
4+
"testing"
5+
6+
"github.com/databricks/cli/libs/config"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestMergeMaps(t *testing.T) {
11+
v1 := config.V(map[string]config.Value{
12+
"foo": config.V("bar"),
13+
"bar": config.V("baz"),
14+
})
15+
16+
v2 := config.V(map[string]config.Value{
17+
"bar": config.V("qux"),
18+
"qux": config.V("foo"),
19+
})
20+
21+
// Merge v2 into v1.
22+
{
23+
out, err := Merge(v1, v2)
24+
assert.NoError(t, err)
25+
assert.Equal(t, map[string]any{
26+
"foo": "bar",
27+
"bar": "qux",
28+
"qux": "foo",
29+
}, out.AsAny())
30+
}
31+
32+
// Merge v1 into v2.
33+
{
34+
out, err := Merge(v2, v1)
35+
assert.NoError(t, err)
36+
assert.Equal(t, map[string]any{
37+
"foo": "bar",
38+
"bar": "baz",
39+
"qux": "foo",
40+
}, out.AsAny())
41+
}
42+
}
43+
44+
func TestMergeMapsNil(t *testing.T) {
45+
v := config.V(map[string]config.Value{
46+
"foo": config.V("bar"),
47+
})
48+
49+
// Merge nil into v.
50+
{
51+
out, err := Merge(v, config.NilValue)
52+
assert.NoError(t, err)
53+
assert.Equal(t, map[string]any{
54+
"foo": "bar",
55+
}, out.AsAny())
56+
}
57+
58+
// Merge v into nil.
59+
{
60+
out, err := Merge(config.NilValue, v)
61+
assert.NoError(t, err)
62+
assert.Equal(t, map[string]any{
63+
"foo": "bar",
64+
}, out.AsAny())
65+
}
66+
}
67+
68+
func TestMergeMapsError(t *testing.T) {
69+
v := config.V(map[string]config.Value{
70+
"foo": config.V("bar"),
71+
})
72+
73+
other := config.V("string")
74+
75+
// Merge a string into v.
76+
{
77+
out, err := Merge(v, other)
78+
assert.EqualError(t, err, "cannot merge map with string")
79+
assert.Equal(t, config.NilValue, out)
80+
}
81+
}
82+
83+
func TestMergeSequences(t *testing.T) {
84+
v1 := config.V([]config.Value{
85+
config.V("bar"),
86+
config.V("baz"),
87+
})
88+
89+
v2 := config.V([]config.Value{
90+
config.V("qux"),
91+
config.V("foo"),
92+
})
93+
94+
// Merge v2 into v1.
95+
{
96+
out, err := Merge(v1, v2)
97+
assert.NoError(t, err)
98+
assert.Equal(t, []any{
99+
"bar",
100+
"baz",
101+
"qux",
102+
"foo",
103+
}, out.AsAny())
104+
}
105+
106+
// Merge v1 into v2.
107+
{
108+
out, err := Merge(v2, v1)
109+
assert.NoError(t, err)
110+
assert.Equal(t, []any{
111+
"qux",
112+
"foo",
113+
"bar",
114+
"baz",
115+
}, out.AsAny())
116+
}
117+
}
118+
119+
func TestMergeSequencesNil(t *testing.T) {
120+
v := config.V([]config.Value{
121+
config.V("bar"),
122+
})
123+
124+
// Merge nil into v.
125+
{
126+
out, err := Merge(v, config.NilValue)
127+
assert.NoError(t, err)
128+
assert.Equal(t, []any{
129+
"bar",
130+
}, out.AsAny())
131+
}
132+
133+
// Merge v into nil.
134+
{
135+
out, err := Merge(config.NilValue, v)
136+
assert.NoError(t, err)
137+
assert.Equal(t, []any{
138+
"bar",
139+
}, out.AsAny())
140+
}
141+
}
142+
143+
func TestMergeSequencesError(t *testing.T) {
144+
v := config.V([]config.Value{
145+
config.V("bar"),
146+
})
147+
148+
other := config.V("string")
149+
150+
// Merge a string into v.
151+
{
152+
out, err := Merge(v, other)
153+
assert.EqualError(t, err, "cannot merge sequence with string")
154+
assert.Equal(t, config.NilValue, out)
155+
}
156+
}
157+
158+
func TestMergePrimitives(t *testing.T) {
159+
v1 := config.V("bar")
160+
v2 := config.V("baz")
161+
162+
// Merge v2 into v1.
163+
{
164+
out, err := Merge(v1, v2)
165+
assert.NoError(t, err)
166+
assert.Equal(t, "baz", out.AsAny())
167+
}
168+
169+
// Merge v1 into v2.
170+
{
171+
out, err := Merge(v2, v1)
172+
assert.NoError(t, err)
173+
assert.Equal(t, "bar", out.AsAny())
174+
}
175+
}
176+
177+
func TestMergePrimitivesNil(t *testing.T) {
178+
v := config.V("bar")
179+
180+
// Merge nil into v.
181+
{
182+
out, err := Merge(v, config.NilValue)
183+
assert.NoError(t, err)
184+
assert.Equal(t, "bar", out.AsAny())
185+
}
186+
187+
// Merge v into nil.
188+
{
189+
out, err := Merge(config.NilValue, v)
190+
assert.NoError(t, err)
191+
assert.Equal(t, "bar", out.AsAny())
192+
}
193+
}
194+
195+
func TestMergePrimitivesError(t *testing.T) {
196+
v := config.V("bar")
197+
other := config.V(map[string]config.Value{
198+
"foo": config.V("bar"),
199+
})
200+
201+
// Merge a map into v.
202+
{
203+
out, err := Merge(v, other)
204+
assert.EqualError(t, err, "cannot merge string with map")
205+
assert.Equal(t, config.NilValue, out)
206+
}
207+
}

0 commit comments

Comments
 (0)