From 59d9263a111f4128a4b17a8d93d77b8454857c4c Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Wed, 12 May 2021 15:30:39 -0500 Subject: [PATCH] Add SetDefaultsFrom to load defaults from a struct In cases where there isn't a configuration file, none of the fields on the struct are bound to environment variables unless BindEnv was explicitly called. AutomaticEnv essentially doesn't work with the config file per https://github.com/spf13/viper/issues/761. If you need to support this scenario, call SetDefaultsFrom on the destination struct before calling Unmarshal. Signed-off-by: Carolyn Van Slyck --- go.mod | 1 + go.sum | 2 ++ viper.go | 30 ++++++++++++++++++++++++++++++ viper_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+) diff --git a/go.mod b/go.mod index 812e7f3c6..eb2272802 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/hashicorp/hcl v1.0.0 + github.com/jeremywohl/flatten v1.0.1 github.com/magiconair/properties v1.8.1 github.com/mitchellh/mapstructure v1.3.3 github.com/pelletier/go-toml v1.2.0 diff --git a/go.sum b/go.sum index 87a929742..87c5f1b85 100644 --- a/go.sum +++ b/go.sum @@ -198,6 +198,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs= +github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= diff --git a/viper.go b/viper.go index 50b478029..7812f0d51 100644 --- a/viper.go +++ b/viper.go @@ -38,6 +38,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/printer" + "github.com/jeremywohl/flatten" "github.com/magiconair/properties" "github.com/mitchellh/mapstructure" "github.com/pelletier/go-toml" @@ -995,6 +996,35 @@ func (v *Viper) Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...)) } +// ReadDefaultsFrom applies the specified data to Viper's configuration +// using SetDefault. +// Useful for setting defaults from the specified structs in cases +// where a configuration file may not be present. +func ReadDefaultsFrom(val interface{}) error { + return v.SetDefaultsFrom(val) +} + +// ReadDefaultsFrom applies the specified data to Viper's configuration +// using SetDefault. +// Useful for setting defaults from the specified structs in cases +// where a configuration file may not be present. +func (v *Viper) SetDefaultsFrom(val interface{}) error { + var tmp map[string]interface{} + err := mapstructure.Decode(val, &tmp) + if err != nil { + return fmt.Errorf("error decoding configuration from struct: %v", err) + } + + defaults, err := flatten.Flatten(tmp, "", flatten.DotStyle) + if err != nil { + return fmt.Errorf("error flattening default configuration from struct: %v", err) + } + for defaultKey, defaultValue := range defaults { + v.SetDefault(defaultKey, defaultValue) + } + return nil +} + // defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot // of time.Duration values & string slices func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig { diff --git a/viper_test.go b/viper_test.go index 45bf8e9ba..eedfc5ec1 100644 --- a/viper_test.go +++ b/viper_test.go @@ -798,6 +798,52 @@ func TestUnmarshal(t *testing.T) { ) } +// Check that we can unmarshal environment variables without +// explicit bindings or a configuration file. +func TestUnmarshalWithoutConfigFile(t *testing.T) { + v := New() + v.AutomaticEnv() + + type Person struct { + Name string + } + type config struct { + Port int + Person Person + Color string + } + + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + testutil.Setenv(t, "PERSON_NAME", "Sally") + + defaults := config{ + Person: Person{ + Name: "default name", + }, + Port: 8080, + Color: "blue", + } + + err := v.SetDefaultsFrom(defaults) + if err != nil { + t.Fatalf("SetDefaultsFrom failed: %v", err) + } + v.SetDefault("port", 80) + + var C config + err = v.Unmarshal(&C) + if err != nil { + t.Fatalf("unable to decode into struct: %v", err) + } + + wantConfig := config{ + Color: "blue", // use default + Port: 80, // override with env var + Person: Person{Name: "Sally"}, // override nested with env var + } + assert.Equal(t, wantConfig, C) +} + func TestUnmarshalWithDecoderOptions(t *testing.T) { Set("credentials", "{\"foo\":\"bar\"}")