Skip to content

Commit

Permalink
✨ feat: LoadFromDir support new option: DataKey. see issues #173
Browse files Browse the repository at this point in the history
- if set DataKey, will load all dir files data as Slice data
  • Loading branch information
inhere committed Jan 12, 2024
1 parent b624f19 commit 1e610d7
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 28 deletions.
1 change: 0 additions & 1 deletion issues_test.go
Expand Up @@ -604,5 +604,4 @@ func TestIssues_178(t *testing.T) {
err := config.Decode(cfg)
assert.NoErr(t, err)
dump.Println(cfg)

}
67 changes: 51 additions & 16 deletions load.go
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
//
// `))
Expand Down Expand Up @@ -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:
Expand All @@ -322,43 +338,62 @@ 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))
if err != nil {
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
Expand Down
23 changes: 18 additions & 5 deletions load_test.go
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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) {
Expand Down
17 changes: 11 additions & 6 deletions options.go
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down

0 comments on commit 1e610d7

Please sign in to comment.