[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$
This commit is contained in:
akutz 2015-08-29 10:54:20 -05:00
parent 3c0ff861e3
commit 7a381db820
1 changed files with 44 additions and 10 deletions

View File

@ -150,6 +150,7 @@ type Viper struct {
pflags map[string]*pflag.Flag pflags map[string]*pflag.Flag
env map[string]string env map[string]string
aliases map[string]string aliases map[string]string
typeByDefValue bool
} }
// Returns an initialized Viper instance. // Returns an initialized Viper instance.
@ -164,6 +165,7 @@ func New() *Viper {
v.pflags = make(map[string]*pflag.Flag) v.pflags = make(map[string]*pflag.Flag)
v.env = make(map[string]string) v.env = make(map[string]string)
v.aliases = make(map[string]string) v.aliases = make(map[string]string)
v.typeByDefValue = false
return v 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 // Viper is essentially repository for configurations
// Get can retrieve any value given the key to use // Get can retrieve any value given the key to use
// Get has the behavior of returning the value associated with the first // 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{} { func (v *Viper) Get(key string) interface{} {
path := strings.Split(key, v.keyDelim) path := strings.Split(key, v.keyDelim)
val := v.find(strings.ToLower(key)) lcaseKey := strings.ToLower(key)
val := v.find(lcaseKey)
if val == nil { if val == nil {
source := v.find(path[0]) 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: case bool:
return cast.ToBool(val) return cast.ToBool(val)
case string: case string:
@ -406,7 +440,7 @@ func (v *Viper) Get(key string) interface{} {
case time.Duration: case time.Duration:
return cast.ToDuration(val) return cast.ToDuration(val)
case []string: case []string:
return val return cast.ToStringSlice(val)
} }
return val return val
} }