From d54d79012b7c7dcef89aaedd987752a0f235465a Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:24:12 +0200 Subject: [PATCH] Upstream gookit/config state of the touched files before 8a81b3bce7 --- read.go | 684 ++++++++++++++++++++++++++++++++++++++++++ read_test.go | 501 +++++++++++++++++++++++++++++++ testdata/issues59.ini | 16 + 3 files changed, 1201 insertions(+) create mode 100644 read.go create mode 100644 read_test.go create mode 100644 testdata/issues59.ini diff --git a/read.go b/read.go new file mode 100644 index 0000000..3bea68c --- /dev/null +++ b/read.go @@ -0,0 +1,684 @@ +package config + +import ( + "strconv" + "strings" + "time" + + "github.com/gookit/goutil/envutil" + "github.com/gookit/goutil/maputil" + "github.com/gookit/goutil/mathutil" + "github.com/gookit/goutil/strutil" +) + +// Exists key exists check +func Exists(key string, findByPath ...bool) bool { return dc.Exists(key, findByPath...) } + +// Exists key exists check +func (c *Config) Exists(key string, findByPath ...bool) (ok bool) { + sep := c.opts.Delimiter + if key = formatKey(key, string(sep)); key == "" { + return + } + + if _, ok = c.data[key]; ok { + return + } + + // disable find by path. + if len(findByPath) > 0 && !findByPath[0] { + return + } + + // has sub key? eg. "lang.dir" + if strings.IndexByte(key, sep) == -1 { + return + } + + keys := strings.Split(key, string(sep)) + topK := keys[0] + + // find top item data based on top key + var item any + if item, ok = c.data[topK]; !ok { + return + } + for _, k := range keys[1:] { + switch typeData := item.(type) { + case map[string]int: // is map(from Set) + if item, ok = typeData[k]; !ok { + return + } + case map[string]string: // is map(from Set) + if item, ok = typeData[k]; !ok { + return + } + case map[string]any: // is map(decode from toml/json/yaml.v3) + if item, ok = typeData[k]; !ok { + return + } + case map[any]any: // is map(decode from yaml.v2) + if item, ok = typeData[k]; !ok { + return + } + case []int: // is array(is from Set) + i, err := strconv.Atoi(k) + if err != nil || i >= len(typeData) { + return false + } + item = typeData[i] + case []string: // is array(is from Set) + i, err := strconv.Atoi(k) + if err != nil || i >= len(typeData) { + return false + } + item = typeData[i] + case []any: // is array(load from file) + i, err := strconv.Atoi(k) + if err != nil || i >= len(typeData) { + return false + } + item = typeData[i] + default: // error + return false + } + } + return true +} + +/************************************************************* + * read config data + *************************************************************/ + +// Data return all config data +func Data() map[string]any { return dc.Data() } + +// Data get all config data. +// +// Note: will don't apply any options, like ParseEnv +func (c *Config) Data() map[string]any { + return c.data +} + +// Sub return a map config data by key +func Sub(key string) map[string]any { return dc.Sub(key) } + +// Sub get a map config data by key +// +// Note: will don't apply any options, like ParseEnv +func (c *Config) Sub(key string) map[string]any { + if mp, ok := c.GetValue(key); ok { + if mmp, ok := mp.(map[string]any); ok { + return mmp + } + } + return nil +} + +// Keys return all config data +func Keys() []string { return dc.Keys() } + +// Keys get all config data +func (c *Config) Keys() []string { + keys := make([]string, 0, len(c.data)) + for key := range c.data { + keys = append(keys, key) + } + return keys +} + +// Get config value by key string, support get sub-value by key path(eg. 'map.key'), +func Get(key string, findByPath ...bool) any { return dc.Get(key, findByPath...) } + +// Get config value by key, findByPath default is true. +func (c *Config) Get(key string, findByPath ...bool) any { + val, _ := c.GetValue(key, findByPath...) + return val +} + +// GetValue get value by given key string. findByPath default is true. +func GetValue(key string, findByPath ...bool) (any, bool) { + return dc.GetValue(key, findByPath...) +} + +// GetValue get value by given key string. findByPath default is true. +// +// Return: +// - ok is true, find value from config +// - ok is false, not found or error +func (c *Config) GetValue(key string, findByPath ...bool) (value any, ok bool) { + sep := c.opts.Delimiter + if key = formatKey(key, string(sep)); key == "" { + c.addError(ErrKeyIsEmpty) + return + } + + // if not is readonly + if !c.opts.Readonly { + c.lock.RLock() + defer c.lock.RUnlock() + } + + // is top key + if value, ok = c.data[key]; ok { + return + } + + // disable find by path. + if len(findByPath) > 0 && !findByPath[0] { + // c.addError(ErrNotFound) + return + } + + // has sub key? eg. "lang.dir" + if strings.IndexByte(key, sep) == -1 { + // c.addError(ErrNotFound) + return + } + + keys := strings.Split(key, string(sep)) + topK := keys[0] + + // find top item data based on top key + var item any + if item, ok = c.data[topK]; !ok { + // c.addError(ErrNotFound) + return + } + + // find child + // NOTICE: don't merge case, will result in an error. + // e.g. case []int, []string + // OR + // case []int: + // case []string: + for _, k := range keys[1:] { + switch typeData := item.(type) { + case map[string]int: // is map(from Set) + if item, ok = typeData[k]; !ok { + return + } + case map[string]string: // is map(from Set) + if item, ok = typeData[k]; !ok { + return + } + case map[string]any: // is map(decode from toml/json) + if item, ok = typeData[k]; !ok { + return + } + case map[any]any: // is map(decode from yaml) + if item, ok = typeData[k]; !ok { + return + } + case []int: // is array(is from Set) + i, err := strconv.Atoi(k) + + // check slice index + if err != nil || len(typeData) <= i { + ok = false + c.addError(err) + return + } + + item = typeData[i] + case []string: // is array(is from Set) + i, err := strconv.Atoi(k) + if err != nil || len(typeData) <= i { + ok = false + c.addError(err) + return + } + + item = typeData[i] + case []any: // is array(load from file) + i, err := strconv.Atoi(k) + if err != nil || len(typeData) <= i { + ok = false + c.addError(err) + return + } + + item = typeData[i] + default: // error + ok = false + c.addErrorf("cannot get value of the key '%s'", key) + return + } + } + + return item, true +} + +/************************************************************* + * read config (basic data type) + *************************************************************/ + +// String get a string by key +func String(key string, defVal ...string) string { return dc.String(key, defVal...) } + +// String get a string by key, if not found return default value +func (c *Config) String(key string, defVal ...string) string { + value, ok := c.getString(key) + + if !ok && len(defVal) > 0 { // give default value + value = defVal[0] + } + return value +} + +// MustString get a string by key, will panic on empty or not exists +func MustString(key string) string { return dc.MustString(key) } + +// MustString get a string by key, will panic on empty or not exists +func (c *Config) MustString(key string) string { + value, ok := c.getString(key) + if !ok { + panic("config: string value not found, key: " + key) + } + return value +} + +func (c *Config) getString(key string) (value string, ok bool) { + // find from cache + if c.opts.EnableCache && len(c.strCache) > 0 { + value, ok = c.strCache[key] + if ok { + return + } + } + + val, ok := c.GetValue(key) + if !ok { + return + } + + switch typVal := val.(type) { + // from json `int` always is float64 + case string: + value = typVal + if c.opts.ParseEnv { + value = envutil.ParseEnvValue(value) + } + default: + var err error + value, err = strutil.AnyToString(val, false) + if err != nil { + return "", false + } + } + + // add cache + if ok && c.opts.EnableCache { + if c.strCache == nil { + c.strCache = make(map[string]string) + } + c.strCache[key] = value + } + return +} + +// Int get an int by key +func Int(key string, defVal ...int) int { return dc.Int(key, defVal...) } + +// Int get a int value, if not found return default value +func (c *Config) Int(key string, defVal ...int) (value int) { + i64, exist := c.tryInt64(key) + + if exist { + value = int(i64) + } else if len(defVal) > 0 { + value = defVal[0] + } + return +} + +// Uint get a uint value, if not found return default value +func Uint(key string, defVal ...uint) uint { return dc.Uint(key, defVal...) } + +// Uint get a int value, if not found return default value +func (c *Config) Uint(key string, defVal ...uint) (value uint) { + i64, exist := c.tryInt64(key) + + if exist { + value = uint(i64) + } else if len(defVal) > 0 { + value = defVal[0] + } + return +} + +// Int64 get a int value, if not found return default value +func Int64(key string, defVal ...int64) int64 { return dc.Int64(key, defVal...) } + +// Int64 get a int value, if not found return default value +func (c *Config) Int64(key string, defVal ...int64) (value int64) { + value, exist := c.tryInt64(key) + + if !exist && len(defVal) > 0 { + value = defVal[0] + } + return +} + +// try to get an int64 value by given key +func (c *Config) tryInt64(key string) (value int64, ok bool) { + strVal, ok := c.getString(key) + if !ok { + return + } + + value, err := strconv.ParseInt(strVal, 10, 0) + if err != nil { + c.addError(err) + } + return +} + +// Duration get a time.Duration type value. if not found return default value +func Duration(key string, defVal ...time.Duration) time.Duration { return dc.Duration(key, defVal...) } + +// Duration get a time.Duration type value. if not found return default value. +// +// The stored value may be a Go duration string (e.g. "300s", "1h30m", "20m"). +// A bare integer is treated as nanoseconds for backwards compatibility. +func (c *Config) Duration(key string, defVal ...time.Duration) time.Duration { + str, exist := c.getString(key) + if !exist { + if len(defVal) > 0 { + return defVal[0] + } + return 0 + } + + // Prefer a Go duration string, e.g. "300s", "1h30m". + if dur, err := time.ParseDuration(str); err == nil { + return dur + } + + // Backwards compatible: a bare integer is nanoseconds. + if n, err := strconv.ParseInt(str, 10, 64); err == nil { + return time.Duration(n) + } + + if len(defVal) > 0 { + return defVal[0] + } + return 0 +} + +// Float get a float64 value, if not found return default value +func Float(key string, defVal ...float64) float64 { return dc.Float(key, defVal...) } + +// Float get a float64 by key +func (c *Config) Float(key string, defVal ...float64) (value float64) { + str, ok := c.getString(key) + if !ok { + if len(defVal) > 0 { + value = defVal[0] + } + return + } + + value, err := strconv.ParseFloat(str, 64) + if err != nil { + c.addError(err) + } + return +} + +// Bool get a bool value, if not found return default value +func Bool(key string, defVal ...bool) bool { return dc.Bool(key, defVal...) } + +// Bool looks up a value for a key in this section and attempts to parse that value as a boolean, +// along with a boolean result similar to a map lookup. +// +// of following(case insensitive): +// - true +// - yes +// - false +// - no +// - 1 +// - 0 +// +// The `ok` boolean will be false in the event that the value could not be parsed as a bool +func (c *Config) Bool(key string, defVal ...bool) (value bool) { + rawVal, ok := c.getString(key) + if !ok { + if len(defVal) > 0 { + return defVal[0] + } + return + } + + lowerCase := strings.ToLower(rawVal) + switch lowerCase { + case "", "0", "false", "no": + value = false + case "1", "true", "yes": + value = true + default: + c.addErrorf("the value '%s' cannot be convert to bool", lowerCase) + } + return +} + +/************************************************************* + * read config (complex data type) + *************************************************************/ + +// Ints get config data as an int slice/array +func Ints(key string) []int { return dc.Ints(key) } + +// Ints get config data as an int slice/array +func (c *Config) Ints(key string) (arr []int) { + rawVal, ok := c.GetValue(key) + if !ok { + return + } + + switch typeData := rawVal.(type) { + case []int: + arr = typeData + case []any: + for _, v := range typeData { + iv, err := mathutil.ToInt(v) + // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) + if err != nil { + c.addError(err) + arr = arr[0:0] // reset + return + } + + arr = append(arr, iv) + } + default: + c.addErrorf("value cannot be convert to []int, key is '%s'", key) + } + return +} + +// IntMap get config data as a map[string]int +func IntMap(key string) map[string]int { return dc.IntMap(key) } + +// IntMap get config data as a map[string]int +func (c *Config) IntMap(key string) (mp map[string]int) { + rawVal, ok := c.GetValue(key) + if !ok { + return + } + + switch typeData := rawVal.(type) { + case map[string]int: // from Set + mp = typeData + case map[string]any: // decode from json,toml + mp = make(map[string]int) + for k, v := range typeData { + // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) + iv, err := mathutil.ToInt(v) + if err != nil { + c.addError(err) + mp = map[string]int{} // reset + return + } + mp[k] = iv + } + case map[any]any: // if decode from yaml + mp = make(map[string]int) + for k, v := range typeData { + // iv, err := strconv.Atoi(fmt.Sprintf( "%v", v)) + iv, err := mathutil.ToInt(v) + if err != nil { + c.addError(err) + mp = map[string]int{} // reset + return + } + + // sk := fmt.Sprintf("%v", k) + sk, _ := strutil.AnyToString(k, false) + mp[sk] = iv + } + default: + c.addErrorf("value cannot be convert to map[string]int, key is '%s'", key) + } + return +} + +// Strings get strings by key +func Strings(key string) []string { return dc.Strings(key) } + +// Strings get config data as a string slice/array +func (c *Config) Strings(key string) (arr []string) { + var ok bool + // find from cache + if c.opts.EnableCache && len(c.sArrCache) > 0 { + arr, ok = c.sArrCache[key] + if ok { + return + } + } + + rawVal, ok := c.GetValue(key) + if !ok { + return + } + + switch typeData := rawVal.(type) { + case []string: + arr = typeData + case []any: + for _, v := range typeData { + // arr = append(arr, fmt.Sprintf("%v", v)) + arr = append(arr, strutil.MustString(v)) + } + default: + c.addErrorf("value cannot be convert to []string, key is '%s'", key) + return + } + + // add cache + if c.opts.EnableCache { + if c.sArrCache == nil { + c.sArrCache = make(map[string]strArr) + } + c.sArrCache[key] = arr + } + return +} + +// StringsBySplit get []string by split a string value. +func StringsBySplit(key, sep string) []string { return dc.StringsBySplit(key, sep) } + +// StringsBySplit get []string by split a string value. +func (c *Config) StringsBySplit(key, sep string) (ss []string) { + if str, ok := c.getString(key); ok { + ss = strutil.Split(str, sep) + } + return +} + +// StringMap get config data as a map[string]string +func StringMap(key string) map[string]string { return dc.StringMap(key) } + +// StringMap get config data as a map[string]string +func (c *Config) StringMap(key string) (mp map[string]string) { + var ok bool + + // find from cache + if c.opts.EnableCache && len(c.sMapCache) > 0 { + mp, ok = c.sMapCache[key] + if ok { + return + } + } + + rawVal, ok := c.GetValue(key) + if !ok { + return + } + + switch typeData := rawVal.(type) { + case map[string]string: // from Set + mp = typeData + case map[string]any: // decode from json,toml,yaml.v3 + mp = make(map[string]string, len(typeData)) + + for k, v := range typeData { + switch tv := v.(type) { + case string: + if c.opts.ParseEnv { + mp[k] = envutil.ParseEnvValue(tv) + } else { + mp[k] = tv + } + default: + mp[k] = strutil.QuietString(v) + } + } + case map[any]any: // decode from yaml v2 + mp = make(map[string]string, len(typeData)) + + for k, v := range typeData { + sk := strutil.QuietString(k) + + switch typVal := v.(type) { + case string: + if c.opts.ParseEnv { + mp[sk] = envutil.ParseEnvValue(typVal) + } else { + mp[sk] = typVal + } + default: + mp[sk] = strutil.QuietString(v) + } + } + default: + c.addErrorf("value cannot be convert to map[string]string, key is %q", key) + return + } + + // add cache + if c.opts.EnableCache { + if c.sMapCache == nil { + c.sMapCache = make(map[string]strMap) + } + c.sMapCache[key] = mp + } + return +} + +// SubDataMap get sub config data as maputil.Map +func SubDataMap(key string) maputil.Map { return dc.SubDataMap(key) } + +// SubDataMap get sub config data as maputil.Map +// +// TIP: will not enable parse Env and more +func (c *Config) SubDataMap(key string) maputil.Map { + if mp, ok := c.GetValue(key); ok { + if mmp, ok := mp.(map[string]any); ok { + return mmp + } + } + + // keep is not nil + return maputil.Map{} +} diff --git a/read_test.go b/read_test.go new file mode 100644 index 0000000..deaf395 --- /dev/null +++ b/read_test.go @@ -0,0 +1,501 @@ +package config + +import ( + "fmt" + "testing" + "time" + + "github.com/gookit/goutil/testutil" + "github.com/gookit/goutil/testutil/assert" +) + +func TestConfig_GetValue(t *testing.T) { + is := assert.New(t) + + ClearAll() + err := LoadStrings(JSON, jsonStr) + is.Nil(err) + + c := Default() + + // error on get + _, ok := GetValue("") + is.False(ok) + + _, ok = c.GetValue("notExist") + is.False(ok) + _, ok = c.GetValue("name.sub") + is.False(ok) + is.Err(c.Error()) + + _, ok = c.GetValue("map1.key", false) + is.False(ok) + is.False(Exists("map1.key", false)) + + val, ok := GetValue("map1.notExist") + is.Nil(val) + is.False(ok) + is.False(Exists("map1.notExist")) + + val, ok = GetValue("notExist.sub") + is.False(ok) + is.Nil(val) + is.False(Exists("notExist.sub")) + + val, ok = c.GetValue("arr1.100") + is.Nil(val) + is.False(ok) + is.False(Exists("arr1.100")) + + val, ok = c.GetValue("arr1.notExist") + is.Nil(val) + is.False(ok) + is.False(Exists("arr1.notExist")) + + // load data for tests + err = c.LoadData(map[string]any{ + "setStrMap": map[string]string{ + "k": "v", + }, + "setIntMap": map[string]int{ + "k2": 23, + }, + }) + is.Nil(err) + // -- assert map[string]string + is.True(Exists("setStrMap.k")) + is.Eq("v", Get("setStrMap.k")) + is.False(Exists("setStrMap.k1")) + + // -- assert map[string]int + is.True(Exists("setIntMap.k2")) + is.Eq(23, Get("setIntMap.k2")) + is.False(Exists("setIntMap.k1")) + + ClearAll() +} + +func TestGet(t *testing.T) { + is := assert.New(t) + + ClearAll() + err := LoadStrings(JSON, jsonStr) + is.Nil(err) + + // fmt.Printf("%#v\n", Data()) + c := Default() + + is.False(c.IsEmpty()) + is.True(Exists("age")) + is.True(Exists("map1.key")) + is.True(Exists("arr1.1")) + is.False(Exists("arr1.1", false)) + is.False(Exists("not-exist.sub")) + is.False(Exists("")) + is.False(Exists("not-exist")) + + // get value + val := Get("age") + is.Eq(float64(123), val) + is.Eq("float64", fmt.Sprintf("%T", val)) + + val = Get("not-exist") + is.Nil(val) + + val = Get("name") + is.Eq("app", val) + + is.Eq([]string{"php", "go"}, StringsBySplit("tagsStr", ",")) + + // get string array + arr := Strings("notExist") + is.Empty(arr) + + arr = Strings("map1") + is.Empty(arr) + + arr = Strings("arr1") + is.Eq(`[]string{"val", "val1", "val2"}`, fmt.Sprintf("%#v", arr)) + + val = String("arr1.1") + is.Eq("val1", val) + + err = LoadStrings(JSON, `{ +"iArr": [12, 34, 36], +"iMap": {"k1": 12, "k2": 34, "k3": 36} +}`) + is.Nil(err) + + // Ints: get int arr + iarr := Ints("name") + is.False(Exists("name.1")) + is.Empty(iarr) + + iarr = Ints("notExist") + is.Empty(iarr) + + iarr = Ints("iArr") + is.Eq(`[]int{12, 34, 36}`, fmt.Sprintf("%#v", iarr)) + + iv := Int("iArr.1") + is.Eq(34, iv) + + iv = Int("iArr.100") + is.Eq(0, iv) + + // IntMap: get int map + imp := IntMap("name") + is.Empty(imp) + imp = IntMap("notExist") + is.Empty(imp) + + imp = IntMap("iMap") + is.NotEmpty(imp) + + iv = Int("iMap.k2") + is.Eq(34, iv) + is.True(Exists("iMap.k2")) + + iv = Int("iMap.notExist") + is.Eq(0, iv) + is.False(Exists("iMap.notExist")) + + // set a intMap + err = Set("intMap0", map[string]int{"a": 1, "b": 2}) + is.Nil(err) + + imp = IntMap("intMap0") + is.NotEmpty(imp) + is.Eq(1, imp["a"]) + is.Eq(2, Get("intMap0.b")) + is.True(Exists("intMap0.a")) + is.False(Exists("intMap0.c")) + + // StringMap: get string map + smp := StringMap("map1") + is.Eq("val1", smp["key1"]) + + // like load from yaml content + // c = New("test") + err = c.LoadData(map[string]any{ + "newIArr": []int{2, 3}, + "newSArr": []string{"a", "b"}, + "newIArr1": []any{12, 23}, + "newIArr2": []any{12, "abc"}, + "invalidMap": map[string]int{"k": 1}, + "yMap": map[any]any{ + "k0": "v0", + "k1": 23, + }, + "yMap1": map[any]any{ + "k": "v", + "k1": 23, + "k2": []any{23, 45}, + }, + "yMap10": map[string]any{ + "k": "v", + "k1": 23, + "k2": []any{23, 45}, + }, + "yMap2": map[any]any{ + "k": 2, + "k1": 23, + }, + "yArr": []any{23, 45, "val", map[string]any{"k4": "v4"}}, + }) + is.Nil(err) + + iarr = Ints("newIArr") + is.Eq("[2 3]", fmt.Sprintf("%v", iarr)) + + iarr = Ints("newIArr1") + is.Eq("[12 23]", fmt.Sprintf("%v", iarr)) + iarr = Ints("newIArr2") + is.Empty(iarr) + + iv = Int("newIArr.1") + is.True(Exists("newIArr.1")) + is.Eq(3, iv) + + iv = Int("newIArr.200") + is.False(Exists("newIArr.200")) + is.Eq(0, iv) + + // invalid intMap + imp = IntMap("yMap1") + is.Empty(imp) + + imp = IntMap("yMap10") + is.Empty(imp) + + imp = IntMap("yMap2") + is.Eq(2, imp["k"]) + + val = String("newSArr.1") + is.True(Exists("newSArr.1")) + is.Eq("b", val) + + val = String("newSArr.100") + is.False(Exists("newSArr.100")) + is.Eq("", val) + + smp = StringMap("invalidMap") + is.Nil(smp) + + smp = StringMap("yMap.notExist") + is.Nil(smp) + + smp = StringMap("yMap") + is.True(Exists("yMap.k0")) + is.False(Exists("yMap.k100")) + is.Eq("v0", smp["k0"]) + + iarr = Ints("yMap1.k2") + is.Eq("[23 45]", fmt.Sprintf("%v", iarr)) +} + +func TestInt(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadStrings(JSON, jsonStr) + + is.True(Exists("age")) + + iv := Int("age") + is.Eq(123, iv) + + iv = Int("name") + is.Eq(0, iv) + + iv = Int("notExist", 34) + is.Eq(34, iv) + + c := Default() + iv = c.Int("age") + is.Eq(123, iv) + iv = c.Int("notExist") + is.Eq(0, iv) + + uiv := Uint("age") + is.Eq(uint(123), uiv) + + dur := Duration("age") + is.Eq(time.Duration(123), dur) + is.Eq(time.Duration(340), Duration("not-exist", 340)) + + ClearAll() +} + +func TestInt64(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadStrings(JSON, jsonStr) + + // get int64 + iv64 := Int64("age") + is.Eq(int64(123), iv64) + + iv64 = Int64("name") + is.Eq(iv64, int64(0)) + + iv64 = Int64("age", 34) + is.Eq(int64(123), iv64) + iv64 = Int64("notExist", 34) + is.Eq(int64(34), iv64) + + c := Default() + iv64 = c.Int64("age") + is.Eq(int64(123), iv64) + iv64 = c.Int64("notExist") + is.Eq(int64(0), iv64) + + ClearAll() +} + +func TestFloat(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadStrings(JSON, jsonStr) + c := Default() + + // get float + err := c.Set("flVal", 23.45) + is.Nil(err) + flt := c.Float("flVal") + is.Eq(23.45, flt) + + flt = Float("name") + is.Eq(float64(0), flt) + + flt = c.Float("notExists") + is.Eq(float64(0), flt) + + flt = c.Float("notExists", 10) + is.Eq(float64(10), flt) + + flt = Float("flVal", 0) + is.Eq(23.45, flt) + + ClearAll() +} + +func TestString(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadStrings(JSON, jsonStr) + + // get string + val := String("arr1") + is.Eq("[val val1 val2]", val) + + str := String("notExists") + is.Eq("", str) + is.Panics(func() { + MustString("notExists") + }) + + str = String("notExists", "defVal") + is.Eq("defVal", str) + + c := Default() + str = c.String("name") + is.Eq("app", str) + str = c.String("notExist") + is.Eq("", str) + + ClearAll() +} + +func TestBool(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadSources(JSON, []byte(jsonStr)) + + // get bool + val := Get("debug") + is.Eq(true, val) + + bv := Bool("debug") + is.Eq(true, bv) + + bv = Bool("age") + is.Eq(false, bv) + + bv = Bool("debug", false) + is.Eq(true, bv) + + bv = Bool("notExist", false) + is.Eq(false, bv) + + c := Default() + bv = c.Bool("debug") + is.True(bv) + bv = c.Bool("notExist") + is.False(bv) + + ClearAll() +} + +func TestSubDataMap(t *testing.T) { + is := assert.New(t) + ClearAll() + _ = LoadSources(JSON, []byte(jsonStr)) + + mp := SubDataMap("map1") + is.NotEmpty(mp) + is.Eq("230", mp.Get("key4")) + is.Eq(230, mp.Int("key4")) + + mp = SubDataMap("notExist") + is.Empty(mp) + + ClearAll() +} + +func TestParseEnv(t *testing.T) { + is := assert.New(t) + + cfg := NewWithOptions("test", ParseEnv) + err := cfg.LoadStrings(JSON, `{ +"ekey": "${EnvKey}", +"ekey0": "${ EnvKey0 }", +"ekey1": "${EnvKey1|defValue}", +"ekey2": "${ EnvKey2 | defValue1 }", +"ekey3": "${ EnvKey3 | app:run }", +"ekey4": "${FirstEnv}/${ SecondEnv }", +"ekey5": "${TEST_SHELL|/bin/bash}", +"ekey6": "${ EnvKey6 | app=run }", +"ekey7": "${ EnvKey7 | app.run }", +"ekey8": "${ EnvKey8 | app/run }" +}`) + + is.NoErr(err) + + tests := []struct{ EKey, EVal, CKey, CVal string }{ + {"EnvKey", "EnvKey val", "ekey", "EnvKey val"}, + {"EnvKey", "", "ekey", ""}, + {"EnvKey0", "EnvKey0 val", "ekey0", "EnvKey0 val"}, + {"EnvKey3", "EnvKey3 val", "ekey3", "EnvKey3 val"}, + {"EnvKey3", "", "ekey3", "app:run"}, + {"EnvKey6", "", "ekey6", "app=run"}, + {"EnvKey7", "", "ekey7", "app.run"}, + {"EnvKey8", "", "ekey8", "app/run"}, + {"TEST_SHELL", "/bin/zsh", "ekey5", "/bin/zsh"}, + {"TEST_SHELL", "", "ekey5", "/bin/bash"}, + } + + for _, smp := range tests { + is.Eq("", Getenv(smp.EKey)) + + testutil.MockEnvValue(smp.EKey, smp.EVal, func(eVal string) { + is.Eq(smp.EVal, eVal) + is.Eq(smp.CVal, cfg.String(smp.CKey)) + }) + } + + // test multi ENV key + is.Eq("", Getenv("FirstEnv")) + + testutil.MockEnvValues(map[string]string{ + "FirstEnv": "abc", + "SecondEnv": "def", + }, func() { + is.Eq("abc", Getenv("FirstEnv")) + is.Eq("def", Getenv("SecondEnv")) + is.Eq("abc/def", cfg.String("ekey4")) + }) + + testutil.MockEnvValues(map[string]string{ + "FirstEnv": "abc", + }, func() { + is.Eq("abc", Getenv("FirstEnv")) + is.Eq("", Getenv("SecondEnv")) + is.Eq("abc/", cfg.String("ekey4")) + }) +} + +func TestConfig_Duration_getter(t *testing.T) { + is := assert.New(t) + c := New("test") + err := c.LoadStrings(JSON, `{ + "ttl": "300s", + "idle": "1h30m", + "short": "20m", + "nanos": 500, + "bad": "not-a-duration" + }`) + is.Nil(err) + + // Go duration strings are parsed. + is.Eq(300*time.Second, c.Duration("ttl")) + is.Eq(90*time.Minute, c.Duration("idle")) + is.Eq(20*time.Minute, c.Duration("short")) + + // A bare integer stays nanoseconds (backwards compatible). + is.Eq(time.Duration(500), c.Duration("nanos")) + + // Unparseable and missing fall back to the default. + is.Eq(5*time.Minute, c.Duration("bad", 5*time.Minute)) + is.Eq(7*time.Second, c.Duration("missing", 7*time.Second)) + is.Eq(time.Duration(0), c.Duration("missing")) +} diff --git a/testdata/issues59.ini b/testdata/issues59.ini new file mode 100644 index 0000000..afab97f --- /dev/null +++ b/testdata/issues59.ini @@ -0,0 +1,16 @@ +; exported at 2025-08-20 14:11:56 + +age = 123 +baseKey = value +debug = false +envKey = ${SHELL} +envKey1 = ${NotExist|defValue} +multiWords = hello world +name = app + +[map1] +key1 = val1 +key2 = val2 +key = val + +