Default values from Tags for missing config values
tburschka opened this issue · comments
It's not a bug in koanf, but i need help to find the right way to solve the issue.
I've created this repository with a minimal example:
https://github.com/tburschka/koanf-default-value
The idea is to define configuration values only "partially" and fill up the residual values via default tag.
Therefore i was using github.com/creasty/defaults for this, but my issue is that there are no additional information that the fields were set and aren't the initial values (e.g. false for a boolean).
My current approach is to create a custom provider which contains the current koanf (or confMap) to check against another confmap which holds only the default values from the target struct. (Not knowing how to create such a config map that holds the same structure).
Another approach which come to my mind while writing this is a custom DecodeHookFunc.
I'm a little bit lost and would be thankful for an idea how to solve this.
Edit:
An example can be found in the test:
https://github.com/tburschka/koanf-default-value/blob/main/config_test.go
I'm not sure if it fully solves your requirement of using struct tags, but maybe you can utilize the structs package to load the default config.
Ref:
https://github.com/knadh/koanf/blob/master/README.md?plain=1#L516-L538
So, how it can look like is this:
// hardcode it
var DefaultConfig MyConfig = DefaultConfig{
...
}
// or
func initDefaults() MyConfig {
myConf := MyConfig{}
if err := defaults.Set(&obj); err != nil {
...
}
return myConf
}
func InitConfig(fpath string) MyConfig {
// Load defaults from hardcoded struct.
k.Load(structs.Provider(DefaultConfig))
// or
// use the defaults package
k.Load(structs.Provider(initDefaults()))
// Override from file.
k.Load(toml.Provider(fpath))
// Override config struct with cli flags
k.Load(pflag.Provider())
}
I finally solved this. First approach was a custom provider, but i swapped to a custom decode hook:
https://github.com/tburschka/koanf-default-value/blob/main/decodehooks/defaults/defaults.go
package defaults
import (
"reflect"
"strings"
"github.com/go-viper/mapstructure/v2"
)
func Defaults(keyTag string, defaultTag string) mapstructure.DecodeHookFunc {
return func(f reflect.Value, t reflect.Value) (interface{}, error) {
tType := t.Type()
if tType.Kind() == reflect.Struct {
for i := 0; i < tType.NumField(); i++ {
setDefault(f, tType.Field(i), keyTag, defaultTag)
}
}
return f.Interface(), nil
}
}
func setDefault(f reflect.Value, t reflect.StructField, keyTag string, defaultTag string) {
if f.Kind() == reflect.Map {
key, _, _ := strings.Cut(t.Tag.Get(keyTag), ",")
val, ok := t.Tag.Lookup(defaultTag)
if !ok {
return
}
// check for key in the map
for _, e := range f.MapKeys() {
// key found, already set, nothing to do
if key == e.String() {
return
}
}
fVal := reflect.ValueOf(val)
// special handling for empty/missing structs
tType := t.Type
if tType.Kind() == reflect.Struct {
// create an empty map
fVal = reflect.ValueOf(map[string]any{})
for i := 0; i < tType.NumField(); i++ {
setDefault(fVal, tType.Field(i), keyTag, defaultTag)
}
}
// add missing key with default to the map
f.SetMapIndex(reflect.ValueOf(key), fVal)
}
}
it iterates over the structs and set the defaults. Note that this decodehook should be the first in the order:
config := &Config{}
_ = k.UnmarshalWithConf("", config, koanf.UnmarshalConf{
Tag: "yaml",
DecoderConfig: &mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
defaults.Defaults("yaml", "default"),
mapstructure.StringToTimeDurationHookFunc(),
stringtoslice.StringToSlice(),
stringtomap.StringToMap(),
),
Metadata: nil,
Result: config,
WeaklyTypedInput: true,
},
})
Also, it's important, that you set the defaults for slices []
and structs {}
if you want the decode hook to handle them.
Finally, since i use two sources, the default merge approach is not working, so i needed a custom merge func.
I use mergo with the WithSliceDeepCopy option.
import "dario.cat/mergo"
// [...]
_ = k.Load(file.Provider(name), yaml.Parser())
_ = k.Load(env2json.Provider(configEnvPrefix, configEnvDelim, func(s string) string {
return strings.ToLower(strings.TrimPrefix(s, configEnvPrefix+configEnvDelim))
}), json.Parser(), koanf.WithMergeFunc(func(src, dest map[string]interface{}) error {
return mergo.Merge(&dest, src, mergo.WithSliceDeepCopy)
}))
I've created tests for all these cases (https://github.com/tburschka/koanf-default-value/blob/main/config_test.go). It works like a charm. I keep it online so in case anyone needs this, he can use it...