Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20251014182838.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: [field] Added utilities to return nil if a value is empty as opposed to a pointer to an empty value
1 change: 1 addition & 0 deletions changes/20251014182937.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:bug: `[config]` escape mapstructure special tags when reporting an validation error
25 changes: 24 additions & 1 deletion utils/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ package config

import (
"reflect"
"strings"

"github.com/ARM-software/golang-utils/utils/collection"
fieldUtils "github.com/ARM-software/golang-utils/utils/field"
)

var specialMapstructureTags = []string{"squash", "remain", "omitempty", "omitzero"} // See https://pkg.go.dev/github.com/go-viper/mapstructure/v2#section-readme

// ValidateEmbedded uses reflection to find embedded structs and validate them
func ValidateEmbedded(cfg Validator) error {
r := reflect.ValueOf(cfg).Elem()
Expand All @@ -32,10 +38,27 @@ func ValidateEmbedded(cfg Validator) error {

func wrapFieldValidationError(field reflect.StructField, err error) error {
mapStructureStr, hasTag := field.Tag.Lookup("mapstructure")
mapStructure := &mapStructureStr
mapStructure := fieldUtils.ToOptionalStringOrNilIfEmpty(processMapStructureString(mapStructureStr))
if !hasTag {
mapStructure = nil
}
err = WrapFieldValidationError(field.Name, mapStructure, nil, err)
return err
}

// mapstructure has some special tags which need to be accounted for.
func processMapStructureString(str string) string {
processedStr := strings.TrimSpace(str)
if processedStr == "-" {
return ""
}

elements := strings.Split(processedStr, ",")
if len(elements) == 1 {
return processedStr
}
elements = collection.GenericRemove(func(str1, str2 string) bool {
return strings.EqualFold(strings.TrimSpace(str1), strings.TrimSpace(str2))
}, elements, specialMapstructureTags...)
return strings.TrimSpace(strings.Join(elements, ","))
}
51 changes: 51 additions & 0 deletions utils/config/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_processMapStructureString(t *testing.T) {
tests := []struct {
mapstructureTag string
expectedProcessedTag string
}{
{},
{
mapstructureTag: " ",
},
{
mapstructureTag: " - ",
},
{
mapstructureTag: " , omitzero ",
},
{
mapstructureTag: " ,omitempty , omitzero , SQUASH ",
},
{
mapstructureTag: "test ,omitempty , omitzero , squash ",
expectedProcessedTag: "test",
},
{
mapstructureTag: "person_name",
expectedProcessedTag: "person_name",
},
{
mapstructureTag: " person_name ",
expectedProcessedTag: "person_name",
},
{
mapstructureTag: " person_name ,remain ",
expectedProcessedTag: "person_name",
},
}

for i := range tests {
test := tests[i]
t.Run(test.mapstructureTag, func(t *testing.T) {
assert.Equal(t, test.expectedProcessedTag, processMapStructureString(test.mapstructureTag))
})
}
}
131 changes: 104 additions & 27 deletions utils/field/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,103 +6,157 @@
// package field provides utilities to set structure fields. It was inspired by the kubernetes package https://pkg.go.dev/k8s.io/utils/pointer.
package field

import "time"
import (
"time"

"github.com/ARM-software/golang-utils/utils/value"
)

// ToOptionalInt returns a pointer to an int
func ToOptionalInt(f int) *int {
return ToOptional(f)
return ToOptional[int](f)
}

// ToOptionalIntOrNilIfEmpty returns a pointer to an int unless it is empty and in that case returns nil.
func ToOptionalIntOrNilIfEmpty(f int) *int {
return ToOptionalOrNilIfEmpty[int](f)
}

// OptionalInt returns the value of an optional field or else
// returns defaultValue.
func OptionalInt(ptr *int, defaultValue int) int {
return Optional(ptr, defaultValue)
return Optional[int](ptr, defaultValue)
}

// ToOptionalInt32 returns a pointer to an int32.
func ToOptionalInt32(f int32) *int32 {
return ToOptional(f)
return ToOptional[int32](f)
}

// ToOptionalInt32OrNilIfEmpty returns a pointer to an int32 unless it is empty and in that case returns nil.
func ToOptionalInt32OrNilIfEmpty(f int32) *int32 {
return ToOptionalOrNilIfEmpty[int32](f)
}

// OptionalInt32 returns the value of an optional field or else
// returns defaultValue.
func OptionalInt32(ptr *int32, defaultValue int32) int32 {
return Optional(ptr, defaultValue)
return Optional[int32](ptr, defaultValue)
}

// ToOptionalUint returns a pointer to an uint
func ToOptionalUint(f uint) *uint {
return ToOptional(f)
return ToOptional[uint](f)
}

// ToOptionalUintOrNilIfEmpty returns a pointer to a Uint unless it is empty and in that case returns nil.
func ToOptionalUintOrNilIfEmpty(f uint) *uint {
return ToOptionalOrNilIfEmpty[uint](f)
}

// OptionalUint returns the value of an optional field or else returns defaultValue.
func OptionalUint(ptr *uint, defaultValue uint) uint {
return Optional(ptr, defaultValue)
return Optional[uint](ptr, defaultValue)
}

// ToOptionalUint32 returns a pointer to an uint32.
func ToOptionalUint32(f uint32) *uint32 {
return ToOptional(f)
return ToOptional[uint32](f)
}

// ToOptionalUint32OrNilIfEmpty returns a pointer to an Uint32 unless it is empty and in that case returns nil.
func ToOptionalUint32OrNilIfEmpty(f uint32) *uint32 {
return ToOptionalOrNilIfEmpty[uint32](f)
}

// OptionalUint32 returns the value of an optional field or else returns defaultValue.
func OptionalUint32(ptr *uint32, defaultValue uint32) uint32 {
return Optional(ptr, defaultValue)
return Optional[uint32](ptr, defaultValue)
}

// ToOptionalInt64 returns a pointer to an int64.
func ToOptionalInt64(f int64) *int64 {
return ToOptional(f)
return ToOptional[int64](f)
}

// ToOptionalInt64OrNilIfEmpty returns a pointer to an int64 unless it is empty and in that case returns nil.
func ToOptionalInt64OrNilIfEmpty(f int64) *int64 {
return ToOptionalOrNilIfEmpty[int64](f)
}

// OptionalInt64 returns the value of an optional field or else returns defaultValue.
func OptionalInt64(ptr *int64, defaultValue int64) int64 {
return Optional(ptr, defaultValue)
return Optional[int64](ptr, defaultValue)
}

// ToOptionalUint64 returns a pointer to an uint64.
func ToOptionalUint64(f uint64) *uint64 {
return ToOptional(f)
return ToOptional[uint64](f)
}

// ToOptionalUint64OrNilIfEmpty returns a pointer to an Uint64 unless it is empty and in that case returns nil.
func ToOptionalUint64OrNilIfEmpty(f uint64) *uint64 {
return ToOptionalOrNilIfEmpty[uint64](f)
}

// OptionalUint64 returns the value of an optional field or else returns defaultValue.
func OptionalUint64(ptr *uint64, defaultValue uint64) uint64 {
return Optional(ptr, defaultValue)
return Optional[uint64](ptr, defaultValue)
}

// ToOptionalBool returns a pointer to a bool.
func ToOptionalBool(b bool) *bool {
return ToOptional(b)
return ToOptional[bool](b)
}

// ToOptionalBoolOrNilIfEmpty returns a pointer to a boolean unless it is empty and in that case returns nil.
func ToOptionalBoolOrNilIfEmpty(f bool) *bool {
return ToOptionalOrNilIfEmpty[bool](f)
}

// OptionalBool returns the value of an optional field or else returns defaultValue.
func OptionalBool(ptr *bool, defaultValue bool) bool {
return Optional(ptr, defaultValue)
return Optional[bool](ptr, defaultValue)
}

// ToOptionalString returns a pointer to a string.
func ToOptionalString(s string) *string {
return ToOptional(s)
return ToOptional[string](s)
}

// ToOptionalStringOrNilIfEmpty returns a pointer to a string unless it is empty and in that case returns nil.
func ToOptionalStringOrNilIfEmpty(f string) *string {
return ToOptionalOrNilIfEmpty[string](f)
}

// OptionalString returns the value of an optional field or else returns defaultValue.
func OptionalString(ptr *string, defaultValue string) string {
return Optional(ptr, defaultValue)
return Optional[string](ptr, defaultValue)
}

// ToOptionalAny returns a pointer to a object.
// ToOptionalAny returns a pointer to an object.
func ToOptionalAny(a any) *any {
return ToOptional(a)
return ToOptional[any](a)
}

// ToOptionalAnyOrNilIfEmpty returns a pointer to an object unless it is empty and in that case returns nil.
func ToOptionalAnyOrNilIfEmpty(f any) *any {
return ToOptionalOrNilIfEmpty[any](f)
}

// OptionalAny returns the value of an optional field or else returns defaultValue.
func OptionalAny(ptr *any, defaultValue any) any {
return Optional(ptr, defaultValue)
return Optional[any](ptr, defaultValue)
}

// ToOptionalFloat32 returns a pointer to a float32.
func ToOptionalFloat32(f float32) *float32 {
return ToOptional(f)
return ToOptional[float32](f)
}

// ToOptionalFloat32OrNilIfEmpty returns a pointer to a float32 unless it is empty and in that case returns nil.
func ToOptionalFloat32OrNilIfEmpty(f float32) *float32 {
return ToOptionalOrNilIfEmpty[float32](f)
}

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

// ToOptionalFloat64 returns a pointer to a float64.
func ToOptionalFloat64(f float64) *float64 {
return ToOptional(f)
return ToOptional[float64](f)
}

// ToOptionalFloat64OrNilIfEmpty returns a pointer to a float64 unless it is empty and in that case returns nil.
func ToOptionalFloat64OrNilIfEmpty(f float64) *float64 {
return ToOptionalOrNilIfEmpty[float64](f)
}

// OptionalFloat64 returns the value of an optional field or else returns defaultValue.
func OptionalFloat64(ptr *float64, defaultValue float64) float64 {
return Optional(ptr, defaultValue)
return Optional[float64](ptr, defaultValue)
}

// ToOptionalDuration returns a pointer to a Duration.
func ToOptionalDuration(f time.Duration) *time.Duration {
return ToOptional(f)
return ToOptional[time.Duration](f)
}

// ToOptionalDurationOrNilIfEmpty returns a pointer to a duration unless it is empty and in that case returns nil.
func ToOptionalDurationOrNilIfEmpty(f time.Duration) *time.Duration {
return ToOptionalOrNilIfEmpty[time.Duration](f)
}

// OptionalDuration returns the value of an optional field or else returns defaultValue.
func OptionalDuration(ptr *time.Duration, defaultValue time.Duration) time.Duration {
return Optional(ptr, defaultValue)
return Optional[time.Duration](ptr, defaultValue)
}

// ToOptionalTime returns a pointer to a Time.
func ToOptionalTime(f time.Time) *time.Time {
return ToOptional(f)
return ToOptional[time.Time](f)
}

// ToOptionalTimeOrNilIfEmpty returns a pointer to a time unless it is empty and in that case returns nil.
func ToOptionalTimeOrNilIfEmpty(f time.Time) *time.Time {
return ToOptionalOrNilIfEmpty[time.Time](f)
}

// OptionalTime returns the value of an optional field or else returns defaultValue.
func OptionalTime(ptr *time.Time, defaultValue time.Time) time.Time {
return Optional(ptr, defaultValue)
return Optional[time.Time](ptr, defaultValue)
}

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

// ToOptionalOrNilIfEmpty returns a pointer to the given field value unless it is empty and in that case returns nil.
func ToOptionalOrNilIfEmpty[T any](v T) *T {
if value.IsEmpty(v) {
return nil
}
return ToOptional[T](v)
}

// Optional returns the value of an optional field or else returns defaultValue.
func Optional[T any](ptr *T, defaultValue T) T {
if ptr != nil {
Expand Down
Loading
Loading