Skip to content

Commit 5ac849e

Browse files
committed
feat: Add tests
1 parent 4d1270e commit 5ac849e

8 files changed

Lines changed: 455 additions & 0 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// flatFinding is an optimized version of Finding
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type flatFinding struct {
9+
ID string `json:"id"`
10+
Name string `json:"name"`
11+
Score *float64 `json:"score,omitempty"`
12+
Severity string `json:"severity"`
13+
Status string `json:"status"`
14+
Deleted bool `json:"deleted"`
15+
Count int64 `json:"count"`
16+
Address map[string]any `json:"address,omitempty"`
17+
Tags []map[string]any `json:"tags,omitempty"`
18+
Connection map[string]any `json:"connection"`
19+
Categories []string `json:"categories,omitempty"`
20+
Entity map[string]any `json:"entity"`
21+
}
22+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// extendedSimple is an optimized version of Simple
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type extendedSimple struct {
9+
AssetType string `json:"assetType,omitempty"`
10+
ID string `json:"id"`
11+
Value int64 `json:"value"`
12+
}
13+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// flatFinding is an optimized version of Finding
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type flatFinding struct {
9+
ID string `json:"id"`
10+
Name string `json:"name"`
11+
Score *float64 `json:"score,omitempty"`
12+
Severity string `json:"severity"`
13+
Status string `json:"status"`
14+
Deleted bool `json:"deleted"`
15+
Count int64 `json:"count"`
16+
Address map[string]any `json:"address,omitempty"`
17+
Tags []map[string]any `json:"tags,omitempty"`
18+
Connection map[string]any `json:"connection"`
19+
Categories []string `json:"categories,omitempty"`
20+
Entity map[string]any `json:"entity"`
21+
}
22+
23+
// Code generated by codegen/structs; DO NOT EDIT.
24+
25+
package testpkg
26+
27+
// flatSimple is an optimized version of Simple
28+
// where deeply nested struct fields are replaced with map[string]any to avoid
29+
// the decode -> allocate -> re-encode cycle.
30+
type flatSimple struct {
31+
ID string `json:"id"`
32+
Value int64 `json:"value"`
33+
}
34+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// sortedFinding is an optimized version of Finding
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type sortedFinding struct {
9+
ID string `json:"id"`
10+
Address map[string]any `json:"address,omitempty"`
11+
Categories []string `json:"categories,omitempty"`
12+
Connection map[string]any `json:"connection"`
13+
Count int64 `json:"count"`
14+
Deleted bool `json:"deleted"`
15+
Entity map[string]any `json:"entity"`
16+
Name string `json:"name"`
17+
Score *float64 `json:"score,omitempty"`
18+
Severity string `json:"severity"`
19+
Status string `json:"status"`
20+
Tags []map[string]any `json:"tags,omitempty"`
21+
}
22+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// sortedFinding is an optimized version of Finding
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type sortedFinding struct {
9+
AssetType string `json:"assetType,omitempty"`
10+
ID string `json:"id"`
11+
Address map[string]any `json:"address,omitempty"`
12+
Categories []string `json:"categories,omitempty"`
13+
Connection map[string]any `json:"connection"`
14+
Count int64 `json:"count"`
15+
Deleted bool `json:"deleted"`
16+
Entity map[string]any `json:"entity"`
17+
Name string `json:"name"`
18+
Score *float64 `json:"score,omitempty"`
19+
Severity string `json:"severity"`
20+
Status string `json:"status"`
21+
Tags []map[string]any `json:"tags,omitempty"`
22+
}
23+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Code generated by codegen/structs; DO NOT EDIT.
2+
3+
package testpkg
4+
5+
// flatFinding is an optimized version of Finding
6+
// where deeply nested struct fields are replaced with map[string]any to avoid
7+
// the decode -> allocate -> re-encode cycle.
8+
type flatFinding struct {
9+
ID string `json:"id"`
10+
Name string `json:"name"`
11+
Score *float64 `json:"score,omitempty"`
12+
Severity string `json:"severity"`
13+
Status string `json:"status"`
14+
Deleted bool `json:"deleted"`
15+
Count int64 `json:"count"`
16+
Address *Address `json:"address,omitempty"`
17+
Tags []map[string]any `json:"tags,omitempty"`
18+
Connection map[string]any `json:"connection"`
19+
Categories []string `json:"categories,omitempty"`
20+
Entity map[string]any `json:"entity"`
21+
}
22+

structs/flatten_test.go

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package structs
2+
3+
import (
4+
"go/ast"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/bradleyjkemp/cupaloy/v2"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestFlatten_Basic(t *testing.T) {
14+
dir := t.TempDir()
15+
err := Flatten("testdata/input.go", []StructConfig{
16+
{
17+
SourceName: "Finding",
18+
OutputName: "flatFinding",
19+
OutputFile: "finding_generated.go",
20+
},
21+
}, dir, WithPackageName("testpkg"))
22+
require.NoError(t, err)
23+
24+
got, err := os.ReadFile(filepath.Join(dir, "finding_generated.go"))
25+
require.NoError(t, err)
26+
cupaloy.SnapshotT(t, string(got))
27+
}
28+
29+
func TestFlatten_MultipleStructs(t *testing.T) {
30+
dir := t.TempDir()
31+
err := Flatten("testdata/input.go", []StructConfig{
32+
{
33+
SourceName: "Finding",
34+
OutputName: "flatFinding",
35+
OutputFile: "finding_generated.go",
36+
},
37+
{
38+
SourceName: "Simple",
39+
OutputName: "flatSimple",
40+
OutputFile: "simple_generated.go",
41+
},
42+
}, dir, WithPackageName("testpkg"))
43+
require.NoError(t, err)
44+
45+
got1, err := os.ReadFile(filepath.Join(dir, "finding_generated.go"))
46+
require.NoError(t, err)
47+
got2, err := os.ReadFile(filepath.Join(dir, "simple_generated.go"))
48+
require.NoError(t, err)
49+
cupaloy.SnapshotT(t, string(got1), string(got2))
50+
}
51+
52+
func TestFlatten_ExtraFields(t *testing.T) {
53+
dir := t.TempDir()
54+
err := Flatten("testdata/input.go", []StructConfig{
55+
{
56+
SourceName: "Simple",
57+
OutputName: "extendedSimple",
58+
OutputFile: "extended_generated.go",
59+
ExtraFields: []Field{
60+
{Name: "AssetType", Type: "string", JSONTag: "assetType,omitempty"},
61+
},
62+
},
63+
}, dir, WithPackageName("testpkg"))
64+
require.NoError(t, err)
65+
66+
got, err := os.ReadFile(filepath.Join(dir, "extended_generated.go"))
67+
require.NoError(t, err)
68+
cupaloy.SnapshotT(t, string(got))
69+
}
70+
71+
func TestFlatten_WithExtraScalars(t *testing.T) {
72+
dir := t.TempDir()
73+
// Treat Address as a scalar — it should stay typed instead of map[string]any.
74+
err := Flatten("testdata/input.go", []StructConfig{
75+
{
76+
SourceName: "Finding",
77+
OutputName: "flatFinding",
78+
OutputFile: "finding_generated.go",
79+
},
80+
}, dir, WithPackageName("testpkg"), WithExtraScalars("Address"))
81+
require.NoError(t, err)
82+
83+
got, err := os.ReadFile(filepath.Join(dir, "finding_generated.go"))
84+
require.NoError(t, err)
85+
cupaloy.SnapshotT(t, string(got))
86+
}
87+
88+
func TestFlatten_SortFields(t *testing.T) {
89+
dir := t.TempDir()
90+
err := Flatten("testdata/input.go", []StructConfig{
91+
{
92+
SourceName: "Finding",
93+
OutputName: "sortedFinding",
94+
OutputFile: "sorted_generated.go",
95+
},
96+
}, dir, WithPackageName("testpkg"), WithSortFields())
97+
require.NoError(t, err)
98+
99+
got, err := os.ReadFile(filepath.Join(dir, "sorted_generated.go"))
100+
require.NoError(t, err)
101+
cupaloy.SnapshotT(t, string(got))
102+
}
103+
104+
func TestFlatten_SortFieldsWithExtraFields(t *testing.T) {
105+
dir := t.TempDir()
106+
err := Flatten("testdata/input.go", []StructConfig{
107+
{
108+
SourceName: "Finding",
109+
OutputName: "sortedFinding",
110+
OutputFile: "sorted_generated.go",
111+
ExtraFields: []Field{
112+
{Name: "AssetType", Type: "string", JSONTag: "assetType,omitempty"},
113+
},
114+
},
115+
}, dir, WithPackageName("testpkg"), WithSortFields())
116+
require.NoError(t, err)
117+
118+
got, err := os.ReadFile(filepath.Join(dir, "sorted_generated.go"))
119+
require.NoError(t, err)
120+
cupaloy.SnapshotT(t, string(got))
121+
}
122+
123+
func TestFlatten_StructNotFound(t *testing.T) {
124+
dir := t.TempDir()
125+
err := Flatten("testdata/input.go", []StructConfig{
126+
{
127+
SourceName: "NonExistent",
128+
OutputName: "flat",
129+
OutputFile: "flat.go",
130+
},
131+
}, dir, WithPackageName("testpkg"))
132+
require.Error(t, err)
133+
require.Contains(t, err.Error(), `struct "NonExistent" not found`)
134+
}
135+
136+
func TestFlatten_FileNotFound(t *testing.T) {
137+
dir := t.TempDir()
138+
err := Flatten("testdata/nonexistent.go", []StructConfig{
139+
{SourceName: "X", OutputName: "Y", OutputFile: "y.go"},
140+
}, dir)
141+
require.Error(t, err)
142+
require.Contains(t, err.Error(), "nonexistent.go")
143+
}
144+
145+
func TestResolveType(t *testing.T) {
146+
typeKinds := map[string]string{
147+
"Severity": "string",
148+
"Address": "struct",
149+
"Tag": "struct",
150+
"Entity": "interface",
151+
}
152+
scalars := defaultScalarKinds
153+
154+
tests := []struct {
155+
name string
156+
expr ast.Expr
157+
want string
158+
}{
159+
{
160+
name: "scalar string",
161+
expr: &ast.Ident{Name: "string"},
162+
want: "string",
163+
},
164+
{
165+
name: "scalar bool",
166+
expr: &ast.Ident{Name: "bool"},
167+
want: "bool",
168+
},
169+
{
170+
name: "string-based enum",
171+
expr: &ast.Ident{Name: "Severity"},
172+
want: "string",
173+
},
174+
{
175+
name: "struct type",
176+
expr: &ast.Ident{Name: "Address"},
177+
want: "map[string]any",
178+
},
179+
{
180+
name: "pointer to scalar",
181+
expr: &ast.StarExpr{X: &ast.Ident{Name: "float64"}},
182+
want: "*float64",
183+
},
184+
{
185+
name: "pointer to struct",
186+
expr: &ast.StarExpr{X: &ast.Ident{Name: "Address"}},
187+
want: "map[string]any",
188+
},
189+
{
190+
name: "slice of scalars",
191+
expr: &ast.ArrayType{Elt: &ast.Ident{Name: "string"}},
192+
want: "[]string",
193+
},
194+
{
195+
name: "slice of structs",
196+
expr: &ast.ArrayType{Elt: &ast.Ident{Name: "Tag"}},
197+
want: "[]map[string]any",
198+
},
199+
{
200+
name: "slice of pointer to struct",
201+
expr: &ast.ArrayType{Elt: &ast.StarExpr{X: &ast.Ident{Name: "Tag"}}},
202+
want: "[]map[string]any",
203+
},
204+
{
205+
name: "cross-package type",
206+
expr: &ast.SelectorExpr{X: &ast.Ident{Name: "time"}, Sel: &ast.Ident{Name: "Time"}},
207+
want: "map[string]any",
208+
},
209+
{
210+
name: "interface type",
211+
expr: &ast.Ident{Name: "Entity"},
212+
want: "map[string]any",
213+
},
214+
{
215+
name: "map type",
216+
expr: &ast.MapType{Key: &ast.Ident{Name: "string"}, Value: &ast.Ident{Name: "string"}},
217+
want: "map[string]any",
218+
},
219+
}
220+
221+
for _, tt := range tests {
222+
t.Run(tt.name, func(t *testing.T) {
223+
got := resolveType(tt.expr, typeKinds, scalars)
224+
require.Equal(t, tt.want, got)
225+
})
226+
}
227+
}
228+
229+
func TestExtractJSONTag(t *testing.T) {
230+
tests := []struct {
231+
name string
232+
tag *ast.BasicLit
233+
want string
234+
}{
235+
{name: "nil tag", tag: nil, want: ""},
236+
{name: "simple", tag: &ast.BasicLit{Value: "`json:\"id\"`"}, want: "id"},
237+
{name: "with omitempty", tag: &ast.BasicLit{Value: "`json:\"name,omitempty\"`"}, want: "name,omitempty"},
238+
{name: "skip tag", tag: &ast.BasicLit{Value: "`json:\"-\"`"}, want: "-"},
239+
{name: "multiple tags", tag: &ast.BasicLit{Value: "`csv:\"id\" json:\"id\"`"}, want: "id"},
240+
{name: "no json tag", tag: &ast.BasicLit{Value: "`csv:\"id\"`"}, want: ""},
241+
}
242+
243+
for _, tt := range tests {
244+
t.Run(tt.name, func(t *testing.T) {
245+
got := extractJSONTag(tt.tag)
246+
require.Equal(t, tt.want, got)
247+
})
248+
}
249+
}

0 commit comments

Comments
 (0)