Skip to content

Commit 0c88295

Browse files
authored
Merge pull request #674 from hanXen/feat/filter-struct-to-map
feat(structs): add `FilterStructToMap` utility function
2 parents 28d2b56 + fcc6514 commit 0c88295

File tree

2 files changed

+151
-12
lines changed

2 files changed

+151
-12
lines changed

structs/structs.go

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,13 @@ func Walk(s interface{}, callback CallbackFunc) {
4040
}
4141
}
4242

43-
// FilterStruct filters the struct based on include and exclude fields and returns a new struct.
44-
// - input: the original struct.
45-
// - includeFields: list of fields to include (if empty, includes all).
46-
// - excludeFields: list of fields to exclude (processed after include).
47-
func FilterStruct[T any](input T, includeFields, excludeFields []string) (T, error) {
48-
var zeroValue T
43+
func walkFilteredFields[T any](input T, includeFields, excludeFields []string, walker func(field reflect.StructField, value reflect.Value)) error {
4944
val := reflect.ValueOf(input)
5045
if val.Kind() == reflect.Ptr {
5146
val = val.Elem()
5247
}
53-
5448
if val.Kind() != reflect.Struct {
55-
return zeroValue, errors.New("input must be a struct")
49+
return errors.New("input must be a struct")
5650
}
5751

5852
includeMap := make(map[string]bool)
@@ -66,7 +60,6 @@ func FilterStruct[T any](input T, includeFields, excludeFields []string) (T, err
6660
}
6761

6862
typeOfStruct := val.Type()
69-
filteredStruct := reflect.New(typeOfStruct).Elem()
7063

7164
for i := 0; i < val.NumField(); i++ {
7265
field := typeOfStruct.Field(i)
@@ -77,13 +70,62 @@ func FilterStruct[T any](input T, includeFields, excludeFields []string) (T, err
7770
fieldValue := val.Field(i)
7871

7972
if (len(includeMap) == 0 || includeMap[fieldName]) && !excludeMap[fieldName] {
80-
filteredStruct.Field(i).Set(fieldValue)
73+
walker(field, fieldValue)
8174
}
8275
}
76+
return nil
77+
}
78+
79+
// FilterStruct filters the struct based on include and exclude fields and returns a new struct.
80+
// - input: the original struct.
81+
// - includeFields: list of fields to include (if empty, includes all).
82+
// - excludeFields: list of fields to exclude (processed after include).
83+
func FilterStruct[T any](input T, includeFields, excludeFields []string) (T, error) {
84+
var zeroValue T
85+
val := reflect.ValueOf(input)
86+
if val.Kind() == reflect.Ptr {
87+
val = val.Elem()
88+
}
89+
90+
filteredStruct := reflect.New(val.Type()).Elem()
91+
92+
walker := func(field reflect.StructField, value reflect.Value) {
93+
filteredStruct.FieldByName(field.Name).Set(value)
94+
}
95+
96+
if err := walkFilteredFields(input, includeFields, excludeFields, walker); err != nil {
97+
return zeroValue, err
98+
}
8399

84100
return filteredStruct.Interface().(T), nil
85101
}
86102

103+
func FilterStructToMap[T any](input T, includeFields, excludeFields []string) (map[string]any, error) {
104+
resultMap := make(map[string]any)
105+
106+
walker := func(field reflect.StructField, value reflect.Value) {
107+
jsonTag := field.Tag.Get("json")
108+
jsonKey := strings.Split(jsonTag, ",")[0]
109+
110+
if jsonKey == "" || jsonKey == "-" {
111+
return
112+
}
113+
114+
fieldValue := value.Interface()
115+
if strings.Contains(jsonTag, "omitempty") && value.IsZero() {
116+
return
117+
}
118+
119+
resultMap[jsonKey] = fieldValue
120+
}
121+
122+
if err := walkFilteredFields(input, includeFields, excludeFields, walker); err != nil {
123+
return nil, err
124+
}
125+
126+
return resultMap, nil
127+
}
128+
87129
// GetStructFields returns all the top-level field names from the given struct.
88130
// - input: the original struct.
89131
// Returns a slice of field names or an error if the input is not a struct.

structs/structs_test.go

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ type NestedStruct struct {
1616
PtrField *TestStruct
1717
}
1818

19+
type MapTestStruct struct {
20+
Name string `json:"name"`
21+
Age int `json:"age,omitempty"`
22+
Password string `json:"-"`
23+
IsActive bool `json:"is_active"`
24+
Address string `json:"address"`
25+
Country string `json:"country,omitempty"`
26+
}
27+
1928
func TestFilterStruct(t *testing.T) {
2029
s := TestStruct{
2130
Name: "John",
@@ -25,7 +34,7 @@ func TestFilterStruct(t *testing.T) {
2534

2635
tests := []struct {
2736
name string
28-
input interface{}
37+
input any
2938
includeFields []string
3039
excludeFields []string
3140
want TestStruct
@@ -79,6 +88,94 @@ func TestFilterStruct(t *testing.T) {
7988
}
8089
}
8190

91+
func TestFilterStructToMap(t *testing.T) {
92+
s := MapTestStruct{
93+
Name: "John",
94+
Age: 30, // To test omitempty on a non zero value
95+
Password: "secret-password", // To test an ignored tag (json:"-")
96+
IsActive: true,
97+
Address: "New York",
98+
Country: "", // To test omitempty on a zero value
99+
}
100+
101+
tests := []struct {
102+
name string
103+
input any
104+
includeFields []string
105+
excludeFields []string
106+
want map[string]any
107+
wantErr bool
108+
}{
109+
{
110+
name: "no filtering",
111+
input: s,
112+
includeFields: nil,
113+
excludeFields: nil,
114+
want: map[string]any{
115+
"name": "John",
116+
"age": 30,
117+
"is_active": true,
118+
"address": "New York",
119+
},
120+
wantErr: false,
121+
},
122+
{
123+
name: "include specific fields",
124+
input: s,
125+
includeFields: []string{"Name", "Address"},
126+
excludeFields: []string{},
127+
want: map[string]any{
128+
"name": "John",
129+
"address": "New York",
130+
},
131+
wantErr: false,
132+
},
133+
{
134+
name: "exclude specific fields",
135+
input: s,
136+
includeFields: []string{},
137+
excludeFields: []string{"Address", "IsActive"},
138+
want: map[string]any{
139+
"name": "John",
140+
"age": 30,
141+
},
142+
wantErr: false,
143+
},
144+
{
145+
name: "include and exclude",
146+
input: s,
147+
includeFields: []string{"Name", "Age", "Address"},
148+
excludeFields: []string{"Age"},
149+
want: map[string]any{
150+
"name": "John",
151+
"address": "New York",
152+
},
153+
wantErr: false,
154+
},
155+
{
156+
name: "non-struct input",
157+
input: "not a struct",
158+
includeFields: []string{},
159+
excludeFields: []string{},
160+
want: nil,
161+
wantErr: true,
162+
},
163+
}
164+
165+
for _, tt := range tests {
166+
t.Run(tt.name, func(t *testing.T) {
167+
got, err := FilterStructToMap(tt.input, tt.includeFields, tt.excludeFields)
168+
if (err != nil) != tt.wantErr {
169+
t.Errorf("FilterStructToMap() error = %v, wantErr %v", err, tt.wantErr)
170+
return
171+
}
172+
if !reflect.DeepEqual(got, tt.want) {
173+
t.Errorf("FilterStructToMap() got = %v, want %v", got, tt.want)
174+
}
175+
})
176+
}
177+
}
178+
82179
func TestGetStructFields(t *testing.T) {
83180
s := TestStruct{
84181
Name: "John",
@@ -88,7 +185,7 @@ func TestGetStructFields(t *testing.T) {
88185

89186
tests := []struct {
90187
name string
91-
input interface{}
188+
input any
92189
want []string
93190
wantErr bool
94191
}{

0 commit comments

Comments
 (0)