Skip to content

Commit ea1f89d

Browse files
committed
full json merging
Signed-off-by: awphi <26072111+awphi@users.noreply.github.com>
1 parent bb5d4e2 commit ea1f89d

2 files changed

Lines changed: 148 additions & 17 deletions

File tree

internal/testutil/testutil.go

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package testutil
33
import (
44
"encoding/json"
55
"fmt"
6+
"maps"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -12,6 +13,7 @@ import (
1213
"testing"
1314

1415
"github.com/arm/topo/internal/template"
16+
"github.com/stretchr/testify/assert"
1517
"github.com/stretchr/testify/require"
1618
)
1719

@@ -110,10 +112,10 @@ func AssertFileContents(t *testing.T, wantContents string, path string) {
110112

111113
func AssertJsonGoldenFile(t *testing.T, got string, goldenPath string) {
112114
t.Helper()
113-
AssertJsonGoldenFileWithOverrides(t, got, goldenPath, map[string]any{})
115+
AssertJsonGoldenFileWithOverrides(t, got, goldenPath, nil)
114116
}
115117

116-
func AssertJsonGoldenFileWithOverrides(t *testing.T, got string, goldenPath string, overrides map[string]any) {
118+
func AssertJsonGoldenFileWithOverrides(t *testing.T, got string, goldenPath string, overrides any) {
117119
t.Helper()
118120

119121
if os.Getenv("UPDATE_GOLDEN") == "1" {
@@ -122,37 +124,75 @@ func AssertJsonGoldenFileWithOverrides(t *testing.T, got string, goldenPath stri
122124
t.Skipf("Updated golden file: %s", goldenPath)
123125
}
124126

125-
gotObj := make(map[string]any)
126-
err := json.Unmarshal([]byte(got), &gotObj)
127+
wantBytes, err := os.ReadFile(goldenPath)
127128
require.NoError(t, err)
128129

129-
wantBytes, err := os.ReadFile(goldenPath)
130+
var wantValue any
131+
err = json.Unmarshal(wantBytes, &wantValue)
130132
require.NoError(t, err)
131133

132-
wantObj := make(map[string]any)
133-
err = json.Unmarshal(wantBytes, &wantObj)
134+
if hasJSONOverrides(overrides) {
135+
wantValue = MergeJSONValues(wantValue, overrides)
136+
}
137+
138+
wantWithOverrides, err := json.Marshal(wantValue)
134139
require.NoError(t, err)
135140

136-
deepMergeMaps(wantObj, overrides)
141+
assert.JSONEq(t, string(wantWithOverrides), got, "output did not match golden file %s", goldenPath)
142+
}
137143

138-
require.Equal(t, wantObj, gotObj, "output did not match golden file %s", goldenPath)
144+
func hasJSONOverrides(value any) bool {
145+
switch typedValue := value.(type) {
146+
case nil:
147+
return false
148+
case map[string]any:
149+
return len(typedValue) > 0
150+
case []any:
151+
return len(typedValue) > 0
152+
default:
153+
return true
154+
}
139155
}
140156

141-
func deepMergeMaps(base, override map[string]any) {
157+
func MergeJSONValues(base, override any) any {
158+
baseObj, baseIsObj := base.(map[string]any)
159+
overrideObj, overrideIsObj := override.(map[string]any)
160+
if baseIsObj && overrideIsObj {
161+
return mergeJSONMaps(baseObj, overrideObj)
162+
}
163+
164+
baseArray, baseIsArray := base.([]any)
165+
overrideArray, overrideIsArray := override.([]any)
166+
if baseIsArray && overrideIsArray {
167+
return mergeJSONArrays(baseArray, overrideArray)
168+
}
169+
170+
return override
171+
}
172+
173+
func mergeJSONMaps(base, override map[string]any) map[string]any {
174+
merged := maps.Clone(base)
142175
for key, overrideValue := range override {
143-
baseValue, ok := base[key]
176+
baseValue, ok := merged[key]
144177
if !ok {
145-
base[key] = overrideValue
178+
merged[key] = overrideValue
146179
continue
147180
}
148181

149-
baseObj, baseIsObj := baseValue.(map[string]any)
150-
overrideObj, overrideIsObj := overrideValue.(map[string]any)
151-
if baseIsObj && overrideIsObj {
152-
deepMergeMaps(baseObj, overrideObj)
182+
merged[key] = MergeJSONValues(baseValue, overrideValue)
183+
}
184+
return merged
185+
}
186+
187+
func mergeJSONArrays(base, override []any) []any {
188+
merged := slices.Clone(base)
189+
for index, overrideValue := range override {
190+
if index >= len(merged) {
191+
merged = append(merged, overrideValue)
153192
continue
154193
}
155194

156-
base[key] = overrideValue
195+
merged[index] = MergeJSONValues(merged[index], overrideValue)
157196
}
197+
return merged
158198
}

internal/testutil/testutil_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package testutil_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/arm/topo/internal/testutil"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestMergeJSONValues(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
base any
14+
override any
15+
want any
16+
}{
17+
{
18+
name: "merges nested maps",
19+
base: map[string]any{
20+
"name": "service",
21+
"metadata": map[string]any{
22+
"version": "1.0",
23+
"status": "old",
24+
},
25+
},
26+
override: map[string]any{
27+
"metadata": map[string]any{
28+
"status": "new",
29+
},
30+
},
31+
want: map[string]any{
32+
"name": "service",
33+
"metadata": map[string]any{
34+
"version": "1.0",
35+
"status": "new",
36+
},
37+
},
38+
},
39+
{
40+
name: "merges nested arrays by index",
41+
base: []any{
42+
map[string]any{
43+
"name": "first",
44+
"checks": []any{
45+
map[string]any{"status": "old", "name": "nested"},
46+
},
47+
},
48+
map[string]any{"name": "second"},
49+
},
50+
override: []any{
51+
map[string]any{
52+
"checks": []any{
53+
map[string]any{"status": "new"},
54+
},
55+
},
56+
},
57+
want: []any{
58+
map[string]any{
59+
"name": "first",
60+
"checks": []any{
61+
map[string]any{"status": "new", "name": "nested"},
62+
},
63+
},
64+
map[string]any{"name": "second"},
65+
},
66+
},
67+
{
68+
name: "appends extra array overrides",
69+
base: []any{"first"},
70+
override: []any{
71+
nil,
72+
"second",
73+
},
74+
want: []any{nil, "second"},
75+
},
76+
{
77+
name: "replaces mismatched values",
78+
base: map[string]any{"name": "old"},
79+
override: "new",
80+
want: "new",
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
got := testutil.MergeJSONValues(tt.base, tt.override)
87+
88+
assert.Equal(t, tt.want, got)
89+
})
90+
}
91+
}

0 commit comments

Comments
 (0)