commit b11959cda719dbc63e20bff82c6a901ce7143d3c Author: upstream Date: Fri Sep 4 08:08:58 2026 +0200 Upstream knadh/koanf state of the touched files before fdcdbdb436 diff --git a/koanf.go b/koanf.go new file mode 100644 index 0000000..92ac982 --- /dev/null +++ b/koanf.go @@ -0,0 +1,662 @@ +package koanf + +import ( + "bytes" + "encoding" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "sync" + + "github.com/go-viper/mapstructure/v2" + "github.com/knadh/koanf/maps" + "github.com/mitchellh/copystructure" +) + +// Koanf is the configuration apparatus. +type Koanf struct { + confMap map[string]any + confMapFlat map[string]any + keyMap KeyMap + conf Conf + mu sync.RWMutex +} + +// Conf is the Koanf configuration. +type Conf struct { + // Delim is the delimiter to use + // when specifying config key paths, for instance a . for `parent.child.key` + // or a / for `parent/child/key`. + Delim string + + // StrictMerge makes the merging behavior strict. + // Meaning when loading two files that have the same key, + // the first loaded file will define the desired type, and if the second file loads + // a different type will cause an error. + StrictMerge bool +} + +// KeyMap represents a map of flattened delimited keys and the non-delimited +// parts as their slices. For nested keys, the map holds all levels of path combinations. +// For example, the nested structure `parent -> child -> key` will produce the map: +// parent.child.key => [parent, child, key] +// parent.child => [parent, child] +// parent => [parent] +type KeyMap map[string][]string + +// UnmarshalConf represents configuration options used by +// Unmarshal() to unmarshal conf maps into arbitrary structs. +type UnmarshalConf struct { + // Tag is the struct field tag to unmarshal. + // `koanf` is used if left empty. + Tag string + + // If this is set to true, instead of unmarshalling nested structures + // based on the key path, keys are taken literally to unmarshal into + // a flat struct. For example: + // ``` + // type MyStuff struct { + // Child1Name string `koanf:"parent1.child1.name"` + // Child2Name string `koanf:"parent2.child2.name"` + // Type string `koanf:"json"` + // } + // ``` + FlatPaths bool + DecoderConfig *mapstructure.DecoderConfig +} + +// New returns a new instance of Koanf. delim is the delimiter to use +// when specifying config key paths, for instance a . for `parent.child.key` +// or a / for `parent/child/key`. +func New(delim string) *Koanf { + return NewWithConf(Conf{ + Delim: delim, + StrictMerge: false, + }) +} + +// NewWithConf returns a new instance of Koanf based on the Conf. +func NewWithConf(conf Conf) *Koanf { + return &Koanf{ + confMap: make(map[string]any), + confMapFlat: make(map[string]any), + keyMap: make(KeyMap), + conf: conf, + } +} + +// Load takes a Provider that either provides a parsed config map[string]any +// in which case pa (Parser) can be nil, or raw bytes to be parsed, where a Parser +// can be provided to parse. Additionally, options can be passed which modify the +// load behavior, such as passing a custom merge function. +func (ko *Koanf) Load(p Provider, pa Parser, opts ...Option) error { + var ( + mp map[string]any + err error + ) + + if p == nil { + return fmt.Errorf("load received a nil provider") + } + + // No Parser is given. Call the Provider's Read() method to get + // the config map. + if pa == nil { + mp, err = p.Read() + if err != nil { + return err + } + } else { + // There's a Parser. Get raw bytes from the Provider to parse. + b, err := p.ReadBytes() + if err != nil { + return err + } + mp, err = pa.Unmarshal(b) + if err != nil { + return err + } + } + + return ko.merge(mp, newOptions(opts)) +} + +// Keys returns the slice of all flattened keys in the loaded configuration +// sorted alphabetically. +func (ko *Koanf) Keys() []string { + ko.mu.RLock() + out := make([]string, 0, len(ko.confMapFlat)) + for k := range ko.confMapFlat { + out = append(out, k) + } + ko.mu.RUnlock() + sort.Strings(out) + return out +} + +// KeyMap returns a map of flattened keys and the individual parts of the +// key as slices. eg: "parent.child.key" => ["parent", "child", "key"]. +func (ko *Koanf) KeyMap() KeyMap { + ko.mu.RLock() + out := make(KeyMap, len(ko.keyMap)) + for key, parts := range ko.keyMap { + out[key] = make([]string, len(parts)) + copy(out[key], parts) + } + ko.mu.RUnlock() + return out +} + +// All returns a map of all flattened key paths and their values. +// Note that it uses maps.Copy to create a copy that uses +// json.Marshal which changes the numeric types to float64. +func (ko *Koanf) All() map[string]any { + ko.mu.RLock() + defer ko.mu.RUnlock() + return maps.Copy(ko.confMapFlat) +} + +// Raw returns a copy of the full raw conf map. +// Note that it uses maps.Copy to create a copy that uses +// json.Marshal which changes the numeric types to float64. +func (ko *Koanf) Raw() map[string]any { + ko.mu.RLock() + defer ko.mu.RUnlock() + return maps.Copy(ko.confMap) +} + +// Sprint returns a key -> value string representation +// of the config map with keys sorted alphabetically. +func (ko *Koanf) Sprint() string { + b := bytes.Buffer{} + keys := ko.Keys() + ko.mu.RLock() + for _, k := range keys { + v := ko.confMapFlat[k] + b.WriteString(fmt.Sprintf("%s -> %v\n", k, v)) + } + ko.mu.RUnlock() + return b.String() +} + +// Print prints a key -> value string representation +// of the config map with keys sorted alphabetically. +func (ko *Koanf) Print() { + fmt.Print(ko.Sprint()) +} + +// Cut cuts the config map at a given key path into a sub map and +// returns a new Koanf instance with the cut config map loaded. +// For instance, if the loaded config has a path that looks like +// parent.child.sub.a.b, `Cut("parent.child")` returns a new Koanf +// instance with the config map `sub.a.b` where everything above +// `parent.child` are cut out. +func (ko *Koanf) Cut(path string) *Koanf { + out := make(map[string]any) + + // Cut only makes sense if the requested key path is a map. + if v, ok := ko.Get(path).(map[string]any); ok { + out = v + } + + n := New(ko.conf.Delim) + _ = n.merge(out, new(options)) + return n +} + +// Copy returns a copy of the Koanf instance. +func (ko *Koanf) Copy() *Koanf { + return ko.Cut("") +} + +// Merge merges the config map of a given Koanf instance into +// the current instance. +func (ko *Koanf) Merge(in *Koanf) error { + return ko.merge(in.Raw(), new(options)) +} + +// MergeAt merges the config map of a given Koanf instance into +// the current instance as a sub map, at the given key path. +// If all or part of the key path is missing, it will be created. +// If the key path is `""`, this is equivalent to Merge. +func (ko *Koanf) MergeAt(in *Koanf, path string) error { + // No path. Merge the two config maps. + if path == "" { + return ko.Merge(in) + } + + // Unflatten the config map with the given key path. + n := maps.Unflatten(map[string]any{ + path: in.Raw(), + }, ko.conf.Delim) + + return ko.merge(n, new(options)) +} + +// Set sets the value at a specific key. +func (ko *Koanf) Set(key string, val any) error { + // Unflatten the config map with the given key path. + n := maps.Unflatten(map[string]any{ + key: val, + }, ko.conf.Delim) + + return ko.merge(n, new(options)) +} + +// Marshal takes a Parser implementation and marshals the config map into bytes, +// for example, to TOML or JSON bytes. +func (ko *Koanf) Marshal(p Parser) ([]byte, error) { + return p.Marshal(ko.Raw()) +} + +// Unmarshal unmarshals a given key path into the given struct using +// the mapstructure lib. If no path is specified, the whole map is unmarshalled. +// `koanf` is the struct field tag used to match field names. To customize, +// use UnmarshalWithConf(). It uses the mitchellh/mapstructure package. +func (ko *Koanf) Unmarshal(path string, o any) error { + return ko.UnmarshalWithConf(path, o, UnmarshalConf{}) +} + +// UnmarshalWithConf is like Unmarshal but takes configuration params in UnmarshalConf. +// See mitchellh/mapstructure's DecoderConfig for advanced customization +// of the unmarshal behaviour. +func (ko *Koanf) UnmarshalWithConf(path string, o any, c UnmarshalConf) error { + if c.DecoderConfig == nil { + c.DecoderConfig = &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + textUnmarshalerHookFunc()), + Metadata: nil, + WeaklyTypedInput: true, + } + } + + c.DecoderConfig.Result = o + + if c.Tag == "" { + c.DecoderConfig.TagName = "koanf" + } else { + c.DecoderConfig.TagName = c.Tag + } + + d, err := mapstructure.NewDecoder(c.DecoderConfig) + if err != nil { + return err + } + + // Unmarshal using flat key paths. + mp := ko.Get(path) + if c.FlatPaths { + if f, ok := mp.(map[string]any); ok { + fmp, _ := maps.Flatten(f, nil, ko.conf.Delim) + mp = fmp + } + } + + return d.Decode(mp) +} + +// Delete removes all nested values from a given path. +// Clears all keys/values if no path is specified. +// Every empty, key on the path, is recursively deleted. +func (ko *Koanf) Delete(path string) { + ko.mu.Lock() + defer ko.mu.Unlock() + + // No path. Erase the entire map. + if path == "" { + ko.confMap = make(map[string]any) + ko.confMapFlat = make(map[string]any) + ko.keyMap = make(KeyMap) + return + } + + // Does the path exist? + p, ok := ko.keyMap[path] + if !ok { + return + } + maps.Delete(ko.confMap, p) + + // Update the flattened version as well. + ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.conf.Delim) + ko.keyMap = populateKeyParts(ko.keyMap, ko.conf.Delim) +} + +// Get returns the raw, uncast any value of a given key path +// in the config map. If the key path does not exist, nil is returned. +func (ko *Koanf) Get(path string) any { + // No path. Return the whole conf map. + if path == "" { + return ko.Raw() + } + + // Does the path exist? + ko.mu.RLock() + defer ko.mu.RUnlock() + + p, ok := ko.keyMap[path] + if !ok { + return nil + } + res := maps.Search(ko.confMap, p) + + // Non-reference types are okay to return directly. + // Other types are "copied" with maps.Copy or json.Marshal + // that change the numeric types to float64. + + switch v := res.(type) { + case int, int8, int16, int32, int64, float32, float64, string, bool: + return v + case map[string]any: + return maps.Copy(v) + case nil: + return nil + } + + // Skip nil pointers before copying. + if rv := reflect.ValueOf(res); rv.Kind() == reflect.Ptr && rv.IsNil() { + return res + } + + out, _ := copystructure.Copy(&res) + if ptrOut, ok := out.(*any); ok { + return *ptrOut + } + return out +} + +// Slices returns a list of Koanf instances constructed from the value +// at path. It accepts []map[string]any or []any containing map[string]any +// elements. Non-slice elements are skipped. +func (ko *Koanf) Slices(path string) []*Koanf { + out := []*Koanf{} + if path == "" { + return out + } + + switch v := ko.Get(path).(type) { + case []map[string]any: + for _, mp := range v { + out = ko.appendMap(mp, out) + } + case []any: + for _, item := range v { + if mp, ok := item.(map[string]any); ok { + out = ko.appendMap(mp, out) + } + } + } + + return out +} + +// Exists returns true if the given key path exists in the conf map. +func (ko *Koanf) Exists(path string) bool { + ko.mu.RLock() + _, ok := ko.keyMap[path] + ko.mu.RUnlock() + return ok +} + +// MapKeys returns a sorted string list of keys in a map addressed by the +// given path. If the path is not a map, an empty string slice is +// returned. +func (ko *Koanf) MapKeys(path string) []string { + var ( + out = []string{} + o = ko.Get(path) + ) + if o == nil { + return out + } + + mp, ok := o.(map[string]any) + if !ok { + return out + } + out = make([]string, 0, len(mp)) + for k := range mp { + out = append(out, k) + } + sort.Strings(out) + return out +} + +// Delim returns delimiter in used by this instance of Koanf. +func (ko *Koanf) Delim() string { + return ko.conf.Delim +} + +func (ko *Koanf) merge(c map[string]any, opts *options) error { + ko.mu.Lock() + + maps.IntfaceKeysToStrings(c) + if opts.merge != nil { + // Deep-copy confMap so the custom merge function can safely call + // ko.Get*() methods (which acquire a read lock) without deadlocking. + dest := maps.Copy(ko.confMap) + + ko.mu.Unlock() + err := opts.merge(c, dest) + ko.mu.Lock() + + if err != nil { + ko.mu.Unlock() + return err + } + ko.confMap = dest + } else if ko.conf.StrictMerge { + if err := maps.MergeStrict(c, ko.confMap); err != nil { + ko.mu.Unlock() + return err + } + } else { + maps.Merge(c, ko.confMap) + } + + // Maintain a flattened version as well. + ko.confMapFlat, ko.keyMap = maps.Flatten(ko.confMap, nil, ko.conf.Delim) + ko.keyMap = populateKeyParts(ko.keyMap, ko.conf.Delim) + + ko.mu.Unlock() + return nil +} + +// toInt64 takes an interface value and if it is an integer type, +// converts and returns int64. If it's any other type, +// forces it to a string and attempts to do a strconv.Atoi +// to get an integer out. +func toInt64(v any) (int64, error) { + switch i := v.(type) { + case int: + return int64(i), nil + case int8: + return int64(i), nil + case int16: + return int64(i), nil + case int32: + return int64(i), nil + case int64: + return i, nil + case uint8: + return int64(i), nil + case uint16: + return int64(i), nil + case uint32: + return int64(i), nil + case uint: + return uintToInt64(uint64(i)) + case uint64: + return uintToInt64(i) + } + + // Force it to a string and try to convert. + s := fmt.Sprintf("%v", v) + + // Try parsing as int64 first, and on failure, attempt float64 parsing. + // Parsing directly as float64, when the number is beyond its upper limit (2^53) + // causes unnecessary precision loss when the value is actually an int. + if i, err := strconv.ParseInt(s, 10, 64); err == nil { + return i, nil + } + + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + + // int64(f) is undefined for numbers that are out of range and returns different + // results on different architectures. float64 cannot exactly represent MaxInt64, + // so check against the ACTUAL upper limit of 2^63. + // If neither match, return an error instead of the undefined result. + if math.IsNaN(f) || f < math.MinInt64 || f >= 1<<63 { + return 0, fmt.Errorf("value %v overflows int64", v) + } + + return int64(f), nil +} + +// uintToInt64 converts an unsigned integer to int64 and throw an error on overflow. +func uintToInt64(v uint64) (int64, error) { + if v > math.MaxInt64 { + return 0, fmt.Errorf("value %d overflows int64", v) + } + + return int64(v), nil +} + +// toInt64 takes a `v any` value and if it is a float type, +// converts and returns a `float64`. If it's any other type, forces it to a +// string and attempts to get a float out using `strconv.ParseFloat`. +func toFloat64(v any) (float64, error) { + switch i := v.(type) { + case float32: + return float64(i), nil + case float64: + return i, nil + } + + // Force it to a string and try to convert. + f, err := strconv.ParseFloat(fmt.Sprintf("%v", v), 64) + if err != nil { + return f, err + } + + return f, nil +} + +// toBool takes an interface value and if it is a bool type, +// returns it. If it's any other type, forces it to a string and attempts +// to parse it as a bool using strconv.ParseBool. +func toBool(v any) (bool, error) { + if b, ok := v.(bool); ok { + return b, nil + } + + // Force it to a string and try to convert. + b, err := strconv.ParseBool(fmt.Sprintf("%v", v)) + if err != nil { + return b, err + } + return b, nil +} + +// populateKeyParts iterates a key map and generates all possible +// traversal paths. For instance, `parent.child.key` generates +// `parent`, and `parent.child`. +func populateKeyParts(m KeyMap, delim string) KeyMap { + out := make(KeyMap, len(m)) // The size of the result is at very least same to KeyMap + for _, parts := range m { + // parts is a slice of [parent, child, key] + var nk string + + for i := range parts { + if i == 0 { + // On first iteration only use first part + nk = parts[i] + } else { + // If nk already contains a part (e.g. `parent`) append delim + `child` + nk += delim + parts[i] + } + if _, ok := out[nk]; ok { + continue + } + out[nk] = make([]string, i+1) + copy(out[nk], parts[0:i+1]) + } + } + return out +} + +// textUnmarshalerHookFunc is a fixed version of mapstructure.TextUnmarshallerHookFunc. +// This hook allows to additionally unmarshal text into custom string types that implement the encoding.Text(Un)Marshaler interface(s). +func textUnmarshalerHookFunc() mapstructure.DecodeHookFuncType { + return func( + f reflect.Type, + t reflect.Type, + data any, + ) (any, error) { + if f.Kind() != reflect.String { + return data, nil + } + result := reflect.New(t).Interface() + unmarshaller, ok := result.(encoding.TextUnmarshaler) + if !ok { + return data, nil + } + + // default text representation is the actual value of the `from` string + var ( + dataVal = reflect.ValueOf(data) + text = []byte(dataVal.String()) + ) + if f.Kind() == t.Kind() { + // source and target are of underlying type string + var ( + err error + ptrVal = reflect.New(dataVal.Type()) + ) + if !ptrVal.Elem().CanSet() { + // cannot set, skip, this should not happen + if err := unmarshaller.UnmarshalText(text); err != nil { + return nil, err + } + return result, nil + } + ptrVal.Elem().Set(dataVal) + + // We need to assert that both, the value type and the pointer type + // do (not) implement the TextMarshaller interface before proceeding and simply + // using the string value of the string type. + // it might be the case that the internal string representation differs from + // the (un)marshalled string. + + for _, v := range []reflect.Value{dataVal, ptrVal} { + if marshaller, ok := v.Interface().(encoding.TextMarshaler); ok { + text, err = marshaller.MarshalText() + if err != nil { + return nil, err + } + break + } + } + } + + // text is either the source string's value or the source string type's marshaled value + // which may differ from its internal string value. + if err := unmarshaller.UnmarshalText(text); err != nil { + return nil, err + } + return result, nil + } +} + +// appendMap creates new Koanf instances from a map returns a slice of Koanf instances. +func (ko *Koanf) appendMap(mp map[string]any, out []*Koanf) []*Koanf { + k := New(ko.conf.Delim) + _ = k.merge(mp, new(options)) + return append(out, k) +} diff --git a/tests/koanf_test.go b/tests/koanf_test.go new file mode 100644 index 0000000..f402572 --- /dev/null +++ b/tests/koanf_test.go @@ -0,0 +1,2310 @@ +package koanf_test + +import ( + encjson "encoding/json" + "errors" + "flag" + "fmt" + "log" + "maps" + "math" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/knadh/koanf/parsers/dotenv" + "github.com/knadh/koanf/parsers/hcl" + "github.com/knadh/koanf/parsers/hjson" + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/basicflag" + "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/env/v2" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/knadh/koanf/v2" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + delim = "." + + mockDir = "../mock" + mockJSON = mockDir + "/mock.json" + mockYAML = mockDir + "/mock.yml" + mockTOML = mockDir + "/mock.toml" + mockHCL = mockDir + "/mock.hcl" + mockProp = mockDir + "/mock.prop" + mockDotEnv = mockDir + "/mock.env" + mockHJSON = mockDir + "/mock.hjson" +) + +// Ordered list of fields in the 'flat' test confs. +var flatTestKeys = []string{ + "COMMENT", + "MORE", + "MiXeD", + "UPPER", + "empty", + "lower", + "quotedSpecial", +} + +var flatTestKeyMap = map[string][]string{ + "COMMENT": {"COMMENT"}, + "MORE": {"MORE"}, + "MiXeD": {"MiXeD"}, + "UPPER": {"UPPER"}, + "empty": {"empty"}, + "lower": {"lower"}, + "quotedSpecial": {"quotedSpecial"}, +} + +var flatTestAll = `COMMENT -> AFTER +MORE -> vars +MiXeD -> CaSe +UPPER -> CASE +empty -> +lower -> case +quotedSpecial -> j18120734xn2&*@#*&R#d1j23d*(*)` + +// Ordered list of fields in the test confs. +var testKeys = []string{ + "bools", + "duration", + "empty", + "intbools", + "negative_int", + "orphan", + "parent1.boolmap.notok3", + "parent1.boolmap.ok1", + "parent1.boolmap.ok2", + "parent1.child1.empty", + "parent1.child1.grandchild1.ids", + "parent1.child1.grandchild1.on", + "parent1.child1.name", + "parent1.child1.type", + "parent1.floatmap.key1", + "parent1.floatmap.key2", + "parent1.floatmap.key3", + "parent1.id", + "parent1.intmap.key1", + "parent1.intmap.key2", + "parent1.intmap.key3", + "parent1.name", + "parent1.strmap.key1", + "parent1.strmap.key2", + "parent1.strmap.key3", + "parent1.strsmap.key1", + "parent1.strsmap.key2", + "parent2.child2.empty", + "parent2.child2.grandchild2.ids", + "parent2.child2.grandchild2.on", + "parent2.child2.name", + "parent2.id", + "parent2.name", + "strbool", + "strbools", + "time", + "type", +} + +var testKeyMap = map[string][]string{ + "bools": {"bools"}, + "duration": {"duration"}, + "empty": {"empty"}, + "intbools": {"intbools"}, + "negative_int": {"negative_int"}, + "orphan": {"orphan"}, + "parent1": {"parent1"}, + "parent1.boolmap": {"parent1", "boolmap"}, + "parent1.boolmap.notok3": {"parent1", "boolmap", "notok3"}, + "parent1.boolmap.ok1": {"parent1", "boolmap", "ok1"}, + "parent1.boolmap.ok2": {"parent1", "boolmap", "ok2"}, + "parent1.child1": {"parent1", "child1"}, + "parent1.child1.empty": {"parent1", "child1", "empty"}, + "parent1.child1.grandchild1": {"parent1", "child1", "grandchild1"}, + "parent1.child1.grandchild1.ids": {"parent1", "child1", "grandchild1", "ids"}, + "parent1.child1.grandchild1.on": {"parent1", "child1", "grandchild1", "on"}, + "parent1.child1.name": {"parent1", "child1", "name"}, + "parent1.child1.type": {"parent1", "child1", "type"}, + "parent1.floatmap": {"parent1", "floatmap"}, + "parent1.floatmap.key1": {"parent1", "floatmap", "key1"}, + "parent1.floatmap.key2": {"parent1", "floatmap", "key2"}, + "parent1.floatmap.key3": {"parent1", "floatmap", "key3"}, + "parent1.id": {"parent1", "id"}, + "parent1.intmap": {"parent1", "intmap"}, + "parent1.intmap.key1": {"parent1", "intmap", "key1"}, + "parent1.intmap.key2": {"parent1", "intmap", "key2"}, + "parent1.intmap.key3": {"parent1", "intmap", "key3"}, + "parent1.name": {"parent1", "name"}, + "parent1.strmap": {"parent1", "strmap"}, + "parent1.strmap.key1": {"parent1", "strmap", "key1"}, + "parent1.strmap.key2": {"parent1", "strmap", "key2"}, + "parent1.strmap.key3": {"parent1", "strmap", "key3"}, + "parent1.strsmap": {"parent1", "strsmap"}, + "parent1.strsmap.key1": {"parent1", "strsmap", "key1"}, + "parent1.strsmap.key2": {"parent1", "strsmap", "key2"}, + "parent2": {"parent2"}, + "parent2.child2": {"parent2", "child2"}, + "parent2.child2.empty": {"parent2", "child2", "empty"}, + "parent2.child2.grandchild2": {"parent2", "child2", "grandchild2"}, + "parent2.child2.grandchild2.ids": {"parent2", "child2", "grandchild2", "ids"}, + "parent2.child2.grandchild2.on": {"parent2", "child2", "grandchild2", "on"}, + "parent2.child2.name": {"parent2", "child2", "name"}, + "parent2.id": {"parent2", "id"}, + "parent2.name": {"parent2", "name"}, + "strbool": {"strbool"}, + "strbools": {"strbools"}, + "time": {"time"}, + "type": {"type"}, +} + +// `parent1.child1.type` and `type` are excluded as their +// values vary between files. +var testAll = `bools -> [true false true] +duration -> 3s +empty -> map[] +intbools -> [1 0 1] +negative_int -> -1234 +orphan -> [red blue orange] +parent1.boolmap.notok3 -> false +parent1.boolmap.ok1 -> true +parent1.boolmap.ok2 -> true +parent1.child1.empty -> map[] +parent1.child1.grandchild1.ids -> [1 2 3] +parent1.child1.grandchild1.on -> true +parent1.child1.name -> child1 +parent1.floatmap.key1 -> 1.1 +parent1.floatmap.key2 -> 1.2 +parent1.floatmap.key3 -> 1.3 +parent1.id -> 1234 +parent1.intmap.key1 -> 1 +parent1.intmap.key2 -> 1 +parent1.intmap.key3 -> 1 +parent1.name -> parent1 +parent1.strmap.key1 -> val1 +parent1.strmap.key2 -> val2 +parent1.strmap.key3 -> val3 +parent1.strsmap.key1 -> [val1 val2 val3] +parent1.strsmap.key2 -> [val4 val5] +parent2.child2.empty -> map[] +parent2.child2.grandchild2.ids -> [4 5 6] +parent2.child2.grandchild2.on -> true +parent2.child2.name -> child2 +parent2.id -> 5678 +parent2.name -> parent2 +strbool -> 1 +strbools -> [1 t f] +time -> 2019-01-01` + +var testParent2 = `child2.empty -> map[] +child2.grandchild2.ids -> [4 5 6] +child2.grandchild2.on -> true +child2.name -> child2 +id -> 5678 +name -> parent2` + +type parentStruct struct { + Name string `koanf:"name"` + ID int `koanf:"id"` + Child1 childStruct `koanf:"child1"` +} +type childStruct struct { + Name string `koanf:"name"` + Type string `koanf:"type"` + Empty map[string]string `koanf:"empty"` + Grandchild1 grandchildStruct `koanf:"grandchild1"` +} +type grandchildStruct struct { + Ids []int `koanf:"ids"` + On bool `koanf:"on"` +} +type testStruct struct { + Type string `koanf:"type"` + Empty map[string]string `koanf:"empty"` + Parent1 parentStruct `koanf:"parent1"` +} + +type testStructFlat struct { + Type string `koanf:"type"` + Empty map[string]string `koanf:"empty"` + Parent1Name string `koanf:"parent1.name"` + Parent1ID int `koanf:"parent1.id"` + Parent1Child1Name string `koanf:"parent1.child1.name"` + Parent1Child1Type string `koanf:"parent1.child1.type"` + Parent1Child1Empty map[string]string `koanf:"parent1.child1.empty"` + Parent1Child1Grandchild1IDs []int `koanf:"parent1.child1.grandchild1.ids"` + Parent1Child1Grandchild1On bool `koanf:"parent1.child1.grandchild1.on"` +} + +type customText int + +func (c *customText) UnmarshalText(text []byte) error { + s := strings.ToLower(string(text)) + + switch { + case strings.HasSuffix(s, "mb"): + s = strings.TrimSuffix(s, "mb") + default: + s = "0" + } + + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + *c = customText(v) + return nil +} + +type Case struct { + koanf *koanf.Koanf + file string + parser koanf.Parser + typeName string +} + +// 'Flat' Case instances to be used in multiple tests. These will not be +// mutated. +var flatCases = []Case{ + {koanf: koanf.New(delim), file: mockDotEnv, parser: dotenv.Parser(), typeName: "dotenv"}, +} + +// Case instances to be used in multiple tests. These will not be mutated. +var cases = []Case{ + {koanf: koanf.New(delim), file: mockJSON, parser: json.Parser(), typeName: "json"}, + {koanf: koanf.New(delim), file: mockYAML, parser: yaml.Parser(), typeName: "yml"}, + {koanf: koanf.New(delim), file: mockTOML, parser: toml.Parser(), typeName: "toml"}, + {koanf: koanf.New(delim), file: mockHCL, parser: hcl.Parser(true), typeName: "hcl"}, + {koanf: koanf.New(delim), file: mockHJSON, parser: hjson.Parser(), typeName: "hjson"}, +} + +func init() { + // Preload 4 Koanf instances with their providers and config. + if err := cases[0].koanf.Load(file.Provider(cases[0].file), json.Parser()); err != nil { + log.Fatalf("error loading config file: %v", err) + } + if err := cases[1].koanf.Load(file.Provider(cases[1].file), yaml.Parser()); err != nil { + log.Fatalf("error loading config file: %v", err) + } + if err := cases[2].koanf.Load(file.Provider(cases[2].file), toml.Parser()); err != nil { + log.Fatalf("error loading config file: %v", err) + } + if err := cases[3].koanf.Load(file.Provider(cases[3].file), hcl.Parser(true)); err != nil { + log.Fatalf("error loading config file: %v", err) + } + if err := cases[4].koanf.Load(file.Provider(cases[4].file), hjson.Parser()); err != nil { + log.Fatalf("error loading config file: %v", err) + } + + // Preload 1 'flat' Koanf instances with their providers and config. + if err := flatCases[0].koanf.Load(file.Provider(flatCases[0].file), dotenv.Parser()); err != nil { + log.Fatalf("error loading config file: %v", err) + } +} + +func BenchmarkLoadFile(b *testing.B) { + k := koanf.New(delim) + + // Don't use TOML here because it distorts memory benchmarks due to high memory use + providers := []*file.File{file.Provider(mockJSON), file.Provider(mockYAML)} + parsers := []koanf.Parser{json.Parser(), yaml.Parser()} + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if err := k.Load(providers[n%2], parsers[n%2]); err != nil { + b.Fatalf("Unexpected error: %+v", k) + } + } +} + +func TestLoadFile(t *testing.T) { + // Load a non-existent file. + _, err := file.Provider("does-not-exist").ReadBytes() + assert.NotNil(t, err, "no error for non-existent file") + + // Load a valid file. + _, err = file.Provider(mockJSON).ReadBytes() + assert.Nil(t, err, "error loading file") +} + +func TestLoadFlatFileAllKeys(t *testing.T) { + assert := assert.New(t) + re, _ := regexp.Compile("(.+?)?type \\-> (.*)\n") + for _, c := range flatCases { + // Check against testKeys. + assert.Equal(flatTestKeys, c.koanf.Keys(), fmt.Sprintf("loaded keys mismatch: %v", c.typeName)) + + // Check against keyMap. + assert.EqualValues(flatTestKeyMap, c.koanf.KeyMap(), "keymap doesn't match") + + // Replace the "type" fields that varies across different files + // to do a complete key -> value map match with testAll. + s := strings.TrimSpace(re.ReplaceAllString(c.koanf.Sprint(), "")) + assert.Equal(flatTestAll, s, fmt.Sprintf("key -> value list mismatch: %v", c.typeName)) + } +} + +func TestLoadFileAllKeys(t *testing.T) { + assert := assert.New(t) + re, _ := regexp.Compile("(.+?)?type \\-> (.*)\n") + for _, c := range cases { + // Check against testKeys. + assert.Equal(testKeys, c.koanf.Keys(), fmt.Sprintf("loaded keys mismatch: %v", c.typeName)) + + // Check against keyMap. + assert.EqualValues(testKeyMap, c.koanf.KeyMap(), "keymap doesn't match") + + // Replace the "type" fields that varies across different files + // to do a complete key -> value map match with testAll. + s := strings.TrimSpace(re.ReplaceAllString(c.koanf.Sprint(), "")) + assert.Equal(testAll, s, fmt.Sprintf("key -> value list mismatch: %v", c.typeName)) + } +} + +func TestDelim(t *testing.T) { + k1 := koanf.New(".") + assert.Equal(t, k1.Delim(), ".") + + k2 := koanf.New("/") + assert.Equal(t, k2.Delim(), "/") +} + +func TestLoadMergeYamlJson(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + assert.NoError(k.Load(file.Provider(mockYAML), yaml.Parser()), + "error loading file") + // loading json after yaml causes the intbools to be loaded as []float64 + assert.NoError(k.Load(file.Provider(mockJSON), yaml.Parser()), + "error loading file") + + // checking that there is no issues with expecting it to be an []int64 + v := k.Int64s("intbools") + assert.Len(v, 3) + + defer func() { + if err := recover(); err != nil { + assert.Failf("panic", "received panic: %v", err) + } + }() + + v2 := k.MustInt64s("intbools") + assert.Len(v2, 3) +} + +func TestLoadMergeJsonYaml(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + assert.NoError(k.Load(file.Provider(mockJSON), yaml.Parser()), + "error loading file") + // loading yaml after json causes the intbools to be loaded as []int after json loaded it with []float64 + assert.NoError(k.Load(file.Provider(mockYAML), yaml.Parser()), + "error loading file") + + // checking that there is no issues with expecting it to be an []float64 + v := k.Float64s("intbools") + assert.Len(v, 3) + + defer func() { + if err := recover(); err != nil { + assert.Failf("panic", "received panic: %v", err) + } + }() + + v2 := k.MustFloat64s("intbools") + assert.Len(v2, 3) +} + +func TestWatchFile(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + // Create a tmp config file. + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "koanf_mock") + err := os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name1"}}`), 0600) + require.NoError(t, err, "error creating temp config file: %v", err) + + // Load the new config and watch it for changes. + f := file.Provider(tmpFile) + require.NoError(t, k.Load(f, json.Parser())) + + // Watch for changes. + changedC := make(chan error, 1) + require.NoError(t, f.Watch(func(event any, err error) { + if err == nil { + // Reload the config. + err = k.Load(f, json.Parser()) + } + + select { + case changedC <- err: + default: + } + })) + defer f.Unwatch() + + // Replace the file atomically so fsnotify cannot report a write while the + // watched file is truncated but not fully rewritten yet. + tmpWriteFile := filepath.Join(tmpDir, "koanf_mock.next") + require.NoError(t, os.WriteFile(tmpWriteFile, []byte(`{"parent": {"name": "name2"}}`), 0600)) + require.NoError(t, os.Rename(tmpWriteFile, tmpFile)) + + select { + case err := <-changedC: + require.NoError(t, err) + assert.Equal("name2", k.String("parent.name"), "file watch reload didn't change config") + case <-time.After(10 * time.Second): + assert.Fail("timeout waiting for file watch trigger") + } +} + +func TestWatchFileSymlink(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + tmpDir, _ := os.MkdirTemp("", "koanf_*") // TODO: replace with t.TempDir() as of go v1.15 + + // Create a symlink. + symPath := filepath.Join(tmpDir, "koanf_test_symlink") + symPath2 := filepath.Join(tmpDir, "koanf_test_symlink2") + + wd, err := os.Getwd() + assert.NoError(err, "error getting working dir") + + jsonFile := filepath.Join(wd, mockJSON) + yamlFile := filepath.Join(wd, mockYAML) + + // Create a symlink to the JSON file which will be swapped out later. + assert.NoError(os.Symlink(jsonFile, symPath), "error creating symlink") + assert.NoError(os.Symlink(yamlFile, symPath2), "error creating symlink2") + + // Load the symlink (to the JSON) file. + f := file.Provider(symPath) + k.Load(f, json.Parser()) + + // Watch for changes. + changedC := make(chan string, 1) + var wg sync.WaitGroup + wg.Add(1) // our assurance that cb is called max once + f.Watch(func(event any, err error) { + // The File watcher always returns a nil `event`, which can + // be ignored. + if err != nil { + // TODO: make use of Error Wrapping-Scheme and assert.ErrorIs() checks as of go v1.13 + assert.Condition(func() bool { + return strings.Contains(err.Error(), "no such file or directory") + }, "received unexpected error. err: %s", err) + return + } + // Reload the config. + k.Load(f, yaml.Parser()) + changedC <- k.String("type") + wg.Done() + }) + + // Wait a second and swap the symlink target from the JSON file to the YAML file. + // Create a temp symlink to the YAML file and rename the old symlink to the new + // symlink. We do this to avoid removing the symlink and triggering a REMOVE event. + time.Sleep(1 * time.Second) + assert.NoError(os.Rename(symPath2, symPath), "error swapping symlink to another file type") + if waitTimeout(&wg, time.Second*10) { + assert.Fail("timeout waiting for file watch trigger") + } else { + assert.Condition(func() bool { + return strings.Compare(<-changedC, "yml") == 0 + }, "symlink watch reload didn't change config") + } +} + +func TestWatchFileDirectorySymlink(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + tmpDir := t.TempDir() + + baseDir := filepath.Join(tmpDir, "base_dir") + baseDir2 := filepath.Join(tmpDir, "base_dir2") + + err := os.Mkdir(baseDir, 0700) + assert.NoError(err, "error creating base dir") + + err = os.Mkdir(baseDir2, 0700) + assert.NoError(err, "error creating base dir 2") + + wd, err := os.Getwd() + assert.NoError(err, "error getting working dir") + + jsonFile := filepath.Join(wd, mockJSON) + yamlFile := filepath.Join(wd, mockYAML) + + jsonData, err := os.ReadFile(jsonFile) + assert.NoError(err, "error reading JSON file") + + err = os.WriteFile(filepath.Join(baseDir, "config"), jsonData, 0600) + assert.NoError(err, "error writing JSON file to base dir") + + yamlData, err := os.ReadFile(yamlFile) + assert.NoError(err, "error reading YAML file") + + err = os.WriteFile(filepath.Join(baseDir2, "config"), yamlData, 0600) + assert.NoError(err, "error writing YAML file to base dir 2") + + // Create a symlink. + symDir := filepath.Join(tmpDir, "koanf_test_symlink") + symDir2 := filepath.Join(tmpDir, "koanf_test_symlink2") + symPath := filepath.Join(tmpDir, "config") + + // Create a symlink to the JSON file which will be swapped out later. + assert.NoError(os.Symlink(baseDir, symDir), "error creating symlink dir") + assert.NoError(os.Symlink(baseDir2, symDir2), "error creating symlink dir2") + assert.NoError(os.Symlink(filepath.Join(symDir, "config"), symPath), "error creating symlink") + + // Load the symlink (to the JSON) file. + f := file.Provider(symPath) + k.Load(f, json.Parser()) + + // Watch for changes. + changedC := make(chan string, 1) + var wg sync.WaitGroup + wg.Add(1) // our assurance that cb is called max once + f.Watch(func(event any, err error) { + // The File watcher always returns a nil `event`, which can + // be ignored. + if err != nil { + // TODO: make use of Error Wrapping-Scheme and assert.ErrorIs() checks as of go v1.13 + assert.Condition(func() bool { + return strings.Contains(err.Error(), "no such file or directory") + }, "received unexpected error. err: %s", err) + return + } + // Reload the config. + k.Load(f, yaml.Parser()) + changedC <- k.String("type") + wg.Done() + }) + + // Wait a second and swap the symlink target from the JSON file to the YAML file. + // Create a temp symlink to the YAML file and rename the old symlink to the new + // symlink. We do this to avoid removing the symlink and triggering a REMOVE event. + time.Sleep(1 * time.Second) + assert.NoError(os.Rename(symDir2, symDir), "error swapping symlink dir to another symlink dir") + + if waitTimeout(&wg, time.Second*10) { + assert.Fail("timeout waiting for file watch trigger") + } else { + assert.Condition(func() bool { + return strings.Compare(<-changedC, "yml") == 0 + }, "symlink watch reload didn't change config") + } +} + +func TestUnwatchFile(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + // Create a tmp config file. + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "koanf_mock") + require.NoError(t, os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name1"}}`), 0600)) + + // Load the new config file. + f := file.Provider(tmpFile) + k.Load(f, json.Parser()) + + // Watch. + var reloaded int32 + f.Watch(func(event any, err error) { + atomic.StoreInt32(&reloaded, 1) + assert.NoError(err) + }) + + // Change the file and check whether the watch triggered. + time.Sleep(100 * time.Millisecond) + os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name2"}}`), 0600) + time.Sleep(100 * time.Millisecond) + assert.True(atomic.LoadInt32(&reloaded) == 1, "watched file didn't reload") + + // Unwatch the file and verify that the watch didn't trigger. + assert.NoError(f.Unwatch()) + atomic.StoreInt32(&reloaded, 0) + time.Sleep(100 * time.Millisecond) + os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name3"}}`), 0600) + time.Sleep(100 * time.Millisecond) + assert.False(atomic.LoadInt32(&reloaded) == 1, "unwatched file reloaded") + + // Re-watch and check again. + atomic.StoreInt32(&reloaded, 0) + f.Watch(func(event any, err error) { + atomic.StoreInt32(&reloaded, 1) + assert.NoError(err) + }) + os.WriteFile(tmpFile, []byte(`{"parent": {"name": "name4"}}`), 0600) + time.Sleep(100 * time.Millisecond) + assert.True(atomic.LoadInt32(&reloaded) == 1, "watched file didn't reload") + + f.Unwatch() +} + +func TestLoadMerge(t *testing.T) { + var ( + assert = assert.New(t) + // Load several types into a fresh Koanf instance. + k = koanf.New(delim) + ) + for _, c := range cases { + assert.Nil(k.Load(file.Provider(c.file), c.parser), + fmt.Sprintf("error loading: %v", c.file)) + + // Check against testKeys. + assert.Equal(testKeys, k.Keys(), fmt.Sprintf("loaded keys don't match in: %v", c.file)) + + // The 'type' fields in different file types have different values. + // As each subsequent file is loaded, the previous value should be overridden. + assert.Equal(c.typeName, k.String("type"), "types don't match") + assert.Equal(c.typeName, k.String("parent1.child1.type"), "types don't match") + } + + // Load env provider and override value. + t.Setenv("PREFIX_PARENT1.CHILD1.TYPE", "env") + + err := k.Load(env.Provider(".", env.Opt{ + Prefix: "PREFIX_", + TransformFunc: func(k, v string) (string, any) { + return strings.ReplaceAll(strings.ToLower(k), "prefix_", ""), v + }, + }), nil) + + assert.Nil(err, "error loading env") + assert.Equal("env", k.String("parent1.child1.type"), "types don't match") + + // Test the env provider than can mutate the value to upper case + err = k.Load(env.Provider(".", env.Opt{ + Prefix: "PREFIX_", + TransformFunc: func(k string, v string) (string, any) { + return strings.ReplaceAll(strings.ToLower(k), "prefix_", ""), strings.ToUpper(v) + }, + }), nil) + + assert.Nil(err, "error loading env with value") + assert.Equal("ENV", k.String("parent1.child1.type"), "types don't match") + + // Override with the confmap provider. + k.Load(confmap.Provider(map[string]any{ + "parent1.child1.type": "confmap", + "type": "confmap", + }, "."), nil) + assert.Equal("confmap", k.String("parent1.child1.type"), "types don't match") + assert.Equal("confmap", k.String("type"), "types don't match") + + // Override with the rawbytes provider. + assert.Nil(k.Load(rawbytes.Provider([]byte(`{"type": "rawbytes", "parent1": {"child1": {"type": "rawbytes"}}}`)), json.Parser()), + "error loading raw bytes") + assert.Equal("rawbytes", k.String("parent1.child1.type"), "types don't match") + assert.Equal("rawbytes", k.String("type"), "types don't match") +} + +func TestFlags(t *testing.T) { + var ( + assert = assert.New(t) + def = koanf.New(delim) + ) + assert.Nil(def.Load(file.Provider(mockJSON), json.Parser()), "error loading file") + + // Override with the posflag provider. + f := pflag.NewFlagSet("test", pflag.ContinueOnError) + + // Key that exists in the loaded conf. Should overwrite with Set(). + f.String("parent1.child1.type", "flag", "") + f.Set("parent1.child1.type", "flag") + f.StringSlice("stringslice", []string{"a", "b", "c"}, "") + f.IntSlice("intslice", []int{1, 2, 3}, "") + + // Key that doesn't exist in the loaded file conf. Should merge the default value. + f.String("flagkey", "flag", "") + + // Key that exists in the loaded conf but no Set(). Default value shouldn't be merged. + f.String("parent1.name", "flag", "") + + // Initialize the provider with the Koanf instance passed where default values + // will merge if the keys are not present in the conf map. + { + k := def.Copy() + assert.Nil(k.Load(posflag.Provider(f, ".", k), nil), "error loading posflag") + assert.Equal("flag", k.String("parent1.child1.type"), "types don't match") + assert.Equal("flag", k.String("flagkey"), "value doesn't match") + assert.NotEqual("flag", k.String("parent1.name"), "value doesn't match") + assert.Equal([]string{"a", "b", "c"}, k.Strings("stringslice"), "value doesn't match") + assert.Equal([]int{1, 2, 3}, k.Ints("intslice"), "value doesn't match") + } + + // Test the posflag provider can mutate the value to upper case + { + k := def.Copy() + assert.Nil(k.Load(posflag.ProviderWithValue(f, ".", nil, func(k string, v string) (string, any) { + return strings.Replace(strings.ToLower(k), "prefix_", "", -1), strings.ToUpper(v) + }), nil), "error loading posflag") + assert.Equal("FLAG", k.String("parent1.child1.type"), "types don't match") + } + + // Test without passing the Koanf instance where default values will not merge. + { + k := def.Copy() + assert.Nil(k.Load(posflag.Provider(f, ".", nil), nil), "error loading posflag") + assert.Equal("flag", k.String("parent1.child1.type"), "types don't match") + assert.Equal("", k.String("flagkey"), "value doesn't match") + assert.NotEqual("", k.String("parent1.name"), "value doesn't match") + } + + // Override with the basicflag provider. + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.type", "flag", "") + bf.String("parent2.child2.name", "override-default", "") + bf.Set("parent1.child1.type", "basicflag") + assert.Nil(k.Load(basicflag.Provider(bf, "."), nil), "error loading basicflag") + assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match") + assert.Equal("override-default", k.String("parent2.child2.name"), "basicflag default value override failed") + } + + // No default-value override behaviour. + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.name", "override-default", "") + bf.String("parent2.child2.name", "override-default", "") + bf.Set("parent2.child2.name", "custom") + assert.Nil(k.Load(basicflag.Provider(bf, ".", &basicflag.Opt{KeyMap: def}), nil), "error loading basicflag") + assert.Equal("child1", k.String("parent1.child1.name"), "basicflag default overwrote") + assert.Equal("custom", k.String("parent2.child2.name"), "basicflag set failed") + } + + // Override with the basicflag provider. + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.type", "flag", "") + bf.String("parent2.child2.name", "override-default", "") + bf.Set("parent1.child1.type", "basicflag") + assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", nil), nil), "error loading basicflag") + assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match") + assert.Equal("override-default", k.String("parent2.child2.name"), "basicflag default value override failed") + } + + // No default-value override behaviour. + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.name", "override-default", "") + bf.String("parent2.child2.name", "override-default", "") + bf.Set("parent2.child2.name", "custom") + assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", nil, def), nil), "error loading basicflag") + assert.Equal("child1", k.String("parent1.child1.name"), "basicflag default overwrote") + assert.Equal("custom", k.String("parent2.child2.name"), "basicflag set failed") + } + + // Override with the basicflag provider. + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.type", "flag", "") + bf.Set("parent1.child1.type", "basicflag") + assert.Nil(k.Load(basicflag.Provider(bf, "."), nil), "error loading basicflag") + assert.Equal("basicflag", k.String("parent1.child1.type"), "types don't match") + } + + // Test the basicflag provider can mutate the value to upper case + { + k := def.Copy() + bf := flag.NewFlagSet("test", flag.ContinueOnError) + bf.String("parent1.child1.type", "flag", "") + bf.Set("parent1.child1.type", "basicflag") + assert.Nil(k.Load(basicflag.ProviderWithValue(bf, ".", func(k string, v string) (string, any) { + return strings.Replace(strings.ToLower(k), "prefix_", "", -1), strings.ToUpper(v) + }), nil), "error loading basicflag") + assert.Equal("BASICFLAG", k.String("parent1.child1.type"), "types don't match") + } +} + +func TestConfMapValues(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), "error loading file") + var ( + c1 = k.All() + ra1 = k.Raw() + r1 = k.Cut("parent2").Raw() + ) + + k = koanf.New(delim) + assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), "error loading file") + var ( + c2 = k.All() + ra2 = k.Raw() + r2 = k.Cut("parent2").Raw() + ) + + assert.EqualValues(c1, c2, "conf map mismatch") + assert.EqualValues(ra1, ra2, "conf map mismatch") + assert.EqualValues(r1, r2, "conf map mismatch") +} + +func TestCutCopy(t *testing.T) { + // Instance 1. + var ( + assert = assert.New(t) + k1 = koanf.New(delim) + ) + assert.Nil(k1.Load(file.Provider(mockJSON), json.Parser()), + "error loading file") + var ( + cp1 = k1.Copy() + cut1 = k1.Cut("") + cutp1 = k1.Cut("parent2") + ) + + // Instance 2. + k2 := koanf.New(delim) + assert.Nil(k2.Load(file.Provider(mockJSON), json.Parser()), + "error loading file") + var ( + cp2 = k2.Copy() + cut2 = k2.Cut("") + cutp2 = k2.Cut("parent2") + ) + + assert.EqualValues(cp1.All(), cp2.All(), "conf map mismatch") + assert.EqualValues(cut1.All(), cut2.All(), "conf map mismatch") + assert.EqualValues(cutp1.All(), cutp2.All(), "conf map mismatch") + assert.Equal(testParent2, strings.TrimSpace(cutp1.Sprint()), "conf map mismatch") + assert.Equal(strings.TrimSpace(cutp1.Sprint()), strings.TrimSpace(cutp2.Sprint()), "conf map mismatch") + + // Cut a single field with no children. Should return empty conf maps. + assert.Equal(k1.Cut("type").Keys(), k2.Cut("type").Keys(), "single field cut mismatch") + assert.Equal(k1.Cut("xxxx").Keys(), k2.Cut("xxxx").Keys(), "single field cut mismatch") + assert.Equal(len(k1.Cut("type").Raw()), 0, "non-map cut returned items") +} + +func TestWithMergeFunc(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + assert.NoError(k.Load(rawbytes.Provider([]byte(`{"foo":"bar"}`)), json.Parser())) + assert.NoError(k.Load(rawbytes.Provider([]byte(`{"baz":"bar"}`)), json.Parser(), koanf.WithMergeFunc(func(a, b map[string]any) error { + // No merge + return nil + }))) + + assert.Equal(map[string]any{ + "foo": "bar", + }, k.All(), "expects the result of the first load only") + + err := errors.New("stub") + assert.ErrorIs(k.Load(rawbytes.Provider([]byte(`{"baz":"bar"}`)), json.Parser(), koanf.WithMergeFunc(func(a, b map[string]any) error { + return err + })), err, "expects the error thrown by WithMergeFunc") +} + +func TestWithMergeFuncGetNoDeadlock(t *testing.T) { + k := koanf.New(delim) + + require.NoError(t, k.Load(rawbytes.Provider([]byte(`{"key":"old"}`)), json.Parser())) + + done := make(chan struct{}) + go func() { + defer close(done) + err := k.Load(rawbytes.Provider([]byte(`{"key":"new"}`)), json.Parser(), koanf.WithMergeFunc(func(src, dest map[string]any) error { + // Calling a typed getter inside the merge func must not deadlock. + _ = k.String("key") + maps.Copy(dest, src) + return nil + })) + require.NoError(t, err) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("deadlock: merge func calling GetString blocked for 5s") + } + + assert.New(t).Equal("new", k.String("key")) +} + +func TestMerge(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), + "error loading file") + + // Make two different cuts that'll have different confmaps. + var ( + cut1 = k.Cut("parent1") + cut2 = k.Cut("parent2") + ) + assert.NotEqual(cut1.All(), cut2.All(), "different cuts incorrectly match") + assert.NotEqual(cut1.Sprint(), cut2.Sprint(), "different cuts incorrectly match") + + // Create an empty Koanf instance. + k2 := koanf.New(delim) + + // Merge cut1 into it and check if they match. + k2.Merge(cut1) + assert.Equal(cut1.All(), k2.All(), "conf map mismatch") +} + +func TestRaw_YamlTypes(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()), + "error loading file") + raw := k.Raw() + + i, ok := raw["intbools"] + assert.True(ok, "ints key does not exist in the map") + + arr, ok := i.([]any) + assert.True(ok, "arr slice is not array of integers") + + for _, integer := range arr { + if _, ok := integer.(int); !ok { + assert.Failf("failure", "%v not an integer but %T", integer, integer) + } + } +} + +func TestRaw_JsonTypes(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + + assert.Nil(k.Load(file.Provider(mockJSON), json.Parser()), + "error loading file") + raw := k.Raw() + + i, ok := raw["intbools"] + assert.True(ok, "ints key does not exist in the map") + + arr, ok := i.([]any) + assert.True(ok, "arr slice is not array of integers") + + for _, integer := range arr { + if _, ok := integer.(float64); !ok { + assert.Failf("failure", "%v not an integer but %T", integer, integer) + } + } +} + +func TestMergeStrictError(t *testing.T) { + assert := assert.New(t) + + ks := koanf.NewWithConf(koanf.Conf{ + Delim: delim, + StrictMerge: true, + }) + + assert.Nil(ks.Load(confmap.Provider(map[string]any{ + "parent2": map[string]any{ + "child2": map[string]any{ + "grandchild2": map[string]any{ + "ids": 123, + }, + }, + }, + }, delim), nil)) + + err := ks.Load(file.Provider(mockYAML), yaml.Parser()) + assert.Error(err) + assert.True(strings.HasPrefix(err.Error(), "incorrect types at key parent2.child2.grandchild2")) +} + +func TestMergeAt(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()), + "error loading file") + + // Get expected koanf, and root data + var ( + expected = k.Cut("parent2") + rootData = map[string]any{ + "name": k.String("parent2.name"), + "id": k.Int("parent2.id"), + } + ) + + // Get nested test data to merge at path + child2 := koanf.New(delim) + assert.Nil(child2.Load(confmap.Provider(map[string]any{ + "name": k.String("parent2.child2.name"), + "empty": k.Get("parent2.child2.empty"), + }, delim), nil)) + grandChild := k.Cut("parent2.child2.grandchild2") + + // Create test koanf + ordered := koanf.New(delim) + assert.Nil(ordered.Load(confmap.Provider(rootData, delim), nil)) + + // Merge at path in order, first child2, then child2.grandchild2 + ordered.MergeAt(child2, "child2") + ordered.MergeAt(grandChild, "child2.grandchild2") + assert.Equal(expected.Get(""), ordered.Get(""), "conf map mismatch") + + // Create test koanf + reversed := koanf.New(delim) + assert.Nil(reversed.Load(confmap.Provider(rootData, delim), nil)) + + // Merge at path in reverse order, first child2.grandchild2, then child2 + reversed.MergeAt(grandChild, "child2.grandchild2") + reversed.MergeAt(child2, "child2") + assert.Equal(expected.Get(""), reversed.Get(""), "conf map mismatch") +} + +func TestSet(t *testing.T) { + var ( + assert = assert.New(t) + k = koanf.New(delim) + ) + assert.Nil(k.Load(file.Provider(mockYAML), yaml.Parser()), + "error loading file") + + assert.Nil(k.Set("parent1.name", "new")) + assert.Equal(k.String("parent1.name"), "new") + + assert.Nil(k.Set("parent1.child1.name", 123)) + assert.Equal(k.Int("parent1.child1.name"), 123) + + assert.Nil(k.Set("parent1.child1.grandchild1.ids", []int{5})) + assert.Equal(k.Ints("parent1.child1.grandchild1.ids"), []int{5}) + + assert.Nil(k.Set("parent1.child1.grandchild1", 123)) + assert.Equal(k.Int("parent1.child1.grandchild1"), 123) + assert.Equal(k.Int("parent1.child1.grandchild1.ids"), 0) + + assert.Nil(k.Set("parent1.child1.grandchild1", map[string]any{"name": "new"})) + assert.Equal(k.Get("parent1.child1.grandchild1"), map[string]any{"name": "new"}) +} + +func TestUnmarshal(t *testing.T) { + assert := assert.New(t) + + // Expected unmarshalled structure. + real := testStruct{ + Type: "json", + Empty: make(map[string]string), + Parent1: parentStruct{ + Name: "parent1", + ID: 1234, + Child1: childStruct{ + Name: "child1", + Type: "json", + Empty: make(map[string]string), + Grandchild1: grandchildStruct{ + Ids: []int{1, 2, 3}, + On: true, + }, + }, + }, + } + + // Unmarshal and check all parsers. + for _, c := range cases { + var ( + k = koanf.New(delim) + ts testStruct + ) + assert.Nil(k.Load(file.Provider(c.file), c.parser), + fmt.Sprintf("error loading: %v", c.file)) + assert.Nil(k.Unmarshal("", &ts), "unmarshal failed") + real.Type = c.typeName + real.Parent1.Child1.Type = c.typeName + assert.Equal(real, ts, "unmarshalled structs don't match") + + // Unmarshal with config. + ts = testStruct{} + assert.Nil(k.UnmarshalWithConf("", &ts, koanf.UnmarshalConf{Tag: "koanf"}), "unmarshal failed") + real.Type = c.typeName + real.Parent1.Child1.Type = c.typeName + assert.Equal(real, ts, "unmarshalled structs don't match") + } +} + +func TestUnmarshalFlat(t *testing.T) { + assert := assert.New(t) + + // Expected unmarshalled structure. + real := testStructFlat{ + Type: "json", + Empty: make(map[string]string), + Parent1Name: "parent1", + Parent1ID: 1234, + Parent1Child1Name: "child1", + Parent1Child1Type: "json", + Parent1Child1Empty: make(map[string]string), + Parent1Child1Grandchild1IDs: []int{1, 2, 3}, + Parent1Child1Grandchild1On: true, + } + + // Unmarshal and check all parsers. + for _, c := range cases { + k := koanf.New(delim) + assert.Nil(k.Load(file.Provider(c.file), c.parser), + fmt.Sprintf("error loading: %v", c.file)) + ts := testStructFlat{} + assert.Nil(k.UnmarshalWithConf("", &ts, koanf.UnmarshalConf{Tag: "koanf", FlatPaths: true}), "unmarshal failed") + real.Type = c.typeName + real.Parent1Child1Type = c.typeName + assert.Equal(real, ts, "unmarshalled structs don't match") + } +} + +func TestUnmarshalCustomText(t *testing.T) { + test := struct { + V1 customText `koanf:"v1"` + V2 string `koanf:"v2"` + V3 customText `koanf:"v3"` + }{} + + // Test custom unmarshalling of strings via mapstructure's UnmarshalText() + // methods. customText is an int type that strips of the `mb` suffix and parses + // the rest into a number. + + k := koanf.New(delim) + err := k.Load(rawbytes.Provider([]byte(`{"v1": "42mb", "v2": "42mb"}`)), json.Parser()) + assert.NoError(t, err) + + k.Unmarshal("", &test) + assert.Equal(t, int(test.V1), 42) + assert.Equal(t, test.V2, "42mb") + assert.Equal(t, int(test.V3), 0) +} + +func TestMarshal(t *testing.T) { + assert := assert.New(t) + + for _, c := range cases { + // HCL does not support marshalling. + if c.typeName == "hcl" { + continue + } + + // Load config. + k := koanf.New(delim) + assert.NoError(k.Load(file.Provider(c.file), c.parser), + fmt.Sprintf("error loading: %v", c.file)) + + // Serialize / marshal into raw bytes using the parser. + b, err := k.Marshal(c.parser) + assert.NoError(err, "error marshalling") + + // Reload raw serialize bytes into a new koanf instance. + k = koanf.New(delim) + assert.NoError(k.Load(rawbytes.Provider(b), c.parser), + fmt.Sprintf("error loading: %v", c.file)) + + // Check if values are intact. + assert.Equal(float64(1234), c.koanf.MustFloat64("parent1.id")) + assert.Equal([]string{"red", "blue", "orange"}, c.koanf.MustStrings("orphan")) + assert.Equal([]int64{1, 2, 3}, c.koanf.MustInt64s("parent1.child1.grandchild1.ids")) + } +} + +func TestGetExists(t *testing.T) { + assert := assert.New(t) + + type exCase struct { + path string + exists bool + } + exCases := []exCase{ + {"xxxxx", false}, + {"parent1.child2", false}, + {"child1", false}, + {"child2", false}, + {"type", true}, + {"parent1", true}, + {"parent2", true}, + {"parent1.name", true}, + {"parent2.name", true}, + {"parent1.child1", true}, + {"parent2.child2", true}, + {"parent1.child1.grandchild1", true}, + {"parent1.child1.grandchild1.on", true}, + {"parent2.child2.grandchild2.on", true}, + } + for _, c := range exCases { + assert.Equal(c.exists, cases[0].koanf.Exists(c.path), + fmt.Sprintf("path resolution failed: %s", c.path)) + assert.Equal(c.exists, cases[0].koanf.Get(c.path) != nil, + fmt.Sprintf("path resolution failed: %s", c.path)) + } +} + +func TestSlices(t *testing.T) { + assert := assert.New(t) + + // Load a slice of confmaps [{}, {}]. + var mp map[string]any + err := encjson.Unmarshal([]byte(`{ + "parent": [ + {"value": 1, "sub": {"value": "1"}}, + {"value": 2, "sub": {"value": "2"}} + ], + "another": "123" + }`), &mp) + assert.NoError(err, "error marshalling test payload") + k := koanf.New(delim) + assert.NoError(k.Load(confmap.Provider(mp, "."), nil)) + + assert.Empty(k.Slices("x")) + assert.Empty(k.Slices("parent.value")) + assert.Empty(k.Slices("parent.value.sub")) + + slices := k.Slices("parent") + assert.NotNil(slices, "got nil slice of confmap") + assert.NotEmpty(slices, "got empty confmap slice") + + for i, s := range slices { + assert.Equal(s.Int("value"), i+1) + assert.Equal(s.String("sub.value"), fmt.Sprintf("%d", i+1)) + } +} + +func TestSlicesNativeMapSlice(t *testing.T) { + assert := assert.New(t) + + // A natively-typed []map[string]any (as produced by the confmap and + // structs providers, and named in Slices' own doc) must be handled too, + // not just a []any of maps decoded by a parser. It was silently dropped. + k := koanf.New(delim) + require.NoError(t, k.Load(confmap.Provider(map[string]any{ + "servers": []map[string]any{ + {"host": "a", "port": 1}, + {"host": "b", "port": 2}, + }, + }, "."), nil)) + + slices := k.Slices("servers") + require.Len(t, slices, 2, "Slices dropped a []map[string]any value") + assert.Equal("a", slices[0].String("host")) + assert.Equal(1, slices[0].Int("port")) + assert.Equal("b", slices[1].String("host")) + assert.Equal(2, slices[1].Int("port")) +} + +func TestSlicesEdgeCases(t *testing.T) { + for _, tt := range []struct { + name string + value any + want []map[string]any + }{ + {"nil_native_slice", []map[string]any(nil), []map[string]any{}}, + {"empty_native_slice", []map[string]any{}, []map[string]any{}}, + {"nil_interface_slice", []any(nil), []map[string]any{}}, + {"empty_interface_slice", []any{}, []map[string]any{}}, + {"nil_and_empty_native_maps", []map[string]any{nil, {}}, []map[string]any{{}, {}}}, + {"nil_and_empty_interface_maps", []any{map[string]any(nil), map[string]any{}}, []map[string]any{{}, {}}}, + { + "mixed_interface_slice", + []any{nil, 42, map[string]any{"host": "a"}, "invalid", map[string]any{"host": "b"}}, + []map[string]any{{"host": "a"}, {"host": "b"}}, + }, + } { + t.Run(tt.name, func(t *testing.T) { + k := koanf.New(delim) + require.NoError(t, k.Load(confmap.Provider(map[string]any{"servers": tt.value}, ""), nil)) + + slices := k.Slices("servers") + require.NotNil(t, slices) + require.Len(t, slices, len(tt.want)) + for i, child := range slices { + assert.Equal(t, tt.want[i], child.Raw()) + } + }) + } +} + +func TestSlicesCopyIsolation(t *testing.T) { + for _, native := range []bool{false, true} { + name := "interface_slice" + if native { + name = "native_slice" + } + t.Run(name, func(t *testing.T) { + // Reuse the same map to check that siblings are independent too. + mp := map[string]any{"nested": map[string]any{"host": "original"}} + var value any = []any{mp, mp} + if native { + value = []map[string]any{mp, mp} + } + + k := koanf.New("/") + require.NoError(t, k.Load(confmap.Provider(map[string]any{"servers": value}, ""), nil)) + before := k.Get("servers") + slices := k.Slices("servers") + require.Len(t, slices, 2) + assert.Equal(t, "/", slices[0].Delim()) + assert.Equal(t, "original", slices[0].String("nested/host")) + + require.NoError(t, slices[0].Set("nested/host", "changed")) + assert.Equal(t, "changed", slices[0].String("nested/host")) + assert.Equal(t, "original", slices[1].String("nested/host")) + assert.Equal(t, before, k.Get("servers")) + }) + } +} + +func TestGetTypes(t *testing.T) { + assert := assert.New(t) + for _, c := range cases { + assert.Equal(nil, c.koanf.Get("xxx")) + assert.Equal(make(map[string]any), c.koanf.Get("empty")) + + // Int. + assert.Equal(int64(0), c.koanf.Int64("xxxx")) + assert.Equal(int64(1234), c.koanf.Int64("parent1.id")) + + assert.Equal(int(0), c.koanf.Int("xxxx")) + assert.Equal(int(1234), c.koanf.Int("parent1.id")) + + assert.Equal([]int64{}, c.koanf.Int64s("xxxx")) + assert.Equal([]int64{1, 2, 3}, c.koanf.Int64s("parent1.child1.grandchild1.ids")) + + assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.intmap")) + assert.Equal(map[string]int64{}, c.koanf.Int64Map("parent1.boolmap")) + assert.Equal(map[string]int64{}, c.koanf.Int64Map("xxxx")) + assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Int64Map("parent1.floatmap")) + + assert.Equal([]int{1, 2, 3}, c.koanf.Ints("parent1.child1.grandchild1.ids")) + assert.Equal([]int{}, c.koanf.Ints("xxxx")) + + assert.Equal(map[string]int{"key1": 1, "key2": 1, "key3": 1}, c.koanf.IntMap("parent1.intmap")) + assert.Equal(map[string]int{}, c.koanf.IntMap("parent1.boolmap")) + assert.Equal(map[string]int{}, c.koanf.IntMap("xxxx")) + + // Float. + assert.Equal(float64(0), c.koanf.Float64("xxx")) + assert.Equal(float64(1234), c.koanf.Float64("parent1.id")) + + assert.Equal([]float64{}, c.koanf.Float64s("xxxx")) + assert.Equal([]float64{1, 2, 3}, c.koanf.Float64s("parent1.child1.grandchild1.ids")) + + assert.Equal(map[string]float64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.Float64Map("parent1.intmap")) + assert.Equal(map[string]float64{"key1": 1.1, "key2": 1.2, "key3": 1.3}, c.koanf.Float64Map("parent1.floatmap")) + assert.Equal(map[string]float64{}, c.koanf.Float64Map("parent1.boolmap")) + assert.Equal(map[string]float64{}, c.koanf.Float64Map("xxxx")) + + // String and bytes. + assert.Equal([]byte{}, c.koanf.Bytes("xxxx")) + assert.Equal([]byte("parent1"), c.koanf.Bytes("parent1.name")) + + assert.Equal("", c.koanf.String("xxxx")) + assert.Equal("parent1", c.koanf.String("parent1.name")) + + assert.Equal([]string{}, c.koanf.Strings("xxxx")) + assert.Equal([]string{"red", "blue", "orange"}, c.koanf.Strings("orphan")) + + assert.Equal(map[string]string{"key1": "val1", "key2": "val2", "key3": "val3"}, c.koanf.StringMap("parent1.strmap")) + assert.Equal(map[string][]string{"key1": {"val1", "val2", "val3"}, "key2": {"val4", "val5"}}, c.koanf.StringsMap("parent1.strsmap")) + assert.Equal(map[string]string{}, c.koanf.StringMap("xxxx")) + assert.Equal(map[string]string{}, c.koanf.StringMap("parent1.intmap")) + + // Bools. + assert.Equal(false, c.koanf.Bool("xxxx")) + assert.Equal(false, c.koanf.Bool("type")) + assert.Equal(true, c.koanf.Bool("parent1.child1.grandchild1.on")) + assert.Equal(true, c.koanf.Bool("strbool")) + + assert.Equal([]bool{}, c.koanf.Bools("xxxx")) + assert.Equal([]bool{true, false, true}, c.koanf.Bools("bools")) + assert.Equal([]bool{true, false, true}, c.koanf.Bools("intbools")) + assert.Equal([]bool{true, true, false}, c.koanf.Bools("strbools")) + + assert.Equal(map[string]bool{"ok1": true, "ok2": true, "notok3": false}, c.koanf.BoolMap("parent1.boolmap")) + assert.Equal(map[string]bool{"key1": true, "key2": true, "key3": true}, c.koanf.BoolMap("parent1.intmap")) + assert.Equal(map[string]bool{}, c.koanf.BoolMap("xxxx")) + + // Others. + assert.Equal(time.Duration(1234), c.koanf.Duration("parent1.id")) + assert.Equal(time.Duration(0), c.koanf.Duration("xxxx")) + assert.Equal(time.Second*3, c.koanf.Duration("duration")) + + assert.Equal(time.Time{}, c.koanf.Time("xxxx", "2006-01-02")) + assert.Equal(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), c.koanf.Time("time", "2006-01-02")) + + assert.Equal([]string{}, c.koanf.MapKeys("xxxx"), "map keys mismatch") + assert.Equal([]string{"bools", "duration", "empty", "intbools", "negative_int", "orphan", "parent1", "parent2", "strbool", "strbools", "time", "type"}, + c.koanf.MapKeys(""), "map keys mismatch") + assert.Equal([]string{"key1", "key2", "key3"}, c.koanf.MapKeys("parent1.strmap"), "map keys mismatch") + + // Attempt to parse int=1234 as a Unix timestamp. + assert.Equal(time.Date(1970, 1, 1, 0, 20, 34, 0, time.UTC), c.koanf.Time("parent1.id", "").UTC()) + } +} + +func TestMustGetTypes(t *testing.T) { + assert := assert.New(t) + for _, c := range cases { + // Int. + assert.Panics(func() { c.koanf.MustInt64("xxxx") }) + assert.Equal(int64(1234), c.koanf.MustInt64("parent1.id")) + assert.Equal(int64(-1234), c.koanf.MustInt64("negative_int")) + + assert.Panics(func() { c.koanf.MustInt("xxxx") }) + assert.Equal(int(1234), c.koanf.MustInt("parent1.id")) + assert.Equal(int(-1234), c.koanf.MustInt("negative_int")) + + assert.Panics(func() { c.koanf.MustInt64s("xxxx") }) + assert.Equal([]int64{1, 2, 3}, c.koanf.MustInt64s("parent1.child1.grandchild1.ids")) + + assert.Panics(func() { c.koanf.MustInt64Map("xxxx") }) + assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustInt64Map("parent1.intmap")) + + assert.Panics(func() { c.koanf.MustInt64Map("parent1.boolmap") }) + assert.Equal(map[string]int64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustInt64Map("parent1.floatmap")) + + assert.Panics(func() { c.koanf.MustInts("xxxx") }) + assert.Equal([]int{1, 2, 3}, c.koanf.MustInts("parent1.child1.grandchild1.ids")) + + assert.Panics(func() { c.koanf.MustIntMap("xxxx") }) + assert.Panics(func() { c.koanf.MustIntMap("parent1.boolmap") }) + assert.Equal(map[string]int{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustIntMap("parent1.intmap")) + + // Float. + assert.Panics(func() { c.koanf.MustInts("xxxx") }) + assert.Equal(float64(1234), c.koanf.MustFloat64("parent1.id")) + + assert.Panics(func() { c.koanf.MustFloat64s("xxxx") }) + assert.Equal([]float64{1, 2, 3}, c.koanf.MustFloat64s("parent1.child1.grandchild1.ids")) + + assert.Panics(func() { c.koanf.MustFloat64Map("xxxx") }) + assert.Panics(func() { c.koanf.MustFloat64Map("parent1.boolmap") }) + assert.Equal(map[string]float64{"key1": 1.1, "key2": 1.2, "key3": 1.3}, c.koanf.MustFloat64Map("parent1.floatmap")) + assert.Equal(map[string]float64{"key1": 1, "key2": 1, "key3": 1}, c.koanf.MustFloat64Map("parent1.intmap")) + + // String and bytes. + assert.Panics(func() { c.koanf.MustBytes("xxxx") }) + assert.Equal([]byte("parent1"), c.koanf.MustBytes("parent1.name")) + + assert.Panics(func() { c.koanf.MustString("xxxx") }) + assert.Equal("parent1", c.koanf.MustString("parent1.name")) + + assert.Panics(func() { c.koanf.MustStrings("xxxx") }) + assert.Equal([]string{"red", "blue", "orange"}, c.koanf.MustStrings("orphan")) + + assert.Panics(func() { c.koanf.MustStringMap("xxxx") }) + assert.Panics(func() { c.koanf.MustStringMap("parent1.intmap") }) + assert.Equal(map[string]string{"key1": "val1", "key2": "val2", "key3": "val3"}, c.koanf.MustStringMap("parent1.strmap")) + assert.Equal(map[string][]string{"key1": {"val1", "val2", "val3"}, "key2": {"val4", "val5"}}, c.koanf.MustStringsMap("parent1.strsmap")) + + // // Bools. + assert.Panics(func() { c.koanf.MustBools("xxxx") }) + assert.Equal([]bool{true, false, true}, c.koanf.MustBools("bools")) + assert.Equal([]bool{true, false, true}, c.koanf.MustBools("intbools")) + assert.Equal([]bool{true, true, false}, c.koanf.MustBools("strbools")) + + assert.Panics(func() { c.koanf.MustBoolMap("xxxx") }) + assert.Equal(map[string]bool{"ok1": true, "ok2": true, "notok3": false}, c.koanf.MustBoolMap("parent1.boolmap")) + assert.Equal(map[string]bool{"key1": true, "key2": true, "key3": true}, c.koanf.MustBoolMap("parent1.intmap")) + + // Others. + assert.Panics(func() { c.koanf.MustDuration("xxxx") }) + assert.Equal(time.Duration(1234), c.koanf.MustDuration("parent1.id")) + assert.Equal(time.Second*3, c.koanf.MustDuration("duration")) + assert.Equal(time.Duration(-1234), c.koanf.MustDuration("negative_int")) + + assert.Panics(func() { c.koanf.MustTime("xxxx", "2006-01-02") }) + assert.Equal(time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC), c.koanf.MustTime("time", "2006-01-02")) + + // // Attempt to parse int=1234 as a Unix timestamp. + assert.Panics(func() { c.koanf.MustTime("time", "2006") }) + assert.Equal(time.Date(1970, 1, 1, 0, 20, 34, 0, time.UTC), c.koanf.MustTime("parent1.id", "").UTC()) + } +} + +func TestDelete(t *testing.T) { + assert := assert.New(t) + for _, c := range cases { + c.koanf.Delete("parent2.child2.grandchild2") + assert.Equal(false, c.koanf.Exists("parent2.child2.grandchild2.on")) + assert.Equal(false, c.koanf.Exists("parent2.child2.grandchild2.ids.5")) + assert.Equal(true, c.koanf.Exists("parent2.child2.name")) + + c.koanf.Delete("") + assert.Equal(false, c.koanf.Exists("duration")) + assert.Equal(false, c.koanf.Exists("empty")) + } +} + +func TestGetStringsMap(t *testing.T) { + assert := assert.New(t) + + k := koanf.New(delim) + + k.Load(confmap.Provider(map[string]any{ + "str": map[string]string{ + "k1": "value", + }, + "strs": map[string][]string{ + "k1": {"value"}, + }, + "iface": map[string]any{ + "k2": "value", + }, + "ifaces": map[string][]any{ + "k2": {"value"}, + }, + "ifaces2": map[string]any{ + "k2": []any{"value"}, + }, + "ifaces3": map[string]any{ + "k2": []string{"value"}, + }, + }, "."), nil) + assert.Equal(map[string]string{"k1": "value"}, k.StringMap("str"), "types don't match") + assert.Equal(map[string]string{"k2": "value"}, k.StringMap("iface"), "types don't match") + assert.Equal(map[string][]string{"k1": {"value"}}, k.StringsMap("strs"), "types don't match") + assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces"), "types don't match") + assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces2"), "types don't match") + assert.Equal(map[string][]string{"k2": {"value"}}, k.StringsMap("ifaces3"), "types don't match") +} + +func TestGetStringsMapEmptySlices(t *testing.T) { + for _, tt := range []struct { + name string + value any + }{ + {"string_slices", map[string][]string{"empty": {}, "nil": nil}}, + {"interface_slices", map[string][]any{"empty": {}, "nil": nil}}, + {"interface_map", map[string]any{"empty": []any{}, "nil": []any(nil)}}, + } { + t.Run(tt.name, func(t *testing.T) { + k := koanf.New(delim) + require.NoError(t, k.Set("lists", tt.value)) + want := map[string][]string{"empty": nil, "nil": nil} + require.Equal(t, want, k.StringsMap("lists")) + require.Equal(t, want, k.MustStringsMap("lists")) + }) + } + + t.Run("json", func(t *testing.T) { + k := koanf.New(delim) + require.NoError(t, k.Load(rawbytes.Provider([]byte(`{"lists":{"empty":[],"populated":["value"]}}`)), json.Parser())) + require.Equal(t, map[string][]string{"empty": nil, "populated": {"value"}}, k.StringsMap("lists")) + }) + + t.Run("invalid_element", func(t *testing.T) { + k := koanf.New(delim) + require.NoError(t, k.Set("lists", map[string][]any{"empty": {}, "invalid": {42}})) + require.Empty(t, k.StringsMap("lists")) + require.Panics(t, func() { k.MustStringsMap("lists") }) + }) +} + +func TestBoolsNativeSlice(t *testing.T) { + assert := assert.New(t) + + k := koanf.New(delim) + assert.NoError(k.Load(confmap.Provider(map[string]any{ + "bools": []bool{true, false, true}, + }, "."), nil)) + + assert.Equal([]bool{true, false, true}, k.Bools("bools")) +} + +func TestTypedGettersNativeMaps(t *testing.T) { + for _, tt := range []struct { + name string + value any + get func(*koanf.Koanf, string) any + }{ + {"int", map[string]int{"a": 1, "b": 2}, func(k *koanf.Koanf, path string) any { return k.IntMap(path) }}, + {"int64", map[string]int64{"a": 1 << 54, "b": -2}, func(k *koanf.Koanf, path string) any { return k.Int64Map(path) }}, + {"float64", map[string]float64{"a": 1.5, "b": -2.5}, func(k *koanf.Koanf, path string) any { return k.Float64Map(path) }}, + {"bool", map[string]bool{"a": true, "b": false}, func(k *koanf.Koanf, path string) any { return k.BoolMap(path) }}, + {"nil_int", map[string]int(nil), func(k *koanf.Koanf, path string) any { return k.IntMap(path) }}, + {"nil_int64", map[string]int64(nil), func(k *koanf.Koanf, path string) any { return k.Int64Map(path) }}, + {"nil_float64", map[string]float64(nil), func(k *koanf.Koanf, path string) any { return k.Float64Map(path) }}, + {"nil_bool", map[string]bool(nil), func(k *koanf.Koanf, path string) any { return k.BoolMap(path) }}, + } { + t.Run(tt.name, func(t *testing.T) { + k := koanf.New(delim) + require.NoError(t, k.Load(confmap.Provider(map[string]any{"value": tt.value}, ""), nil)) + got := tt.get(k, "value") + require.NotNil(t, got) + if strings.HasPrefix(tt.name, "nil_") { + assert.Empty(t, got) + return + } + assert.Equal(t, tt.value, got) + + // Changing the returned map must not change the stored configuration. + switch mp := got.(type) { + case map[string]int: + mp["a"] = 0 + case map[string]int64: + mp["a"] = 0 + case map[string]float64: + mp["a"] = 0 + case map[string]bool: + mp["a"] = false + } + assert.Equal(t, tt.value, tt.get(k, "value")) + }) + } +} + +// waitTimeout waits for the waitgroup for the specified max timeout. +// Returns true if waiting timed out. +func waitTimeout(wg *sync.WaitGroup, timeout time.Duration) bool { + c := make(chan struct{}) + go func() { + defer close(c) + wg.Wait() + }() + select { + case <-c: + return false // completed normally + case <-time.After(timeout): + return true // timed out + } +} + +// TestFileWatcherRaceCondition reproduces Issue #305 +// File watcher reloading config while reader goroutine accesses values +// This test verifies our thread safety fix prevents empty string reads +func TestFileWatcherRaceCondition(t *testing.T) { + if testing.Short() { + t.Skip("skipping race condition test in short mode") + } + + // Create temp config file + tmpDir, err := os.MkdirTemp("", "koanf_race_*") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + + tmpFile := filepath.Join(tmpDir, "config.yaml") + writeConfig := func(value string) { + content := fmt.Sprintf("rpc: %q\n", value) + os.WriteFile(tmpFile, []byte(content), 0600) + } + + // Initial config + writeConfig("initial") + + k := koanf.New(".") + provider := file.Provider(tmpFile) + err = k.Load(provider, yaml.Parser()) + if err != nil { + t.Fatal(err) + } + + // Setup file watcher that reloads the SAME koanf instance + // This tests our internal thread safety, not user pattern issues + var reloadCount int64 + provider.Watch(func(event any, err error) { + if err != nil { + t.Logf("watch error: %v", err) + return + } + + // Reload into the same koanf instance - this tests our thread safety + err = k.Load(provider, yaml.Parser()) + if err != nil { + t.Logf("reload error: %v", err) + } + // Use atomic to avoid race in test counters + atomic.AddInt64(&reloadCount, 1) + }) + + // Start reader goroutine that continuously reads the config + done := make(chan struct{}) + var emptyCount, totalReads int64 + + go func() { + for { + select { + case <-done: + return + default: + value := k.String("rpc") + reads := atomic.AddInt64(&totalReads, 1) + if value == "" { + atomic.AddInt64(&emptyCount, 1) + // With proper thread safety, this should never happen + t.Errorf("Got empty string on read #%d", reads) + } + time.Sleep(time.Microsecond) // Small delay to allow interleaving + } + } + }() + + // Trigger multiple file changes to increase chance of race + for i := 0; i < 5; i++ { + time.Sleep(20 * time.Millisecond) + writeConfig(fmt.Sprintf("value-%d", i)) + } + + // Wait for file watching to settle + time.Sleep(100 * time.Millisecond) + close(done) + + finalReads := atomic.LoadInt64(&totalReads) + finalEmpties := atomic.LoadInt64(&emptyCount) + finalReloads := atomic.LoadInt64(&reloadCount) + + t.Logf("Total reads: %d, Empty reads: %d, Reloads: %d", finalReads, finalEmpties, finalReloads) + if finalEmpties > 0 { + t.Errorf("Thread safety issue: got %d empty reads out of %d total reads", finalEmpties, finalReads) + } +} + +// TestConcurrentLoadRaceCondition reproduces Issue #335 +// Multiple goroutines calling k.Load() simultaneously +// This test should fail with "concurrent map writes" panic +func TestConcurrentLoadRaceCondition(t *testing.T) { + if testing.Short() { + t.Skip("skipping race condition test in short mode") + } + + k := koanf.New(".") + + // Number of concurrent goroutines + numGoroutines := 10 + numLoadsPerGoroutine := 10 + + var wg sync.WaitGroup + + // Channel to collect any panics + panics := make(chan any, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + panics <- r + } + }() + + for j := 0; j < numLoadsPerGoroutine; j++ { + // Create different configs to load + config := map[string]any{ + fmt.Sprintf("key_%d_%d", id, j): fmt.Sprintf("value_%d_%d", id, j), + "common": fmt.Sprintf("common_%d_%d", id, j), + } + + // This should trigger concurrent map writes + err := k.Load(confmap.Provider(config, "."), nil) + if err != nil { + t.Errorf("Load failed: %v", err) + } + + // Small delay to increase chance of race + time.Sleep(time.Microsecond) + } + }(i) + } + + wg.Wait() + close(panics) + + // Check if we got any panics (which indicates the race condition) + panicCount := 0 + for p := range panics { + panicCount++ + t.Logf("Concurrent Load panic: %v", p) + } + + if panicCount > 0 { + t.Errorf("Race condition detected: got %d panics from concurrent Load operations", panicCount) + } +} + +// TestConcurrentReadWriteMix tests mixed concurrent reads and writes +// This should expose various race conditions with inconsistent reads +func TestConcurrentReadWriteMix(t *testing.T) { + if testing.Short() { + t.Skip("skipping race condition test in short mode") + } + + k := koanf.New(".") + + // Initialize with some data + k.Load(confmap.Provider(map[string]any{ + "test.key": "initial", + "counter": 0, + }, "."), nil) + + done := make(chan struct{}) + var wg sync.WaitGroup + + // Start multiple reader goroutines + for i := 0; i < 5; i++ { + wg.Add(1) + go func(readerID int) { + defer wg.Done() + readCount := 0 + for { + select { + case <-done: + t.Logf("Reader %d: performed %d reads", readerID, readCount) + return + default: + // Mix different types of reads + _ = k.String("test.key") + _ = k.Int("counter") + _ = k.Keys() + _ = k.All() + readCount++ + time.Sleep(time.Microsecond) + } + } + }(i) + } + + // Start multiple writer goroutines + for i := 0; i < 3; i++ { + wg.Add(1) + go func(writerID int) { + defer wg.Done() + writeCount := 0 + for { + select { + case <-done: + t.Logf("Writer %d: performed %d writes", writerID, writeCount) + return + default: + // Mix different types of writes + config := map[string]any{ + "test.key": fmt.Sprintf("writer-%d-count-%d", writerID, writeCount), + "counter": writeCount, + fmt.Sprintf("dynamic.key.%d", writeCount): writerID, + } + k.Load(confmap.Provider(config, "."), nil) + writeCount++ + time.Sleep(time.Microsecond) + } + } + }(i) + } + + // Let the race run for a short time + time.Sleep(50 * time.Millisecond) + close(done) + wg.Wait() + + t.Log("Concurrent read/write test completed - check for race detector warnings") +} + +// TestConcurrentEdgeCases tests concurrent access to edge case methods +func TestConcurrentEdgeCases(t *testing.T) { + if testing.Short() { + t.Skip("skipping race condition test in short mode") + } + + k := koanf.New(".") + k.Load(confmap.Provider(map[string]any{ + "parent": map[string]any{ + "child": "value", + }, + "list": []any{"a", "b", "c"}, + }, "."), nil) + + done := make(chan struct{}) + var wg sync.WaitGroup + + // Test concurrent Cut operations + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-done: + return + default: + _ = k.Cut("parent") + time.Sleep(time.Microsecond) + } + } + }() + + // Test concurrent Copy operations + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-done: + return + default: + _ = k.Copy() + time.Sleep(time.Microsecond) + } + } + }() + + // Test concurrent MapKeys access + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-done: + return + default: + _ = k.MapKeys("parent") + time.Sleep(time.Microsecond) + } + } + }() + + // Test concurrent modifications + wg.Add(1) + go func() { + defer wg.Done() + count := 0 + for { + select { + case <-done: + return + default: + k.Set(fmt.Sprintf("dynamic.%d", count), count) + count++ + time.Sleep(time.Microsecond) + } + } + }() + + time.Sleep(30 * time.Millisecond) + close(done) + wg.Wait() + + t.Log("Concurrent edge cases test completed - check for race detector warnings") +} + +// TestNoDeadlock verifies that our locking patterns don't cause deadlocks +// Tests concurrent reads and writes with various method combinations +func TestNoDeadlock(t *testing.T) { + if testing.Short() { + t.Skip("skipping deadlock test in short mode") + } + + k := koanf.New(".") + k.Load(confmap.Provider(map[string]any{ + "parent": map[string]any{ + "child": "value", + "count": 42, + }, + "list": []any{"a", "b", "c"}, + }, "."), nil) + + // Test duration + testDuration := 500 * time.Millisecond + + var wg sync.WaitGroup + done := make(chan struct{}) + + // Start multiple reader goroutines with different read patterns + for i := 0; i < 3; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + readCount := 0 + for { + select { + case <-done: + t.Logf("Reader %d completed %d operations", id, readCount) + return + default: + // Mix different read operations + _ = k.Get("parent.child") + _ = k.Keys() + _ = k.All() + _ = k.Raw() + _ = k.Exists("parent") + _ = k.Sprint() // This internally uses RLock and avoids calling Keys() + _ = k.KeyMap() + _ = k.MapKeys("parent") + + // Test methods that call other locked methods + _ = k.Cut("parent") // Calls Get() + _ = k.Copy() // Calls Cut() → Get() + + readCount++ + if readCount%100 == 0 { + time.Sleep(time.Microsecond) + } + } + } + }(i) + } + + // Start writer goroutines with different write patterns + for i := 0; i < 2; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + writeCount := 0 + for { + select { + case <-done: + t.Logf("Writer %d completed %d operations", id, writeCount) + return + default: + // Mix different write operations + k.Set(fmt.Sprintf("writer_%d.key", id), writeCount) + k.Delete("nonexistent") // Should be safe + + // Load new data + newData := map[string]any{ + fmt.Sprintf("load_%d", writeCount): id, + "nested": map[string]any{ + "value": writeCount, + }, + } + k.Load(confmap.Provider(newData, "."), nil) + + // Merge operations + other := koanf.New(".") + other.Set("merge_key", writeCount) + k.Merge(other) + + writeCount++ + if writeCount%50 == 0 { + time.Sleep(time.Microsecond) + } + } + } + }(i) + } + + // Run the test for specified duration + time.Sleep(testDuration) + close(done) + + // Use a timeout to detect deadlocks + waitChan := make(chan struct{}) + go func() { + wg.Wait() + close(waitChan) + }() + + select { + case <-waitChan: + t.Log("Deadlock test completed successfully - no deadlocks detected") + case <-time.After(5 * time.Second): + t.Fatal("DEADLOCK DETECTED: Goroutines did not complete within timeout") + } +} + +// TestFileProviderConcurrency specifically tests the file provider's synchronization +// under heavy concurrent load to catch any races in Watch/Unwatch +func TestFileProviderConcurrency(t *testing.T) { + if testing.Short() { + t.Skip("skipping file provider concurrency test in short mode") + } + + // Create temp config file + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "config.json") + initialData := []byte(`{"test": "value"}`) + err := os.WriteFile(tmpFile, initialData, 0600) + if err != nil { + t.Fatal(err) + } + + var wg sync.WaitGroup + done := make(chan struct{}) + + // Stress test: multiple goroutines rapidly watch/unwatch the same file + for i := 0; i < 5; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + + for { + select { + case <-done: + return + default: + // Create new file provider for each iteration + f := file.Provider(tmpFile) + + // Try to watch + var watchErr error + watchErr = f.Watch(func(event any, err error) { + // Simple callback that doesn't do much + _ = event + _ = err + }) + + // Sometimes watch will fail if another goroutine is already watching + if watchErr == nil { + // If watch succeeded, unwatch after a short delay + time.Sleep(time.Millisecond) + unwatchErr := f.Unwatch() + if unwatchErr != nil { + // Log but don't fail - this can happen during cleanup + t.Logf("Unwatch error (normal during stress test): %v", unwatchErr) + } + } else if watchErr.Error() != "file is already being watched" { + // Unexpected error + t.Errorf("Unexpected watch error: %v", watchErr) + } + + // Small delay to prevent tight loop + time.Sleep(time.Microsecond) + } + } + }(i) + } + + // Also test concurrent file modifications while watching + wg.Add(1) + go func() { + defer wg.Done() + counter := 0 + + for { + select { + case <-done: + return + default: + // Write new data to trigger file events + newData := fmt.Sprintf(`{"test": "value", "counter": %d}`, counter) + os.WriteFile(tmpFile, []byte(newData), 0600) + counter++ + time.Sleep(5 * time.Millisecond) + } + } + }() + + // Let the stress test run for 200ms + time.Sleep(200 * time.Millisecond) + close(done) + + // Wait with timeout to detect deadlocks + waitChan := make(chan struct{}) + go func() { + wg.Wait() + close(waitChan) + }() + + select { + case <-waitChan: + t.Log("File provider concurrency test completed successfully") + case <-time.After(5 * time.Second): + t.Fatal("FILE PROVIDER DEADLOCK: Goroutines did not complete within timeout") + } +} + +// TestGetNilPointer tests Get()'ing nil pointers. +func TestGetNilPointer(t *testing.T) { + assert := assert.New(t) + k := koanf.New(".") + + type test struct { + Name string + } + var nt *test + assert.Nil(k.Set("key", nt)) + assert.True(k.Exists("key")) + got := k.Get("key") + assert.Nil(got) + _, ok := got.(*test) + assert.True(ok, "expected type *test, got %T", got) + + // Test nil value. + assert.Nil(k.Set("val", nil)) + assert.Nil(k.Get("val")) + + // Test slice. + var s *[]string + assert.Nil(k.Set("slice", s)) + gotSlice := k.Get("slice") + assert.Nil(gotSlice) + _, ok = gotSlice.(*[]string) + assert.True(ok, "expected type *[]string, got %T", gotSlice) +} + +// TesConvertInt64 tests various int64 conversion/overflow scenarios. +func TesConvertInt64(t *testing.T) { + assert := assert.New(t) + k := koanf.New(delim) + + assert.Nil(k.Load(confmap.Provider(map[string]any{ + // Good values. + "int64": int64(math.MaxInt64), + "uint64": uint64(math.MaxInt64), + "strmaxint": "9223372036854775807", + "strminint": "-9223372036854775808", + "str2p53": "9007199254740993", + "float": 1.9, + "minfloat": float64(math.MinInt64), + "strfloat": "3.7", + + // Overflows. + "bigfloat": float64(math.MaxInt64), + "hugefloat": 1e300, + "neghuge": -1e300, + "inf": math.Inf(1), + "neginf": math.Inf(-1), + "nan": math.NaN(), + "biguint64": uint64(math.MaxInt64) + 1, + }, delim), nil)) + + assert.Equal(int64(math.MaxInt64), k.Int64("int64")) + assert.Equal(int64(math.MaxInt64), k.Int64("uint64")) + assert.Equal(int64(math.MaxInt64), k.Int64("strmaxint")) + assert.Equal(int64(math.MinInt64), k.Int64("strminint")) + assert.Equal(int64(9007199254740993), k.Int64("str2p53")) + assert.Equal(int64(1), k.Int64("float")) + assert.Equal(int64(math.MinInt64), k.Int64("minfloat")) + assert.Equal(int64(3), k.Int64("strfloat")) + + // Out of range must return 0 and not a wrapped value. + for _, key := range []string{"bigfloat", "hugefloat", "neghuge", "inf", "neginf", "nan", "biguint64"} { + assert.Equal(int64(0), k.Int64(key), "Int64(%q) must be 0", key) + } +}