Initial QSfera import
This commit is contained in:
+524
@@ -0,0 +1,524 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/gookit/goutil/errorx"
|
||||
"github.com/gookit/goutil/fsutil"
|
||||
)
|
||||
|
||||
// LoadFiles load one or multi files, will fire OnLoadData event
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadFiles(file1, file2, ...)
|
||||
func LoadFiles(sourceFiles ...string) error { return dc.LoadFiles(sourceFiles...) }
|
||||
|
||||
// LoadFiles load and parse config files, will fire OnLoadData event
|
||||
func (c *Config) LoadFiles(sourceFiles ...string) (err error) {
|
||||
for _, file := range sourceFiles {
|
||||
if err = c.loadFile(file, false, ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExists load one or multi files, will ignore not exist
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadExists(file1, file2, ...)
|
||||
func LoadExists(sourceFiles ...string) error { return dc.LoadExists(sourceFiles...) }
|
||||
|
||||
// LoadExists load and parse config files, but will ignore not exists file.
|
||||
func (c *Config) LoadExists(sourceFiles ...string) (err error) {
|
||||
for _, file := range sourceFiles {
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = c.loadFile(file, true, ""); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadRemote load config data from remote URL.
|
||||
func LoadRemote(format, url string) error { return dc.LoadRemote(format, url) }
|
||||
|
||||
// LoadRemote load config data from remote URL.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// c.LoadRemote(config.JSON, "http://abc.com/api-config.json")
|
||||
func (c *Config) LoadRemote(format, url string) (err error) {
|
||||
// create http client
|
||||
client := http.Client{Timeout: 300 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//noinspection GoUnhandledErrorResult
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("fetch remote config error, reply status code is %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// read response content
|
||||
bts, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
if err = c.parseSourceCode(format, bts); err != nil {
|
||||
return
|
||||
}
|
||||
c.loadedUrls = append(c.loadedUrls, url)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadOSEnv load data from OS ENV
|
||||
//
|
||||
// Deprecated: please use LoadOSEnvs()
|
||||
func LoadOSEnv(keys []string, keyToLower bool) { dc.LoadOSEnv(keys, keyToLower) }
|
||||
|
||||
// LoadOSEnv load data from os ENV
|
||||
//
|
||||
// Deprecated: please use Config.LoadOSEnvs()
|
||||
func (c *Config) LoadOSEnv(keys []string, keyToLower bool) {
|
||||
for _, key := range keys {
|
||||
// NOTICE: if is Windows os, os.Getenv() Key is not case-sensitive
|
||||
val := os.Getenv(key)
|
||||
if keyToLower {
|
||||
key = strings.ToLower(key)
|
||||
}
|
||||
_ = c.Set(key, val)
|
||||
}
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// LoadOSEnvs load data from OS ENVs. see Config.LoadOSEnvs
|
||||
func LoadOSEnvs(nameToKeyMap map[string]string) { dc.LoadOSEnvs(nameToKeyMap) }
|
||||
|
||||
// LoadOSEnvs load data from os ENVs. format: `{ENV_NAME: config_key}`
|
||||
//
|
||||
// - `config_key` allow use key path. eg: `{"DB_USERNAME": "db.username"}`
|
||||
func (c *Config) LoadOSEnvs(nameToKeyMap map[string]string) {
|
||||
for name, cfgKey := range nameToKeyMap {
|
||||
if val := os.Getenv(name); val != "" {
|
||||
if cfgKey == "" {
|
||||
cfgKey = strings.ToLower(name)
|
||||
}
|
||||
_ = c.Set(cfgKey, val)
|
||||
}
|
||||
}
|
||||
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// support bound types for CLI flags vars
|
||||
var validTypes = map[string]int{
|
||||
"int": 1,
|
||||
"uint": 1,
|
||||
"bool": 1,
|
||||
// string is default
|
||||
"string": 1,
|
||||
}
|
||||
|
||||
// LoadFlags load data from cli flags. see Config.LoadFlags
|
||||
func LoadFlags(defines []string) error { return dc.LoadFlags(defines) }
|
||||
|
||||
// LoadFlags parse command line arguments, based on provide keys.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// // 'debug' flag is bool type
|
||||
// c.LoadFlags([]string{"env", "debug:bool"})
|
||||
// // can with flag desc message
|
||||
// c.LoadFlags([]string{"env:set the run env"})
|
||||
// c.LoadFlags([]string{"debug:bool:set debug mode"})
|
||||
// // can set value to map key. eg: myapp --map1.sub-key=val
|
||||
// c.LoadFlags([]string{"--map1.sub-key"})
|
||||
func (c *Config) LoadFlags(defines []string) (err error) {
|
||||
hash := map[string]int8{}
|
||||
|
||||
// bind vars
|
||||
for _, str := range defines {
|
||||
key, typ, desc := parseVarNameAndType(str)
|
||||
if desc == "" {
|
||||
desc = "config flag " + key
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "int":
|
||||
ptr := new(int)
|
||||
flag.IntVar(ptr, key, c.Int(key), desc)
|
||||
hash[key] = 0
|
||||
case "uint":
|
||||
ptr := new(uint)
|
||||
flag.UintVar(ptr, key, c.Uint(key), desc)
|
||||
hash[key] = 0
|
||||
case "bool":
|
||||
ptr := new(bool)
|
||||
flag.BoolVar(ptr, key, c.Bool(key), desc)
|
||||
hash[key] = 0
|
||||
default: // as string
|
||||
ptr := new(string)
|
||||
flag.StringVar(ptr, key, c.String(key), desc)
|
||||
hash[key] = 0
|
||||
}
|
||||
}
|
||||
|
||||
// parse and collect
|
||||
flag.Parse()
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
name := f.Name
|
||||
// only get name in the keys.
|
||||
if _, ok := hash[name]; !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// if f.Value implement the flag.Getter, read typed value
|
||||
if gtr, ok := f.Value.(flag.Getter); ok {
|
||||
_ = c.Set(name, gtr.Get())
|
||||
// } else { // TIP: basic type flag always implements Getter interface
|
||||
// _ = c.Set(name, f.Value.String()) // ignore error
|
||||
}
|
||||
})
|
||||
|
||||
c.fireHook(OnLoadData)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadData load one or multi data
|
||||
func LoadData(dataSource ...any) error { return dc.LoadData(dataSource...) }
|
||||
|
||||
// LoadData load data from map OR struct
|
||||
//
|
||||
// The dataSources type allow:
|
||||
// - map[string]any
|
||||
// - map[string]string
|
||||
func (c *Config) LoadData(dataSources ...any) (err error) {
|
||||
if c.opts.Delimiter == 0 {
|
||||
c.opts.Delimiter = defaultDelimiter
|
||||
}
|
||||
|
||||
var loaded bool
|
||||
for _, ds := range dataSources {
|
||||
if smp, ok := ds.(map[string]string); ok {
|
||||
loaded = true
|
||||
c.LoadSMap(smp)
|
||||
continue
|
||||
}
|
||||
|
||||
err = mergo.Merge(&c.data, ds, c.opts.MergeOptions...)
|
||||
if err != nil {
|
||||
return errorx.WithStack(err)
|
||||
}
|
||||
loaded = true
|
||||
}
|
||||
|
||||
if loaded {
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadSMap to config
|
||||
func (c *Config) LoadSMap(smp map[string]string) {
|
||||
for k, v := range smp {
|
||||
c.data[k] = v
|
||||
}
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
|
||||
// LoadSources load one or multi byte data
|
||||
func LoadSources(format string, src []byte, more ...[]byte) error {
|
||||
return dc.LoadSources(format, src, more...)
|
||||
}
|
||||
|
||||
// LoadSources load data from byte content.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// config.LoadSources(config.Yaml, []byte(`
|
||||
// name: blog
|
||||
// arr:
|
||||
// key: val
|
||||
//
|
||||
// `))
|
||||
func (c *Config) LoadSources(format string, src []byte, more ...[]byte) (err error) {
|
||||
err = c.parseSourceCode(format, src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, sc := range more {
|
||||
err = c.parseSourceCode(format, sc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadStrings load one or multi string
|
||||
func LoadStrings(format string, str string, more ...string) error {
|
||||
return dc.LoadStrings(format, str, more...)
|
||||
}
|
||||
|
||||
// LoadStrings load data from source string content.
|
||||
func (c *Config) LoadStrings(format string, str string, more ...string) (err error) {
|
||||
err = c.parseSourceCode(format, []byte(str))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range more {
|
||||
err = c.parseSourceCode(format, []byte(s))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadFilesByFormat load one or multi config files by give format, will fire OnLoadData event
|
||||
func LoadFilesByFormat(format string, configFiles ...string) error {
|
||||
return dc.LoadFilesByFormat(format, configFiles...)
|
||||
}
|
||||
|
||||
// LoadFilesByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func (c *Config) LoadFilesByFormat(format string, configFiles ...string) (err error) {
|
||||
for _, file := range configFiles {
|
||||
if err = c.loadFile(file, false, format); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func LoadExistsByFormat(format string, configFiles ...string) error {
|
||||
return dc.LoadExistsByFormat(format, configFiles...)
|
||||
}
|
||||
|
||||
// LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event
|
||||
func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err error) {
|
||||
for _, file := range configFiles {
|
||||
if err = c.loadFile(file, true, format); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
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:
|
||||
//
|
||||
// // file: /somedir/task.json
|
||||
// LoadFromDir("/somedir", "json")
|
||||
//
|
||||
// // after load
|
||||
// Config.data = map[string]any{"task": file data}
|
||||
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, loFns ...LoadOptFn) (err error) {
|
||||
extName := "." + format
|
||||
extLen := len(extName)
|
||||
|
||||
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]
|
||||
if lo.DataKey != "" {
|
||||
dataList = append(dataList, data)
|
||||
} else {
|
||||
dirData[onlyName] = data
|
||||
}
|
||||
|
||||
// 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
|
||||
func ReloadFiles() error { return dc.ReloadFiles() }
|
||||
|
||||
// ReloadFiles reload config data use loaded files. use on watching loaded files change
|
||||
func (c *Config) ReloadFiles() (err error) {
|
||||
files := c.loadedFiles
|
||||
if len(files) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data := c.Data()
|
||||
c.reloading = true
|
||||
c.ClearCaches()
|
||||
|
||||
defer func() {
|
||||
// revert to back up data on error
|
||||
if err != nil {
|
||||
c.data = data
|
||||
}
|
||||
|
||||
c.lock.Unlock()
|
||||
c.reloading = false
|
||||
|
||||
if err == nil {
|
||||
c.fireHook(OnReloadData)
|
||||
}
|
||||
}()
|
||||
|
||||
// with lock
|
||||
c.lock.Lock()
|
||||
|
||||
// reload config files
|
||||
return c.LoadFiles(files...)
|
||||
}
|
||||
|
||||
// load config file, will fire OnLoadData event
|
||||
func (c *Config) loadFile(file string, loadExist bool, format string) (err error) {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
// skip not exist file
|
||||
if os.IsNotExist(err) && loadExist {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
//noinspection GoUnhandledErrorResult
|
||||
defer fd.Close()
|
||||
|
||||
// read file content
|
||||
bts, err := io.ReadAll(fd)
|
||||
if err == nil {
|
||||
// get format for file ext
|
||||
if format == "" {
|
||||
format = strings.Trim(filepath.Ext(file), ".")
|
||||
}
|
||||
|
||||
// parse file content
|
||||
if err = c.parseSourceCode(format, bts); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !c.reloading {
|
||||
c.loadedFiles = append(c.loadedFiles, file)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// parse config source code to Config.
|
||||
func (c *Config) parseSourceCode(format string, blob []byte) (err error) {
|
||||
data, err := c.parseSourceToMap(format, blob)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.loadDataMap(data)
|
||||
}
|
||||
|
||||
func (c *Config) loadDataMap(data map[string]any) (err error) {
|
||||
// first: init config data
|
||||
if len(c.data) == 0 {
|
||||
c.data = data
|
||||
} else {
|
||||
// again ... will merge data
|
||||
err = mergo.Merge(&c.data, data, c.opts.MergeOptions...)
|
||||
}
|
||||
|
||||
if !c.reloading && err == nil {
|
||||
c.fireHook(OnLoadData)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// parse config source code to Config.
|
||||
func (c *Config) parseSourceToMap(format string, blob []byte) (map[string]any, error) {
|
||||
format = c.resolveFormat(format)
|
||||
decode := c.decoders[format]
|
||||
if decode == nil {
|
||||
return nil, errors.New("not register decoder for the format: " + format)
|
||||
}
|
||||
|
||||
if c.opts.Delimiter == 0 {
|
||||
c.opts.Delimiter = defaultDelimiter
|
||||
}
|
||||
|
||||
// decode content to data
|
||||
data := make(map[string]any)
|
||||
|
||||
if err := decode(blob, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user