From f3638581fd798b2743656cb55042ae574427938f Mon Sep 17 00:00:00 2001 From: Samuel Nelson Date: Sun, 6 Nov 2016 00:17:30 -0600 Subject: [PATCH] Add more intricate TypeByDefaultValue logic In addition to testing the old code, we're now supporting more generic types. You could set the default value to a struct type, and with SetTypeByDefualtValue(true), the struct type will be filled in as expected, so long as the struct type has matching fields in the data. --- viper.go | 48 ++++++++++++++++++++++++++++++++++++++-- viper_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/viper.go b/viper.go index 4ed2d40..4a190f2 100644 --- a/viper.go +++ b/viper.go @@ -592,7 +592,6 @@ func (v *Viper) Get(key string) interface{} { valType := val if v.typeByDefValue { - // TODO(bep) this branch isn't covered by a single test. path := strings.Split(lcaseKey, v.keyDelim) defVal := v.searchMap(v.defaults, path) if defVal != nil { @@ -615,8 +614,53 @@ func (v *Viper) Get(key string) interface{} { return cast.ToDuration(val) case []string: return cast.ToStringSlice(val) + default: + return convert(reflect.ValueOf(val), reflect.TypeOf(valType)).Interface() + } +} + +func convert(v reflect.Value, typ reflect.Type) reflect.Value { + if v.Kind() == reflect.Interface { + return convert(v.Elem(), typ) + } + switch typ.Kind() { + case reflect.Ptr: + res := reflect.New(typ.Elem()) + res.Elem().Set(convert(v, typ.Elem())) + return res + case reflect.Struct: + if v.Kind() != reflect.Map { + return v + } + res := reflect.New(typ).Elem() + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldVal := v.MapIndex(reflect.ValueOf(field.Name)) + if !fieldVal.IsValid() { + fieldVal = v.MapIndex(reflect.ValueOf(strings.ToLower(field.Name))) + if !fieldVal.IsValid() { + continue + } + } + res.Field(i).Set(convert(fieldVal, field.Type)) + } + return res + case reflect.Slice, reflect.Array: + if v.Kind() != reflect.Slice { + return v + } + res := reflect.Zero(typ) + elemType := typ.Elem() + for i := 0; i < v.Len(); i++ { + res = reflect.Append(res, convert(v.Index(i), elemType)) + } + return res + default: + if !v.Type().ConvertibleTo(typ) { + return v + } + return v.Convert(typ) } - return val } // Sub returns new Viper instance representing a sub tree of this instance. diff --git a/viper_test.go b/viper_test.go index 60e75a3..ee437d3 100644 --- a/viper_test.go +++ b/viper_test.go @@ -789,6 +789,67 @@ func TestSub(t *testing.T) { assert.Equal(t, (*Viper)(nil), subv) } +var yamlTypeExample = []byte(` +hobbies: +- skateboarding +- snowboarding +languages: go golang python +job: + salary: 100,000 USD + medical: full +clothes: +- jacket: leather + trousers: denim + pants: + size: large +- jacket: denim + trousers: slacks + pants: + size: medium +`) + +type Clothing struct { + Jacket string + Trousers string + Pants Pants +} + +type Pants struct { + Size string +} + +type Job struct { + Salary string + Medical string +} + +func TestTypeByDefaultValue(t *testing.T) { + v := New() + v.SetConfigType("yaml") + v.SetDefault("hobbies", []string{}) + v.SetDefault("languages", []string{}) + v.SetDefault("job", &Job{}) + v.SetDefault("clothes", []Clothing{}) + v.SetTypeByDefaultValue(true) + err := v.ReadConfig(bytes.NewBuffer(yamlTypeExample)) + assert.NoError(t, err) + + assert.Equal(t, []string{"skateboarding", "snowboarding"}, v.Get("hobbies")) + assert.Equal(t, []string{"go", "golang", "python"}, v.Get("languages")) + assert.Equal(t, []Clothing{ + { + Jacket: "leather", + Trousers: "denim", + Pants: Pants{Size: "large"}, + }, + { + Jacket: "denim", + Trousers: "slacks", + Pants: Pants{Size: "medium"}, + }, + }, v.Get("clothes")) +} + var yamlMergeExampleTgt = []byte(` hello: pop: 37890