Skip to content

Commit 73dfb94

Browse files
feat: make Unmarshal work with AutomaticEnv
Co-authored-by: Filip Krakowski <krakowski@hhu.de> Signed-off-by: Mark Sagi-Kazar <mark.sagikazar@gmail.com>
1 parent 6ea31ae commit 73dfb94

File tree

2 files changed

+125
-1
lines changed

2 files changed

+125
-1
lines changed

viper.go

+26-1
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,32 @@ func Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
11111111
}
11121112

11131113
func (v *Viper) Unmarshal(rawVal any, opts ...DecoderConfigOption) error {
1114-
return decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
1114+
// TODO: make this optional?
1115+
structKeys, err := v.decodeStructKeys(rawVal, opts...)
1116+
if err != nil {
1117+
return err
1118+
}
1119+
1120+
// TODO: struct keys should be enough?
1121+
return decode(v.getSettings(append(v.AllKeys(), structKeys...)), defaultDecoderConfig(rawVal, opts...))
1122+
}
1123+
1124+
func (v *Viper) decodeStructKeys(input any, opts ...DecoderConfigOption) ([]string, error) {
1125+
var structKeyMap map[string]any
1126+
1127+
err := decode(input, defaultDecoderConfig(&structKeyMap, opts...))
1128+
if err != nil {
1129+
return nil, err
1130+
}
1131+
1132+
flattenedStructKeyMap := v.flattenAndMergeMap(map[string]bool{}, structKeyMap, "")
1133+
1134+
r := make([]string, 0, len(flattenedStructKeyMap))
1135+
for v := range flattenedStructKeyMap {
1136+
r = append(r, v)
1137+
}
1138+
1139+
return r, nil
11151140
}
11161141

11171142
// defaultDecoderConfig returns default mapstructure.DecoderConfig with support

viper_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,105 @@ func TestUnmarshalWithDecoderOptions(t *testing.T) {
948948
}, &C)
949949
}
950950

951+
func TestUnmarshalWithAutomaticEnv(t *testing.T) {
952+
t.Setenv("PORT", "1313")
953+
t.Setenv("NAME", "Steve")
954+
t.Setenv("DURATION", "1s1ms")
955+
t.Setenv("MODES", "1,2,3")
956+
t.Setenv("SECRET", "42")
957+
t.Setenv("FILESYSTEM_SIZE", "4096")
958+
959+
type AuthConfig struct {
960+
Secret string `mapstructure:"secret"`
961+
}
962+
963+
type StorageConfig struct {
964+
Size int `mapstructure:"size"`
965+
}
966+
967+
type Configuration struct {
968+
Port int `mapstructure:"port"`
969+
Name string `mapstructure:"name"`
970+
Duration time.Duration `mapstructure:"duration"`
971+
972+
// Infer name from struct
973+
Modes []int
974+
975+
// Squash nested struct (omit prefix)
976+
Authentication AuthConfig `mapstructure:",squash"`
977+
978+
// Different key
979+
Storage StorageConfig `mapstructure:"filesystem"`
980+
981+
// Omitted field
982+
Flag bool `mapstructure:"flag"`
983+
}
984+
985+
v := New()
986+
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
987+
v.AutomaticEnv()
988+
989+
t.Run("OK", func(t *testing.T) {
990+
var config Configuration
991+
if err := v.Unmarshal(&config); err != nil {
992+
t.Fatalf("unable to decode into struct, %v", err)
993+
}
994+
995+
assert.Equal(
996+
t,
997+
Configuration{
998+
Name: "Steve",
999+
Port: 1313,
1000+
Duration: time.Second + time.Millisecond,
1001+
Modes: []int{1, 2, 3},
1002+
Authentication: AuthConfig{
1003+
Secret: "42",
1004+
},
1005+
Storage: StorageConfig{
1006+
Size: 4096,
1007+
},
1008+
},
1009+
config,
1010+
)
1011+
})
1012+
1013+
t.Run("Precedence", func(t *testing.T) {
1014+
var config Configuration
1015+
1016+
v.Set("port", 1234)
1017+
if err := v.Unmarshal(&config); err != nil {
1018+
t.Fatalf("unable to decode into struct, %v", err)
1019+
}
1020+
1021+
assert.Equal(
1022+
t,
1023+
Configuration{
1024+
Name: "Steve",
1025+
Port: 1234,
1026+
Duration: time.Second + time.Millisecond,
1027+
Modes: []int{1, 2, 3},
1028+
Authentication: AuthConfig{
1029+
Secret: "42",
1030+
},
1031+
Storage: StorageConfig{
1032+
Size: 4096,
1033+
},
1034+
},
1035+
config,
1036+
)
1037+
})
1038+
1039+
t.Run("Unset", func(t *testing.T) {
1040+
var config Configuration
1041+
1042+
err := v.Unmarshal(&config, func(config *mapstructure.DecoderConfig) {
1043+
config.ErrorUnset = true
1044+
})
1045+
1046+
assert.Error(t, err, "expected viper.Unmarshal to return error due to unset field 'FLAG'")
1047+
})
1048+
}
1049+
9511050
func TestBindPFlags(t *testing.T) {
9521051
v := New() // create independent Viper object
9531052
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)

0 commit comments

Comments
 (0)