diff --git a/confext/confParser.go b/confext/confParser.go index d2996dd..7f54bf6 100644 --- a/confext/confParser.go +++ b/confext/confParser.go @@ -14,9 +14,21 @@ import ( // ApplyEnvOverrides overrides field values from environment variables // // fields must be tagged with `env:"env_key"` -func ApplyEnvOverrides[T any](c *T) error { - +// +// only works on exported fields +// +// fields without an env tag are ignored +// fields with an `env:"-"` tag are ignore +// +// sub-structs are recursively parsed (if they have an env tag) and the env-variable keys are delimited by the delim parameter +// sub-structs with `env:""` are also parsed, but the delimited is skipped (they are handled as if they were one level higher) +func ApplyEnvOverrides[T any](c *T, delim string) error { rval := reflect.ValueOf(c).Elem() + + return processEnvOverrides(rval, delim, "") +} + +func processEnvOverrides(rval reflect.Value, delim string, prefix string) error { rtyp := rval.Type() for i := 0; i < rtyp.NumField(); i++ { @@ -24,12 +36,36 @@ func ApplyEnvOverrides[T any](c *T) error { rsfield := rtyp.Field(i) rvfield := rval.Field(i) - envkey := rsfield.Tag.Get("env") - if envkey == "" { + if !rsfield.IsExported() { continue } - envval, efound := os.LookupEnv(envkey) + if rvfield.Kind() == reflect.Struct { + + envkey, found := rsfield.Tag.Lookup("env") + if !found || envkey == "-" { + continue + } + + subPrefix := prefix + if envkey != "" { + subPrefix = subPrefix + envkey + delim + } + + err := processEnvOverrides(rvfield, delim, subPrefix) + if err != nil { + return err + } + } + + envkey := rsfield.Tag.Get("env") + if envkey == "" || envkey == "-" { + continue + } + + fullEnvKey := prefix + envkey + + envval, efound := os.LookupEnv(fullEnvKey) if !efound { continue } @@ -38,86 +74,86 @@ func ApplyEnvOverrides[T any](c *T) error { rvfield.Set(reflect.ValueOf(envval)) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) + fmt.Printf("[CONF] Overwrite config '%s' () with '%s'\n", fullEnvKey, envval) } else if rvfield.Type() == reflect.TypeOf(int(0)) { envint, err := strconv.ParseInt(envval, 10, bits.UintSize) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(int(envint))) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval) } else if rvfield.Type() == reflect.TypeOf(int64(0)) { envint, err := strconv.ParseInt(envval, 10, 64) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int64 (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(int64(envint))) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval) } else if rvfield.Type() == reflect.TypeOf(int32(0)) { envint, err := strconv.ParseInt(envval, 10, 32) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(int32(envint))) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval) } else if rvfield.Type() == reflect.TypeOf(int8(0)) { envint, err := strconv.ParseInt(envval, 10, 8) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to int32 (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(int8(envint))) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, envval) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, envval) } else if rvfield.Type() == reflect.TypeOf(time.Duration(0)) { dur, err := timeext.ParseDurationShortString(envval) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to duration (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(dur)) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, dur.String()) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, dur.String()) } else if rvfield.Type() == reflect.TypeOf(time.UnixMilli(0)) { tim, err := time.Parse(time.RFC3339Nano, envval) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to time.time (value := '%s')", fullEnvKey, envval)) } rvfield.Set(reflect.ValueOf(tim)) - fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", envkey, tim.String()) + fmt.Printf("[CONF] Overwrite config '%s' with '%s'\n", fullEnvKey, tim.String()) } else if rvfield.Type().ConvertibleTo(reflect.TypeOf(int(0))) { envint, err := strconv.ParseInt(envval, 10, 8) if err != nil { - return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), envkey, envval)) + return errors.New(fmt.Sprintf("Failed to parse env-config variable '%s' to <%s, ,int> (value := '%s')", rvfield.Type().Name(), fullEnvKey, envval)) } envcvl := reflect.ValueOf(envint).Convert(rvfield.Type()) rvfield.Set(envcvl) - fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", envkey, envcvl.Interface()) + fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface()) } else if rvfield.Type().ConvertibleTo(reflect.TypeOf("")) { @@ -125,7 +161,7 @@ func ApplyEnvOverrides[T any](c *T) error { rvfield.Set(envcvl) - fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", envkey, envcvl.Interface()) + fmt.Printf("[CONF] Overwrite config '%s' with '%v'\n", fullEnvKey, envcvl.Interface()) } else { return errors.New(fmt.Sprintf("Unknown kind/type in config: [ %s | %s ]", rvfield.Kind().String(), rvfield.Type().String())) diff --git a/confext/confParser_test.go b/confext/confParser_test.go index f2ff7fc..ac558d5 100644 --- a/confext/confParser_test.go +++ b/confext/confParser_test.go @@ -1,6 +1,7 @@ package confext import ( + "gogs.mikescher.com/BlackForestBytes/goext/timeext" "testing" "time" ) @@ -40,7 +41,7 @@ func TestApplyEnvOverridesNoop(t *testing.T) { output := input - err := ApplyEnvOverrides(&output) + err := ApplyEnvOverrides(&output, ".") if err != nil { t.Errorf("%v", err) t.FailNow() @@ -92,7 +93,7 @@ func TestApplyEnvOverridesSimple(t *testing.T) { t.Setenv("TEST_V8", "1min4s") t.Setenv("TEST_V9", "2009-11-10T23:00:00Z") - err := ApplyEnvOverrides(&data) + err := ApplyEnvOverrides(&data, ".") if err != nil { t.Errorf("%v", err) t.FailNow() @@ -109,6 +110,109 @@ func TestApplyEnvOverridesSimple(t *testing.T) { assertEqual(t, data.V9, time.Unix(1257894000, 0).UTC()) } +func TestApplyEnvOverridesRecursive(t *testing.T) { + + type subdata struct { + V1 int `env:"SUB_V1"` + VX string `` + V2 string `env:"SUB_V2"` + V8 time.Duration `env:"SUB_V3"` + V9 time.Time `env:"SUB_V4"` + } + + type testdata struct { + V1 int `env:"TEST_V1"` + VX string `` + Sub1 subdata `` + Sub2 subdata `env:"TEST_V2"` + Sub3 subdata `env:"TEST_V3"` + Sub4 subdata `env:""` + V5 string `env:"-"` + } + + data := testdata{ + V1: 1, + VX: "2", + V5: "no", + Sub1: subdata{ + V1: 3, + VX: "4", + V2: "5", + V8: 6 * time.Second, + V9: time.Date(2000, 1, 7, 1, 1, 1, 0, time.UTC), + }, + Sub2: subdata{ + V1: 8, + VX: "9", + V2: "10", + V8: 11 * time.Second, + V9: time.Date(2000, 1, 12, 1, 1, 1, 0, timeext.TimezoneBerlin), + }, + Sub3: subdata{ + V1: 13, + VX: "14", + V2: "15", + V8: 16 * time.Second, + V9: time.Date(2000, 1, 17, 1, 1, 1, 0, timeext.TimezoneBerlin), + }, + Sub4: subdata{ + V1: 18, + VX: "19", + V2: "20", + V8: 21 * time.Second, + V9: time.Date(2000, 1, 22, 1, 1, 1, 0, timeext.TimezoneBerlin), + }, + } + + t.Setenv("TEST_V1", "999") + t.Setenv("-", "yes") + + t.Setenv("TEST_V2_SUB_V1", "846") + t.Setenv("TEST_V2_SUB_V2", "222_hello_world") + t.Setenv("TEST_V2_SUB_V3", "1min4s") + t.Setenv("TEST_V2_SUB_V4", "2009-11-10T23:00:00Z") + + t.Setenv("TEST_V3_SUB_V1", "33846") + t.Setenv("TEST_V3_SUB_V2", "33_hello_world") + t.Setenv("TEST_V3_SUB_V3", "33min4s") + t.Setenv("TEST_V3_SUB_V4", "2033-11-10T23:00:00Z") + + t.Setenv("SUB_V1", "11") + t.Setenv("SUB_V2", "22") + t.Setenv("SUB_V3", "33min") + t.Setenv("SUB_V4", "2044-01-01T00:00:00Z") + + err := ApplyEnvOverrides(&data, "_") + if err != nil { + t.Errorf("%v", err) + t.FailNow() + } + + assertEqual(t, data.V1, 999) + assertEqual(t, data.VX, "2") + assertEqual(t, data.V5, "no") + assertEqual(t, data.Sub1.V1, 3) + assertEqual(t, data.Sub1.VX, "4") + assertEqual(t, data.Sub1.V2, "5") + assertEqual(t, data.Sub1.V8, time.Second*6) + assertEqual(t, data.Sub1.V9, time.Unix(947206861, 0).UTC()) + assertEqual(t, data.Sub2.V1, 846) + assertEqual(t, data.Sub2.VX, "9") + assertEqual(t, data.Sub2.V2, "222_hello_world") + assertEqual(t, data.Sub2.V8, time.Second*64) + assertEqual(t, data.Sub2.V9, time.Unix(1257894000, 0).UTC()) + assertEqual(t, data.Sub3.V1, 33846) + assertEqual(t, data.Sub3.VX, "14") + assertEqual(t, data.Sub3.V2, "33_hello_world") + assertEqual(t, data.Sub3.V8, time.Second*1984) + assertEqual(t, data.Sub3.V9, time.Unix(2015276400, 0).UTC()) + assertEqual(t, data.Sub4.V1, 11) + assertEqual(t, data.Sub4.VX, "19") + assertEqual(t, data.Sub4.V2, "22") + assertEqual(t, data.Sub4.V8, time.Second*1980) + assertEqual(t, data.Sub4.V9, time.Unix(2335219200, 0).UTC()) +} + func assertEqual[T comparable](t *testing.T, actual T, expected T) { if actual != expected { t.Errorf("values differ: Actual: '%v', Expected: '%v'", actual, expected)