package config import ( "strings" "github.com/mitchellh/mapstructure" "github.com/spf13/cast" "github.com/spf13/viper" ) // BindStructEnv binds struct using environment variables. // Note: borrowed, see https://github.com/spf13/viper/pull/1429 func BindStructEnv(input interface{}) error { envKeysMap := map[string]interface{}{} if err := mapstructure.Decode(input, &envKeysMap); err != nil { return err } structKeys := flattenAndMergeMap(map[string]bool{}, envKeysMap, "") for key := range structKeys { if err := viper.BindEnv(key); err != nil { return err } } return nil } // FlattenAndMergeMap recursively flattens the given map into a map[string]bool // of key paths (used as a set, easier to manipulate than a []string): // - each path is merged into a single key string, delimited with v.keyDelim // - if a path is shadowed by an earlier value in the initial shadow map, // it is skipped. // // The resulting set of paths is merged to the given shadow set at the same time. // Note: borrowed, see https://github.com/spf13/viper/pull/1429 func flattenAndMergeMap(shadow map[string]bool, m map[string]any, prefix string) map[string]bool { if shadow != nil && prefix != "" && shadow[prefix] { // prefix is shadowed => nothing more to flatten return shadow } if shadow == nil { shadow = make(map[string]bool) } var m2 map[string]any if prefix != "" { prefix += "." // assuming that delimiter is a dot. } for k, val := range m { fullKey := prefix + k switch val := val.(type) { case map[string]any: m2 = val case map[any]any: m2 = cast.ToStringMap(val) default: // immediate value shadow[strings.ToLower(fullKey)] = true continue } // recursively merge to shadow map shadow = flattenAndMergeMap(shadow, m2, fullKey) } return shadow }