Skip to content

Commit 3a55a37

Browse files
committed
🐛 [config] excape mapstructure special tags when reporting an validation error
1 parent f24d24c commit 3a55a37

File tree

6 files changed

+235
-33
lines changed

6 files changed

+235
-33
lines changed

changes/20251014182838.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [field] Added utilities to return nil if a value is empty as opposed to a pointer to an empty value

changes/20251014182937.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:bug: [config] excape mapstructure special tags when reporting an validation error

utils/config/validation.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ package config
66

77
import (
88
"reflect"
9+
"strings"
10+
11+
"github.com/ARM-software/golang-utils/utils/collection"
12+
fieldUtils "github.com/ARM-software/golang-utils/utils/field"
913
)
1014

15+
var specialMapstructureTags = []string{"squash", "remain", "omitempty", "omitzero"} // See https://pkg.go.dev/github.com/go-viper/mapstructure/v2#section-readme
16+
1117
// ValidateEmbedded uses reflection to find embedded structs and validate them
1218
func ValidateEmbedded(cfg Validator) error {
1319
r := reflect.ValueOf(cfg).Elem()
@@ -32,10 +38,27 @@ func ValidateEmbedded(cfg Validator) error {
3238

3339
func wrapFieldValidationError(field reflect.StructField, err error) error {
3440
mapStructureStr, hasTag := field.Tag.Lookup("mapstructure")
35-
mapStructure := &mapStructureStr
41+
mapStructure := fieldUtils.ToOptionalStringOrNilIfEmpty(processMapStructureString(mapStructureStr))
3642
if !hasTag {
3743
mapStructure = nil
3844
}
3945
err = WrapFieldValidationError(field.Name, mapStructure, nil, err)
4046
return err
4147
}
48+
49+
// mapstructure has some special tags which need to be accounted for.
50+
func processMapStructureString(str string) string {
51+
processedStr := strings.TrimSpace(str)
52+
if processedStr == "-" {
53+
return ""
54+
}
55+
56+
elements := strings.Split(processedStr, ",")
57+
if len(elements) == 1 {
58+
return processedStr
59+
}
60+
elements = collection.GenericRemove(func(str1, str2 string) bool {
61+
return strings.EqualFold(strings.TrimSpace(str1), strings.TrimSpace(str2))
62+
}, elements, specialMapstructureTags...)
63+
return strings.TrimSpace(strings.Join(elements, ","))
64+
}

utils/config/validation_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func Test_processMapStructureString(t *testing.T) {
10+
tests := []struct {
11+
mapstructureTag string
12+
expectedProcessedTag string
13+
}{
14+
{},
15+
{
16+
mapstructureTag: " ",
17+
},
18+
{
19+
mapstructureTag: " - ",
20+
},
21+
{
22+
mapstructureTag: " , omitzero ",
23+
},
24+
{
25+
mapstructureTag: " ,omitempty , omitzero , SQUASH ",
26+
},
27+
{
28+
mapstructureTag: "test ,omitempty , omitzero , squash ",
29+
expectedProcessedTag: "test",
30+
},
31+
{
32+
mapstructureTag: "person_name",
33+
expectedProcessedTag: "person_name",
34+
},
35+
{
36+
mapstructureTag: " person_name ",
37+
expectedProcessedTag: "person_name",
38+
},
39+
{
40+
mapstructureTag: " person_name ,remain ",
41+
expectedProcessedTag: "person_name",
42+
},
43+
}
44+
45+
for i := range tests {
46+
test := tests[i]
47+
t.Run(test.mapstructureTag, func(t *testing.T) {
48+
assert.Equal(t, test.expectedProcessedTag, processMapStructureString(test.mapstructureTag))
49+
})
50+
}
51+
}

utils/field/fields.go

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,103 +6,157 @@
66
// package field provides utilities to set structure fields. It was inspired by the kubernetes package https://pkg.go.dev/k8s.io/utils/pointer.
77
package field
88

9-
import "time"
9+
import (
10+
"time"
11+
12+
"github.com/ARM-software/golang-utils/utils/reflection"
13+
)
1014

1115
// ToOptionalInt returns a pointer to an int
1216
func ToOptionalInt(f int) *int {
13-
return ToOptional(f)
17+
return ToOptional[int](f)
18+
}
19+
20+
// ToOptionalIntOrNilIfEmpty returns a pointer to an int unless it is empty and in that case returns nil.
21+
func ToOptionalIntOrNilIfEmpty(f int) *int {
22+
return ToOptionalOrNilIfEmpty[int](f)
1423
}
1524

1625
// OptionalInt returns the value of an optional field or else
1726
// returns defaultValue.
1827
func OptionalInt(ptr *int, defaultValue int) int {
19-
return Optional(ptr, defaultValue)
28+
return Optional[int](ptr, defaultValue)
2029
}
2130

2231
// ToOptionalInt32 returns a pointer to an int32.
2332
func ToOptionalInt32(f int32) *int32 {
24-
return ToOptional(f)
33+
return ToOptional[int32](f)
34+
}
35+
36+
// ToOptionalInt32OrNilIfEmpty returns a pointer to an int32 unless it is empty and in that case returns nil.
37+
func ToOptionalInt32OrNilIfEmpty(f int32) *int32 {
38+
return ToOptionalOrNilIfEmpty[int32](f)
2539
}
2640

2741
// OptionalInt32 returns the value of an optional field or else
2842
// returns defaultValue.
2943
func OptionalInt32(ptr *int32, defaultValue int32) int32 {
30-
return Optional(ptr, defaultValue)
44+
return Optional[int32](ptr, defaultValue)
3145
}
3246

3347
// ToOptionalUint returns a pointer to an uint
3448
func ToOptionalUint(f uint) *uint {
35-
return ToOptional(f)
49+
return ToOptional[uint](f)
50+
}
51+
52+
// ToOptionalUintOrNilIfEmpty returns a pointer to a Uint unless it is empty and in that case returns nil.
53+
func ToOptionalUintOrNilIfEmpty(f uint) *uint {
54+
return ToOptionalOrNilIfEmpty[uint](f)
3655
}
3756

3857
// OptionalUint returns the value of an optional field or else returns defaultValue.
3958
func OptionalUint(ptr *uint, defaultValue uint) uint {
40-
return Optional(ptr, defaultValue)
59+
return Optional[uint](ptr, defaultValue)
4160
}
4261

4362
// ToOptionalUint32 returns a pointer to an uint32.
4463
func ToOptionalUint32(f uint32) *uint32 {
45-
return ToOptional(f)
64+
return ToOptional[uint32](f)
65+
}
66+
67+
// ToOptionalUint32OrNilIfEmpty returns a pointer to an Uint32 unless it is empty and in that case returns nil.
68+
func ToOptionalUint32OrNilIfEmpty(f uint32) *uint32 {
69+
return ToOptionalOrNilIfEmpty[uint32](f)
4670
}
4771

4872
// OptionalUint32 returns the value of an optional field or else returns defaultValue.
4973
func OptionalUint32(ptr *uint32, defaultValue uint32) uint32 {
50-
return Optional(ptr, defaultValue)
74+
return Optional[uint32](ptr, defaultValue)
5175
}
5276

5377
// ToOptionalInt64 returns a pointer to an int64.
5478
func ToOptionalInt64(f int64) *int64 {
55-
return ToOptional(f)
79+
return ToOptional[int64](f)
80+
}
81+
82+
// ToOptionalInt64OrNilIfEmpty returns a pointer to an int64 unless it is empty and in that case returns nil.
83+
func ToOptionalInt64OrNilIfEmpty(f int64) *int64 {
84+
return ToOptionalOrNilIfEmpty[int64](f)
5685
}
5786

5887
// OptionalInt64 returns the value of an optional field or else returns defaultValue.
5988
func OptionalInt64(ptr *int64, defaultValue int64) int64 {
60-
return Optional(ptr, defaultValue)
89+
return Optional[int64](ptr, defaultValue)
6190
}
6291

6392
// ToOptionalUint64 returns a pointer to an uint64.
6493
func ToOptionalUint64(f uint64) *uint64 {
65-
return ToOptional(f)
94+
return ToOptional[uint64](f)
95+
}
96+
97+
// ToOptionalUint64OrNilIfEmpty returns a pointer to an Uint64 unless it is empty and in that case returns nil.
98+
func ToOptionalUint64OrNilIfEmpty(f uint64) *uint64 {
99+
return ToOptionalOrNilIfEmpty[uint64](f)
66100
}
67101

68102
// OptionalUint64 returns the value of an optional field or else returns defaultValue.
69103
func OptionalUint64(ptr *uint64, defaultValue uint64) uint64 {
70-
return Optional(ptr, defaultValue)
104+
return Optional[uint64](ptr, defaultValue)
71105
}
72106

73107
// ToOptionalBool returns a pointer to a bool.
74108
func ToOptionalBool(b bool) *bool {
75-
return ToOptional(b)
109+
return ToOptional[bool](b)
110+
}
111+
112+
// ToOptionalBoolOrNilIfEmpty returns a pointer to a boolean unless it is empty and in that case returns nil.
113+
func ToOptionalBoolOrNilIfEmpty(f bool) *bool {
114+
return ToOptionalOrNilIfEmpty[bool](f)
76115
}
77116

78117
// OptionalBool returns the value of an optional field or else returns defaultValue.
79118
func OptionalBool(ptr *bool, defaultValue bool) bool {
80-
return Optional(ptr, defaultValue)
119+
return Optional[bool](ptr, defaultValue)
81120
}
82121

83122
// ToOptionalString returns a pointer to a string.
84123
func ToOptionalString(s string) *string {
85-
return ToOptional(s)
124+
return ToOptional[string](s)
125+
}
126+
127+
// ToOptionalStringOrNilIfEmpty returns a pointer to a string unless it is empty and in that case returns nil.
128+
func ToOptionalStringOrNilIfEmpty(f string) *string {
129+
return ToOptionalOrNilIfEmpty[string](f)
86130
}
87131

88132
// OptionalString returns the value of an optional field or else returns defaultValue.
89133
func OptionalString(ptr *string, defaultValue string) string {
90-
return Optional(ptr, defaultValue)
134+
return Optional[string](ptr, defaultValue)
91135
}
92136

93-
// ToOptionalAny returns a pointer to a object.
137+
// ToOptionalAny returns a pointer to an object.
94138
func ToOptionalAny(a any) *any {
95-
return ToOptional(a)
139+
return ToOptional[any](a)
140+
}
141+
142+
// ToOptionalAnyOrNilIfEmpty returns a pointer to an object unless it is empty and in that case returns nil.
143+
func ToOptionalAnyOrNilIfEmpty(f any) *any {
144+
return ToOptionalOrNilIfEmpty[any](f)
96145
}
97146

98147
// OptionalAny returns the value of an optional field or else returns defaultValue.
99148
func OptionalAny(ptr *any, defaultValue any) any {
100-
return Optional(ptr, defaultValue)
149+
return Optional[any](ptr, defaultValue)
101150
}
102151

103152
// ToOptionalFloat32 returns a pointer to a float32.
104153
func ToOptionalFloat32(f float32) *float32 {
105-
return ToOptional(f)
154+
return ToOptional[float32](f)
155+
}
156+
157+
// ToOptionalFloat32OrNilIfEmpty returns a pointer to a float32 unless it is empty and in that case returns nil.
158+
func ToOptionalFloat32OrNilIfEmpty(f float32) *float32 {
159+
return ToOptionalOrNilIfEmpty[float32](f)
106160
}
107161

108162
// OptionalFloat32 returns the value of an optional field or else returns defaultValue.
@@ -112,39 +166,62 @@ func OptionalFloat32(ptr *float32, defaultValue float32) float32 {
112166

113167
// ToOptionalFloat64 returns a pointer to a float64.
114168
func ToOptionalFloat64(f float64) *float64 {
115-
return ToOptional(f)
169+
return ToOptional[float64](f)
170+
}
171+
172+
// ToOptionalFloat64OrNilIfEmpty returns a pointer to a float64 unless it is empty and in that case returns nil.
173+
func ToOptionalFloat64OrNilIfEmpty(f float64) *float64 {
174+
return ToOptionalOrNilIfEmpty[float64](f)
116175
}
117176

118177
// OptionalFloat64 returns the value of an optional field or else returns defaultValue.
119178
func OptionalFloat64(ptr *float64, defaultValue float64) float64 {
120-
return Optional(ptr, defaultValue)
179+
return Optional[float64](ptr, defaultValue)
121180
}
122181

123182
// ToOptionalDuration returns a pointer to a Duration.
124183
func ToOptionalDuration(f time.Duration) *time.Duration {
125-
return ToOptional(f)
184+
return ToOptional[time.Duration](f)
185+
}
186+
187+
// ToOptionalDurationOrNilIfEmpty returns a pointer to a duration unless it is empty and in that case returns nil.
188+
func ToOptionalDurationOrNilIfEmpty(f time.Duration) *time.Duration {
189+
return ToOptionalOrNilIfEmpty[time.Duration](f)
126190
}
127191

128192
// OptionalDuration returns the value of an optional field or else returns defaultValue.
129193
func OptionalDuration(ptr *time.Duration, defaultValue time.Duration) time.Duration {
130-
return Optional(ptr, defaultValue)
194+
return Optional[time.Duration](ptr, defaultValue)
131195
}
132196

133197
// ToOptionalTime returns a pointer to a Time.
134198
func ToOptionalTime(f time.Time) *time.Time {
135-
return ToOptional(f)
199+
return ToOptional[time.Time](f)
200+
}
201+
202+
// ToOptionalTimeOrNilIfEmpty returns a pointer to a time unless it is empty and in that case returns nil.
203+
func ToOptionalTimeOrNilIfEmpty(f time.Time) *time.Time {
204+
return ToOptionalOrNilIfEmpty[time.Time](f)
136205
}
137206

138207
// OptionalTime returns the value of an optional field or else returns defaultValue.
139208
func OptionalTime(ptr *time.Time, defaultValue time.Time) time.Time {
140-
return Optional(ptr, defaultValue)
209+
return Optional[time.Time](ptr, defaultValue)
141210
}
142211

143212
// ToOptional returns a pointer to the given field value.
144213
func ToOptional[T any](v T) *T {
145214
return &v
146215
}
147216

217+
// ToOptionalOrNilIfEmpty returns a pointer to the given field value unless it is empty and in that case returns nil.
218+
func ToOptionalOrNilIfEmpty[T any](v T) *T {
219+
if reflection.IsEmpty(v) {
220+
return nil
221+
}
222+
return ToOptional[T](v)
223+
}
224+
148225
// Optional returns the value of an optional field or else returns defaultValue.
149226
func Optional[T any](ptr *T, defaultValue T) T {
150227
if ptr != nil {

0 commit comments

Comments
 (0)