diff --git a/config/keyvalue/keyvalue.go b/config/keyvalue/keyvalue.go index 631bef2..7b4c3fb 100644 --- a/config/keyvalue/keyvalue.go +++ b/config/keyvalue/keyvalue.go @@ -23,21 +23,30 @@ func (kv KVSlice) Less(i, j int) bool { return kv[i].Key < kv[j].Key } func (kv KVSlice) Swap(i, j int) { kv[i], kv[j] = kv[j], kv[i] } // EnvKV turns a struct with `env` keys (used with go-simpler/env) into a standard formatted -// environment variable key/value pair list, one per line. Note you must dereference a pointer -// type to use this. This allows the composition of the config in this file with an extended -// form with a customized variant of realy to produce correct environment variables both read -// and write. +// environment variable key/value pair list, one per line. If a pointer to a struct is passed, +// it will be automatically dereferenced. This allows the composition of the config in this file +// with an extended form with a customized variant of realy to produce correct environment +// variables both read and write. func EnvKV(cfg any) (m KVSlice) { - t := reflect.TypeOf(cfg) + // Handle pointer types by dereferencing them + v := reflect.ValueOf(cfg) + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return // Return empty slice for nil pointers + } + return EnvKV(v.Elem().Interface()) + } + + t := v.Type() for i := 0; i < t.NumField(); i++ { k := t.Field(i).Tag.Get("env") - v := reflect.ValueOf(cfg).Field(i).Interface() + fieldValue := v.Field(i).Interface() var val string - switch u := v.(type) { + switch u := fieldValue.(type) { case string: val = u case int, int64, int32, uint64, uint32, bool, time.Duration: - val = fmt.Sprint(v) + val = fmt.Sprint(fieldValue) case []string: if len(u) > 0 { val = strings.Join(u, ",") @@ -52,7 +61,8 @@ func EnvKV(cfg any) (m KVSlice) { return } -// PrintEnv renders the key/values of a config.C to a provided io.Writer. +// PrintEnv renders the key/values of a config struct to a provided io.Writer. +// If a pointer to a struct is passed, it will be automatically dereferenced. func PrintEnv(cfg any, printer io.Writer) { _, _ = fmt.Fprintln(printer, "#!/usr/bin/env bash") kvs := EnvKV(cfg) diff --git a/config/keyvalue/keyvalue_test.go b/config/keyvalue/keyvalue_test.go index cd24eb8..3408409 100644 --- a/config/keyvalue/keyvalue_test.go +++ b/config/keyvalue/keyvalue_test.go @@ -90,10 +90,58 @@ func TestEnvKV(t *testing.T) { if !found { t.Errorf("Expected to find ADDITIONAL_FIELD with value 'additional' in embedded struct") } + + // Test case 3: Pointer to a struct + pointerStruct := &TestStruct{ + StringField: "pointer-string", + IntField: 123, + BoolField: false, + DurationField: 10 * time.Second, + StringSlice: []string{"four", "five", "six"}, + } + + pointerKVs := EnvKV(pointerStruct) + + // Expected key-value pairs for pointer struct + expectedPointer := KVSlice{ + {Key: "STRING_FIELD", Value: "pointer-string"}, + {Key: "INT_FIELD", Value: "123"}, + {Key: "BOOL_FIELD", Value: "false"}, + {Key: "DURATION_FIELD", Value: "10s"}, + {Key: "STRING_SLICE", Value: "four,five,six"}, + {Key: "EMPTY_SLICE", Value: ""}, + } + + // Check if the number of key-value pairs is correct + if len(pointerKVs) != len(expectedPointer) { + t.Errorf("Expected %d key-value pairs for pointer struct, got %d", len(expectedPointer), len(pointerKVs)) + } + + // Create a map for easier comparison + pointerKVMap := make(map[string]string) + for _, kv := range pointerKVs { + pointerKVMap[kv.Key] = kv.Value + } + + // Check if all expected key-value pairs are present + for _, kv := range expectedPointer { + if val, ok := pointerKVMap[kv.Key]; !ok || val != kv.Value { + t.Errorf("Expected key %s with value %s for pointer struct, got %s", kv.Key, kv.Value, val) + } + } + + // Test case 4: Nil pointer + var nilPointer *TestStruct = nil + nilKVs := EnvKV(nilPointer) + + // Nil pointer should return empty slice + if len(nilKVs) != 0 { + t.Errorf("Expected empty slice for nil pointer, got %d items", len(nilKVs)) + } } func TestPrintEnv(t *testing.T) { - // Create a test struct + // Test case 1: Regular struct testStruct := TestStruct{ StringField: "test-string", IntField: 42, @@ -123,6 +171,56 @@ func TestPrintEnv(t *testing.T) { if output != expected { t.Errorf("PrintEnv output does not match expected.\nExpected:\n%s\nGot:\n%s", expected, output) } + + // Test case 2: Pointer to struct + pointerStruct := &TestStruct{ + StringField: "pointer-string", + IntField: 123, + BoolField: false, + DurationField: 10 * time.Second, + StringSlice: []string{"four", "five", "six"}, + } + + // Create a new buffer for the pointer test + var pointerBuf bytes.Buffer + + // Call PrintEnv with a pointer + PrintEnv(pointerStruct, &pointerBuf) + + // Get the output as a string + pointerOutput := pointerBuf.String() + + // Expected output for pointer struct + expectedPointer := "#!/usr/bin/env bash\n" + + "export BOOL_FIELD=false\n" + + "export DURATION_FIELD=10s\n" + + "export EMPTY_SLICE=\n" + + "export INT_FIELD=123\n" + + "export STRING_FIELD=pointer-string\n" + + "export STRING_SLICE=four,five,six\n" + + if pointerOutput != expectedPointer { + t.Errorf("PrintEnv output for pointer struct does not match expected.\nExpected:\n%s\nGot:\n%s", + expectedPointer, pointerOutput) + } + + // Test case 3: Nil pointer + var nilPointer *TestStruct = nil + var nilBuf bytes.Buffer + + // Call PrintEnv with a nil pointer + PrintEnv(nilPointer, &nilBuf) + + // Get the output as a string + nilOutput := nilBuf.String() + + // Expected output for nil pointer (just the shebang line) + expectedNil := "#!/usr/bin/env bash\n" + + if nilOutput != expectedNil { + t.Errorf("PrintEnv output for nil pointer does not match expected.\nExpected:\n%s\nGot:\n%s", + expectedNil, nilOutput) + } } func TestKVSliceSorting(t *testing.T) {