From 1e610d75dba60184dd53f686bd565ecf586c8b80 Mon Sep 17 00:00:00 2001 From: Inhere Date: Fri, 12 Jan 2024 15:19:53 +0800 Subject: [PATCH] :sparkles: feat: LoadFromDir support new option: DataKey. see issues #173 - if set DataKey, will load all dir files data as Slice data --- issues_test.go | 1 - load.go | 67 ++++++++++++++++++++++++++++++++++++++------------ load_test.go | 23 +++++++++++++---- options.go | 17 ++++++++----- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/issues_test.go b/issues_test.go index fc0da3a..82325df 100644 --- a/issues_test.go +++ b/issues_test.go @@ -604,5 +604,4 @@ func TestIssues_178(t *testing.T) { err := config.Decode(cfg) assert.NoErr(t, err) dump.Println(cfg) - } diff --git a/load.go b/load.go index 740aa2c..0c0de0a 100644 --- a/load.go +++ b/load.go @@ -103,10 +103,8 @@ func (c *Config) LoadOSEnv(keys []string, keyToLower bool) { if keyToLower { key = strings.ToLower(key) } - _ = c.Set(key, val) } - c.fireHook(OnLoadData) } @@ -242,8 +240,8 @@ func LoadSources(format string, src []byte, more ...[]byte) error { // Usage: // // config.LoadSources(config.Yaml, []byte(` -// name: blog -// arr: +// name: blog +// arr: // key: val // // `)) @@ -313,6 +311,24 @@ func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err e return } +// LoadOptions for load config from dir. +type LoadOptions struct { + // DataKey use for load config from dir. + // see https://github.com/gookit/config/issues/173 + DataKey string +} + +// LoadOptFn type func +type LoadOptFn func(lo *LoadOptions) + +func newLoadOptions(loFns []LoadOptFn) *LoadOptions { + lo := &LoadOptions{} + for _, fn := range loFns { + fn(lo) + } + return lo +} + // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. // // Example: @@ -322,24 +338,30 @@ func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err e // // // after load // Config.data = map[string]any{"task": file data} -func LoadFromDir(dirPath, format string) error { - return dc.LoadFromDir(dirPath, format) +func LoadFromDir(dirPath, format string, loFns ...LoadOptFn) error { + return dc.LoadFromDir(dirPath, format, loFns...) } // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. // +// NOTE: will not be reloaded on call ReloadFiles(), if data loaded by the method. +// // Example: // // // file: /somedir/task.json , will use filename 'task' as key // Config.LoadFromDir("/somedir", "json") // // // after load, the data will be: -// Config.data = map[string]any{"task": file data} -func (c *Config) LoadFromDir(dirPath, format string) (err error) { +// Config.data = map[string]any{"task": {file data}} +func (c *Config) LoadFromDir(dirPath, format string, loFns ...LoadOptFn) (err error) { extName := "." + format extLen := len(extName) - return fsutil.FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error { + lo := newLoadOptions(loFns) + dirData := make(map[string]any) + dataList := make([]map[string]any, 0, 8) + + err = fsutil.FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error { baseName := ent.Name() if strings.HasSuffix(baseName, extName) { data, err := c.parseSourceToMap(format, fsutil.MustReadFile(fPath)) @@ -347,18 +369,31 @@ func (c *Config) LoadFromDir(dirPath, format string) (err error) { return err } + // filename without ext. onlyName := baseName[:len(baseName)-extLen] - err = c.loadDataMap(map[string]any{onlyName: data}) - - // use file name as key, it cannot be reloaded. SO, cannot append to loadedFiles - // if err == nil { - // c.loadedFiles = append(c.loadedFiles, fPath) - // } + if lo.DataKey != "" { + dataList = append(dataList, data) + } else { + dirData[onlyName] = data + } - return err + // TODO use file name as key, it cannot be reloaded. So, cannot append to loadedFiles + // c.loadedFiles = append(c.loadedFiles, fPath) } return nil }) + + if err != nil { + return err + } + if lo.DataKey != "" { + dirData[lo.DataKey] = dataList + } + + if len(dirData) == 0 { + return nil + } + return c.loadDataMap(dirData) } // ReloadFiles reload config data use loaded files diff --git a/load_test.go b/load_test.go index 7f788e6..941437b 100644 --- a/load_test.go +++ b/load_test.go @@ -2,9 +2,11 @@ package config import ( "os" + "reflect" "runtime" "testing" + "github.com/gookit/goutil/dump" "github.com/gookit/goutil/testutil" "github.com/gookit/goutil/testutil/assert" ) @@ -246,16 +248,27 @@ func TestLoadOSEnvs(t *testing.T) { func TestLoadFromDir(t *testing.T) { ClearAll() - assert.NoErr(t, LoadFiles("testdata/json_base.json")) - - err := LoadFromDir("testdata/subdir", JSON) - assert.NoErr(t, err) - // dump.P(Data()) + assert.NoErr(t, LoadStrings(JSON, `{ +"topKey": "a value" +}`)) + assert.NoErr(t, LoadFromDir("testdata/subdir", JSON)) + dump.P(Data()) assert.Eq(t, "value in sub data", Get("subdata.key01")) assert.Eq(t, "value in task.json", Get("task.key01")) ClearAll() + assert.NoErr(t, LoadFromDir("testdata/emptydir", JSON)) + + // with DataKey option. see https://github.com/gookit/config/issues/173 + assert.NoErr(t, LoadFromDir("testdata/subdir", JSON, func(lo *LoadOptions) { + lo.DataKey = "dataList" + })) + dump.P(Data()) + dl := Get("dataList") + assert.NotNil(t, dl) + assert.IsKind(t, reflect.Slice, dl) + ClearAll() } func TestReloadFiles(t *testing.T) { diff --git a/options.go b/options.go index 595468f..c1e32d3 100644 --- a/options.go +++ b/options.go @@ -20,20 +20,22 @@ type HookFunc func(event string, c *Config) // Options config options type Options struct { - // ParseEnv parse env value. like: "${EnvName}" "${EnvName|default}" + // ParseEnv parse env in string value and default value. like: "${EnvName}" "${EnvName|default}" ParseEnv bool // ParseTime parses a duration string to time.Duration // eg: 10s, 2m ParseTime bool // Readonly config is readonly Readonly bool + // ParseDefault tag on binding data to struct. tag: default + ParseDefault bool // EnableCache enable config data cache EnableCache bool // ParseKey parse key path, allow find value by key path. eg: 'key.sub' will find `map[key]sub` ParseKey bool // TagName tag name for binding data to struct // - // Tips: please set tag name by DecoderConfig + // Deprecated: please set tag name by DecoderConfig, or use SetTagName() TagName string // Delimiter the delimiter char for split key path, if `FindByPath=true`. default is '.' Delimiter byte @@ -45,8 +47,6 @@ type Options struct { DecoderConfig *mapstructure.DecoderConfig // HookFunc on data changed. you can do something... HookFunc HookFunc - // ParseDefault tag on binding data to struct. tag: default - ParseDefault bool // WatchChange bool } @@ -79,6 +79,12 @@ func newDefaultDecoderConfig(tagName string) *mapstructure.DecoderConfig { } } +// SetTagName for mapping data to struct +func (o *Options) SetTagName(tagName string) { + o.TagName = tagName + o.DecoderConfig.TagName = tagName +} + func (o *Options) shouldAddHookFunc() bool { return o.ParseTime || o.ParseEnv } @@ -113,8 +119,7 @@ func (o *Options) makeDecoderConfig() *mapstructure.DecoderConfig { // WithTagName set tag name for export to struct func WithTagName(tagName string) func(*Options) { return func(opts *Options) { - opts.TagName = tagName - opts.DecoderConfig.TagName = tagName + opts.SetTagName(tagName) } }