Skip to content

Commit

Permalink
Add SetDefaultsFrom to load defaults from a struct
Browse files Browse the repository at this point in the history
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 spf13#761.

If you need to support this scenario, call SetDefaultsFrom on the
destination struct before calling Unmarshal.

Signed-off-by: Carolyn Van Slyck <me@carolynvanslyck.com>
  • Loading branch information
carolynvs committed May 14, 2021
1 parent 36be6bf commit 59d9263
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
30 changes: 30 additions & 0 deletions viper.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
46 changes: 46 additions & 0 deletions viper_test.go
Expand Up @@ -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\"}")

Expand Down

0 comments on commit 59d9263

Please sign in to comment.