diff --git a/setter/setter.go b/setter/setter.go index b643ad0..30e062a 100644 --- a/setter/setter.go +++ b/setter/setter.go @@ -53,6 +53,54 @@ func SetDefault(dest interface{}, defaultValue ...interface{}) { } } +// Default assigns the first non-zero default value to `dest` +// if `dest`, itself, is the zero value of type T. +// To work with slices: use DefaultSlice +// +// var config struct { +// Verbose *bool +// Foo string +// Bar int +// } +// holster.Default(&config.Foo, "default") +// holster.Default(&config.Bar, 200) +// +// Supply additional default values and SetDefault will +// choose the first default that is not of zero value +// +// holster.SetDefault(&config.Foo, os.Getenv("FOO"), "default") +func Default[T comparable](dest *T, defaultValues ...T) { + if IsZeroNew(*dest) { + for _, value := range defaultValues { + if !IsZeroNew(value) { + *dest = value + return + } + } + } +} + +// DefaultSlice assigns the first non-empty default value to `dest` if +// `dest`, itself, is an empty slice. +func DefaultSlice[T comparable](dest *[]T, defaultValues ...[]T) { + if len(*dest) == 0 { + for _, value := range defaultValues { + if len(value) != 0 { + *dest = make([]T, len(value)) + copy(*dest, value) + return + } + } + } +} + +// IsZeroNew compares the given value to its Golang-specified zero value. +// It works for any type T that satisfies comparable. +func IsZeroNew[T comparable](value T) bool { + var zero T + return value == zero +} + // Assign the first value that is not empty or of zero value. // This panics if the value is not a pointer or if value and // default value are not of the same type. diff --git a/setter/setter_test.go b/setter/setter_test.go index c01aac8..85fe416 100644 --- a/setter/setter_test.go +++ b/setter/setter_test.go @@ -20,6 +20,7 @@ import ( "github.com/mailgun/holster/v4/setter" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIfEmpty(t *testing.T) { @@ -145,3 +146,151 @@ func TestIsNil(t *testing.T) { assert.True(t, setter.IsNil(thing)) assert.False(t, setter.IsNil(&MyImplementation{})) } + +// --------------------------------------------------------- + +var newStrRes string + +func BenchmarkSetterNew(b *testing.B) { + var r string + for i := 0; i < b.N; i++ { + setter.Default(&r, "", "", "42") + } + newStrRes = r +} + +var oldStrRes string + +func BenchmarkSetter(b *testing.B) { + var r string + for i := 0; i < b.N; i++ { + setter.SetDefault(&r, "", "", "42") + } + oldStrRes = r +} + +var newSliceRs []string + +func BenchmarkSetterNew_Slice(b *testing.B) { + r := make([]string, 0, 3) + b.ResetTimer() + for i := 0; i < b.N; i++ { + setter.DefaultSlice(&r, []string{}, []string{"welcome all", "to a benchmark", "of SILLY proportions"}) + } + newSliceRs = r +} + +var oldSliceRs []string + +func BenchmarkSetter_Slice(b *testing.B) { + r := make([]string, 0, 3) + b.ResetTimer() + for i := 0; i < b.N; i++ { + setter.SetDefault(&r, []string{""}, []string{"welcome all", "to a benchmark", "of SILLY proportions"}) + } + oldSliceRs = r +} + +func TestSetterNew_IfEmpty(t *testing.T) { + var conf struct { + Foo string + Bar int + } + assert.Equal(t, "", conf.Foo) + assert.Equal(t, 0, conf.Bar) + + // Should apply the default values + setter.Default(&conf.Foo, "default") + setter.Default(&conf.Bar, 200) + + assert.Equal(t, "default", conf.Foo) + assert.Equal(t, 200, conf.Bar) + + conf.Foo = "thrawn" + conf.Bar = 500 + + // Should NOT apply the default values + setter.Default(&conf.Foo, "default") + setter.Default(&conf.Bar, 200) + + assert.Equal(t, "thrawn", conf.Foo) + assert.Equal(t, 500, conf.Bar) +} + +func TestSetterNew_Slices(t *testing.T) { + var foo []string + require.Len(t, foo, 0) + + // Should apply the default values + setter.DefaultSlice(&foo, []string{"default"}) + require.Len(t, foo, 1) + assert.Equal(t, "default", foo[0]) + + foo = []string{"thrawn"} + + // Should NOT apply the default values + setter.DefaultSlice(&foo, []string{"default"}) + require.Len(t, foo, 1) + assert.Equal(t, "thrawn", foo[0]) +} + +func TestSetterNew_IfDefaultPrecedence(t *testing.T) { + var conf struct { + Foo string + Bar string + } + assert.Equal(t, "", conf.Foo) + assert.Equal(t, "", conf.Bar) + + // Should use the final default value + envValue := "" + setter.Default(&conf.Foo, envValue, "default") + assert.Equal(t, "default", conf.Foo) + + // Should use envValue + envValue = "bar" + setter.Default(&conf.Bar, envValue, "default") + assert.Equal(t, "bar", conf.Bar) +} + +func TestSetterNew_IsEmpty(t *testing.T) { + var count64 int64 + var thing string + + // Should return true + assert.Equal(t, true, setter.IsZeroNew(count64)) + assert.Equal(t, true, setter.IsZeroNew(thing)) + + thing = "thrawn" + count64 = int64(1) + assert.Equal(t, false, setter.IsZeroNew(count64)) + assert.Equal(t, false, setter.IsZeroNew(thing)) +} + +// Not possible now given compiler warnings +// func TestSetterNew_IfEmptyTypePanic(t *testing.T) { +// defer func() { +// if r := recover(); r != nil { +// assert.Equal(t, "reflect.Set: value of type int is not assignable to type string", r) +// } +// }() + +// var thing string +// // Should panic +// setter.Default(&thing, 1) +// assert.Fail(t, "Should have caught panic") +// } + +// Not possible now given argument is now a pointer to T +// func TestSetterNew_IfEmptyNonPtrPanic(t *testing.T) { +// defer func() { +// if r := recover(); r != nil { +// assert.Equal(t, "setter.SetDefault: Expected first argument to be of type reflect.Ptr", r) +// } +// }() + +// var thing string +// // Should panic +// setter.Default(thing, "thrawn") +// assert.Fail(t, "Should have caught panic") +// }