From 7a381db82011197e0cd5ddc1e79cbb921ee6a3c6 Mon Sep 17 00:00:00 2001 From: akutz Date: Sat, 29 Aug 2015 10:54:20 -0500 Subject: [PATCH] [110] Default Values Specify Type This patch adds a feature, if enabled, will infer a value's type from its default value no matter from where else the value is set. This is particularly important when working with environment variables. For example: package main import ( "fmt" "os" "github.com/spf13/viper" ) func print(name string, val interface{}) { fmt.Printf("%-15[1]s%-15[2]T%[2]v\n", name, val) } func main() { viper.BindEnv("mykey", "MYPREFIX_MYKEY") viper.SetDefault("mykey", []string{}) os.Setenv("MYPREFIX_MYKEY", "a b c") v1 := viper.GetStringSlice("mykey") v2 := viper.Get("mykey") print("v1", v1) print("v2", v2) } When this program is executed the following is emitted: [0]akutz@pax:ex$ ./ex1 v1 []string [a b c] v2 string a b c [0]akutz@pax:ex$ You may wonder, why is this important? Just use the GetStringSlice function. Well, it *becomes* important when dealing with marshaling. If we update the above program to this: package main import ( "fmt" "os" "github.com/spf13/viper" ) type Data struct { MyKey []string } func print(name string, val interface{}) { fmt.Printf("%-15[1]s%-15[2]T%[2]v\n", name, val) } func main() { viper.BindEnv("mykey", "MYPREFIX_MYKEY") viper.SetDefault("mykey", []string{}) os.Setenv("MYPREFIX_MYKEY", "a b c") v1 := viper.GetStringSlice("mykey") v2 := viper.Get("mykey") print("v1", v1) print("v2", v2) d := &Data{} viper.Marshal(d) print("d.MyKey", d.MyKey) } Now we can see the issue when we execute the updated program: [0]akutz@pax:ex$ ./ex2 v1 []string [a b c] v2 string a b c d.MyKey []string [] [0]akutz@pax:ex$ The marshalled data structure's field MyKey is empty when in fact it should have a string slice equal to, in value, []string {"a", "b", "c"}. The problem is that viper's Marshal function calls AllSettings which ultimately uses the Get function. The Get function does try to infer the value's type, but it does so using the type of the value retrieved using this logic: Get has the behavior of returning the value associated with the first place from where it is set. Viper will check in the following order: * override * flag * env * config file * key/value store * default While the above order is the one we want when retrieving the values, this patch enables users to decide if it's the order they want to be used when inferring a value's type. To that end the function SetTypeByDefaultValue is introduced. When SetTypeByDefaultValue(true) is called, a call to the Get function will now first check a key's default value, if set, when inferring a value's type. This is demonstrated using a modified version of the same program above: package main import ( "fmt" "os" "github.com/spf13/viper" ) type Data struct { MyKey []string } func print(name string, val interface{}) { fmt.Printf("%-15[1]s%-15[2]T%[2]v\n", name, val) } func main() { viper.BindEnv("mykey", "MYPREFIX_MYKEY") viper.SetDefault("mykey", []string{}) os.Setenv("MYPREFIX_MYKEY", "a b c") v1 := viper.GetStringSlice("mykey") v2 := viper.Get("mykey") print("v1", v1) print("v2", v2) d1 := &Data{} viper.Marshal(d1) print("d1.MyKey", d1.MyKey) viper.SetTypeByDefaultValue(true) d2 := &Data{} viper.Marshal(d2) print("d2.MyKey", d2.MyKey) } Now the following is emitted: [0]akutz@pax:ex$ ./ex3 v1 []string [a b c] v2 string a b c d1.MyKey []string [] d2.MyKey []string [a b c] [0]akutz@pax:ex$ --- viper.go | 54 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/viper.go b/viper.go index 5e01aa5..e249430 100644 --- a/viper.go +++ b/viper.go @@ -143,13 +143,14 @@ type Viper struct { automaticEnvApplied bool envKeyReplacer *strings.Replacer - config map[string]interface{} - override map[string]interface{} - defaults map[string]interface{} - kvstore map[string]interface{} - pflags map[string]*pflag.Flag - env map[string]string - aliases map[string]string + config map[string]interface{} + override map[string]interface{} + defaults map[string]interface{} + kvstore map[string]interface{} + pflags map[string]*pflag.Flag + env map[string]string + aliases map[string]string + typeByDefValue bool } // Returns an initialized Viper instance. @@ -164,6 +165,7 @@ func New() *Viper { v.pflags = make(map[string]*pflag.Flag) v.env = make(map[string]string) v.aliases = make(map[string]string) + v.typeByDefValue = false return v } @@ -368,6 +370,25 @@ func (v *Viper) searchMap(source map[string]interface{}, path []string) interfac } } +// SetTypeByDefaultValue enables or disables the inference of a key value's +// type when the Get function is used based upon a key's default value as +// opposed to the value returned based on the normal fetch logic. +// +// For example, if a key has a default value of []string{} and the same key +// is set via an environment variable to "a b c", a call to the Get function +// would return a string slice for the key if the key's type is inferred by +// the default value and the Get function would return: +// +// []string {"a", "b", "c"} +// +// Otherwise the Get function would return: +// +// "a b c" +func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) } +func (v *Viper) SetTypeByDefaultValue(enable bool) { + v.typeByDefValue = enable +} + // Viper is essentially repository for configurations // Get can retrieve any value given the key to use // Get has the behavior of returning the value associated with the first @@ -379,7 +400,8 @@ func Get(key string) interface{} { return v.Get(key) } func (v *Viper) Get(key string) interface{} { path := strings.Split(key, v.keyDelim) - val := v.find(strings.ToLower(key)) + lcaseKey := strings.ToLower(key) + val := v.find(lcaseKey) if val == nil { source := v.find(path[0]) @@ -392,7 +414,19 @@ func (v *Viper) Get(key string) interface{} { } } - switch val.(type) { + var valType interface{} + if !v.typeByDefValue { + valType = val + } else { + defVal, defExists := v.defaults[lcaseKey] + if defExists { + valType = defVal + } else { + valType = val + } + } + + switch valType.(type) { case bool: return cast.ToBool(val) case string: @@ -406,7 +440,7 @@ func (v *Viper) Get(key string) interface{} { case time.Duration: return cast.ToDuration(val) case []string: - return val + return cast.ToStringSlice(val) } return val }