Skip to content

Commit

Permalink
Implement viper.BindStruct for automatic unmarshalling from environme…
Browse files Browse the repository at this point in the history
…nt variables
  • Loading branch information
krakowski committed Sep 6, 2022
1 parent 57cc9a0 commit 0ac9227
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 1 deletion.
7 changes: 6 additions & 1 deletion README.md
Expand Up @@ -242,6 +242,7 @@ with ENV:

* `AutomaticEnv()`
* `BindEnv(string...) : error`
* `BindStruct(interface{}) : error`
* `SetEnvPrefix(string)`
* `SetEnvKeyReplacer(string...) *strings.Replacer`
* `AllowEmptyEnv(bool)`
Expand All @@ -251,7 +252,7 @@ treats ENV variables as case sensitive._

Viper provides a mechanism to try to ensure that ENV variables are unique. By
using `SetEnvPrefix`, you can tell Viper to use a prefix while reading from
the environment variables. Both `BindEnv` and `AutomaticEnv` will use this
the environment variables. `BindEnv`, `BindStruct` and `AutomaticEnv` will use this
prefix.

`BindEnv` takes one or more parameters. The first parameter is the key name, the
Expand All @@ -266,6 +267,10 @@ One important thing to recognize when working with ENV variables is that the
value will be read each time it is accessed. Viper does not fix the value when
the `BindEnv` is called.

`BindStruct` takes a struct pointer as its parameter and binds each (nested) struct
field to an environment variable using `BindEnv` and the field's name. A field's environment
variable name can be adjusted by setting the `mapstructure` tag.

`AutomaticEnv` is a powerful helper especially when combined with
`SetEnvPrefix`. When called, Viper will check for an environment variable any
time a `viper.Get` request is made. It will apply the following rules. It will
Expand Down
30 changes: 30 additions & 0 deletions viper.go
Expand Up @@ -1206,6 +1206,26 @@ func (v *Viper) BindEnv(input ...string) error {
return nil
}

// BindStruct binds a struct to the environment by extracting all Viper keys
// from it and binding each to a ENV variable.
func BindStruct(input interface{}) error { return v.BindStruct(input) }

func (v *Viper) BindStruct(input interface{}) error {
envKeysMap := map[string]interface{}{}
if err := mapstructure.Decode(input, &envKeysMap); err != nil {
return err
}

structKeys := v.flattenAndMergeMap(map[string]bool{}, envKeysMap, "")
for key, _ := range structKeys {
if err := v.BindEnv(key); err != nil {
return err
}
}

return nil
}

// MustBindEnv wraps BindEnv in a panic.
// If there is an error binding an environment variable, MustBindEnv will
// panic.
Expand All @@ -1217,6 +1237,16 @@ func (v *Viper) MustBindEnv(input ...string) {
}
}

// MustBindStruct wraps BindStruct in a panic.
// If there is an error binding the struct, BindStruct will panic.
func MustBindStruct(input interface{}) { v.MustBindStruct(input) }

func (v *Viper) MustBindStruct(input interface{}) {
if err := v.BindStruct(input); err != nil {
panic(fmt.Sprintf("error while binding struct: %v", err))
}
}

// Given a key, find the value.
//
// Viper will check to see if an alias exists first.
Expand Down
96 changes: 96 additions & 0 deletions viper_test.go
Expand Up @@ -927,6 +927,102 @@ func TestUnmarshal(t *testing.T) {
)
}

func TestBindStruct(t *testing.T) {
testutil.Setenv(t, "PORT", "1313")
testutil.Setenv(t, "NAME", "Steve")
testutil.Setenv(t, "DURATION", "1s1ms")
testutil.Setenv(t, "MODES", "1,2,3")
testutil.Setenv(t, "SECRET", "42")
testutil.Setenv(t, "FILESYSTEM_SIZE", "4096")

type AuthConfig struct {
Secret string `mapstructure:"SECRET"`
}

type StorageConfig struct {
Size int `mapstructure:"SIZE"`
}

type Configuration struct {
Port int `mapstructure:"PORT"`
Name string `mapstructure:"NAME"`
Duration time.Duration `mapstructure:"DURATION"`

// Here, the tag is left out on purpose so that
// BindStruct will use the field name.
Modes []int

// Here, the nested struct is squashed so that
// BindStruct will omit the "AUTHENTICATION_" prefix.
Authentication AuthConfig `mapstructure:",squash"`

// Here, the tag contains a different key so that
// BindStruct will use it instead of the field name.
Storage StorageConfig `mapstructure:"FILESYSTEM"`

// This field won't get set, since there is
// no environment variable set for it.
Flag bool `mapstructure:"FLAG"`
}

localViper := New()
localViper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

var config Configuration
if err := localViper.BindStruct(&config); err != nil {
t.Fatalf("unable to bind struct, %v", err)
}

if err := localViper.Unmarshal(&config); err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}

assert.Equal(
t,
&Configuration{
Name: "Steve",
Port: 1313,
Duration: time.Second + time.Millisecond,
Modes: []int{1, 2, 3},
Authentication: AuthConfig{
Secret: "42",
},
Storage: StorageConfig{
Size: 4096,
},
},
&config,
)

localViper.Set("port", 1234)
if err := localViper.Unmarshal(&config); err != nil {
t.Fatalf("unable to decode into struct, %v", err)
}

assert.Equal(
t,
&Configuration{
Name: "Steve",
Port: 1234,
Duration: time.Second + time.Millisecond,
Modes: []int{1, 2, 3},
Authentication: AuthConfig{
Secret: "42",
},
Storage: StorageConfig{
Size: 4096,
},
},
&config,
)

err := localViper.Unmarshal(&config, func(config *mapstructure.DecoderConfig) {
config.ErrorUnset = true
})

assert.NotNil(t, err, "expected viper.Unmarshal to return error due to unset field 'FLAG'")
}

func TestUnmarshalWithDecoderOptions(t *testing.T) {
Set("credentials", "{\"foo\":\"bar\"}")

Expand Down

0 comments on commit 0ac9227

Please sign in to comment.