From 049cf065a816eac4c7e49701dea4cfdee111ec5c Mon Sep 17 00:00:00 2001 From: Matheus Nogueira Date: Mon, 4 Dec 2023 12:40:21 -0300 Subject: [PATCH] feat: auto bind environment variables in viper.Unmarshal --- viper.go | 21 +++++++++++++++++++++ viper_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/viper.go b/viper.go index 7de2e78..5fd4248 100644 --- a/viper.go +++ b/viper.go @@ -1111,9 +1111,30 @@ func Unmarshal(rawVal any, opts ...DecoderConfigOption) error { } func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error { + err := v.autoBindEnvs(rawVal) + if err != nil { + return fmt.Errorf("could not auto bind environment variables: %w", err) + } + return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...)) } +func (v *Viper) autoBindEnvs(rawVal any) error { + envKeys := map[string]any{} + if err := mapstructure.Decode(rawVal, &envKeys); err != nil { + return fmt.Errorf("could not decode mapstructure: %w", err) + } + + structKeys := v.flattenAndMergeMap(map[string]bool{}, envKeys, "") + for key, _ := range structKeys { + if err := v.BindEnv(key); err != nil { + return fmt.Errorf(`could not bind env "%s": %w`, key, err) + } + } + + return nil +} + // defaultDecoderConfig returns default mapstructure.DecoderConfig with support // of time.Duration values & string slices. func defaultDecoderConfig(output any, opts ...DecoderConfigOption) *mapstructure.DecoderConfig { diff --git a/viper_test.go b/viper_test.go index 0e416e7..e270ea7 100644 --- a/viper_test.go +++ b/viper_test.go @@ -914,6 +914,37 @@ func TestUnmarshal(t *testing.T) { ) } +func TestUnmarshalWithEnvs(t *testing.T) { + type config struct { + Port int + Host string + Nested struct { + Value string + AnotherValue string + RenamedValue string `mapstructure:"another"` + } + } + + t.Setenv("CONFIG_PORT", "8080") + t.Setenv("CONFIG_HOST", "http://localhost") + t.Setenv("CONFIG_NESTED_VALUE", "baz") + t.Setenv("CONFIG_NESTED_ANOTHER", "value") + + v := New() + v.SetEnvPrefix("CONFIG") + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + configObject := config{} + err := v.Unmarshal(&configObject) + + require.NoError(t, err) + assert.Equal(t, 8080, configObject.Port) + assert.Equal(t, "http://localhost", configObject.Host) + assert.Equal(t, "baz", configObject.Nested.Value) + assert.Empty(t, configObject.Nested.AnotherValue) + assert.Equal(t, "value", configObject.Nested.RenamedValue) +} + func TestUnmarshalWithDecoderOptions(t *testing.T) { Set("credentials", "{\"foo\":\"bar\"}")