Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
*.log
*.swp
.idea
*.patch
### Go template
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.DS_Store
vendor
#go.sum
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2016 inhere
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+559
View File
@@ -0,0 +1,559 @@
# Config
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
[![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config)
[![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions)
[![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config)
[![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](https://pkg.go.dev/github.com/gookit/config/v2)
`config` - Simple, full-featured Go application configuration management tool library.
> **[中文说明](README.zh-CN.md)**
## Features
- Support multi format: `JSON`(default), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags`
- `JSON` content support comments. will auto clear comments
- Other drivers are used on demand, not used will not be loaded into the application.
- Possibility to add custom driver for your specific format
- Support multi-file and multi-data loading
- Support for loading configuration from system ENV
- Support for loading configuration data from remote URLs
- Support for setting configuration data from command line(`flags`)
- Support listen and fire events on config data changed.
- allow events: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data`
- Support data overlay and merge, automatically load by key when loading multiple copies of data
- Support for binding all or part of the configuration data to the structure
- Support init default value by struct tag `default:"def_value"`
- Support init default value from ENV `default:"${APP_ENV | dev}"`
- Support get sub value by key-path, like `map.key` `arr.2`
- Support parse ENV name and allow with default value. like `envKey: ${SHELL|/bin/bash}` -> `envKey: /bin/zsh`
- Generic API: `Get` `Int` `Uint` `Int64` `Float` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ...
- Complete unit test(code coverage > 95%)
## Only use INI
If you just want to use INI for simple config management, recommended use [gookit/ini](https://github.com/gookit/ini)
### Load dotenv file
On `gookit/ini`: Provide a sub-package `dotenv` that supports importing data from files (eg `.env`) to ENV
```shell
go get github.com/gookit/ini/v2/dotenv
```
## GoDoc
- [godoc for github](https://pkg.go.dev/github.com/gookit/config)
## Install
```bash
go get github.com/gookit/config/v2
```
## Usage
Here using the yaml format as an example(`testdata/yml_other.yml`):
```yaml
name: app2
debug: false
baseKey: value2
shell: ${SHELL}
envKey1: ${NotExist|defValue}
map1:
key: val2
key2: val20
arr1:
- val1
- val21
```
### Load data
> examples code please see [_examples/yaml.go](_examples/yaml.go):
```go
package main
import (
"github.com/gookit/config/v2"
"github.com/gookit/config/v2/yaml"
)
// go run ./examples/yaml.go
func main() {
// config.ParseEnv: will parse env var in string value. eg: shell: ${SHELL}
config.WithOptions(config.ParseEnv)
// add driver for support yaml content
config.AddDriver(yaml.Driver)
err := config.LoadFiles("testdata/yml_base.yml")
if err != nil {
panic(err)
}
// load more files
err = config.LoadFiles("testdata/yml_other.yml")
// can also load multi at once
// err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml")
if err != nil {
panic(err)
}
// fmt.Printf("config data: \n %#v\n", config.Data())
}
```
**Usage tips**:
- More extra options can be added using `WithOptions()`. For example: `ParseEnv`, `ParseDefault`
- You can use `AddDriver()` to add the required format driver (`json` is loaded by default, no need to add)
- The configuration data can then be loaded using `LoadFiles()` `LoadStrings()` etc.
- You can pass in multiple files or call multiple times
- Data loaded multiple times will be automatically merged by key
## Bind Structure
> Note: The default binding mapping tag of a structure is `mapstructure`, which can be changed by setting the decoder's option `options.DecoderConfig.TagName`
```go
type User struct {
Age int `mapstructure:"age"`
Key string `mapstructure:"key"`
UserName string `mapstructure:"user_name"`
Tags []int `mapstructure:"tags"`
}
user := User{}
err = config.BindStruct("user", &user)
fmt.Println(user.UserName) // inhere
```
**Change struct tag name**
```go
config.WithOptions(func(opt *Options) {
options.DecoderConfig.TagName = "config"
})
// use custom tag name.
type User struct {
Age int `config:"age"`
Key string `config:"key"`
UserName string `config:"user_name"`
Tags []int `config:"tags"`
}
user := User{}
err = config.Decode(&user)
```
**Can bind all config data to a struct**:
```go
config.Decode(&myConf)
// can also
config.BindStruct("", &myConf)
```
> `config.MapOnExists` like `BindStruct`but map binding only if key exists
### Direct read data
- Get integer
```go
age := config.Int("age")
fmt.Print(age) // 100
```
- Get bool
```go
val := config.Bool("debug")
fmt.Print(val) // true
```
- Get string
```go
name := config.String("name")
fmt.Print(name) // inhere
```
- Get strings(slice)
```go
arr1 := config.Strings("arr1")
fmt.Printf("%#v", arr1) // []string{"val1", "val21"}
```
- Get string map
```go
val := config.StringMap("map1")
fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"}
```
- Value contains ENV var
```go
value := config.String("shell")
fmt.Print(value) // "/bin/zsh"
```
- Get value by key path
```go
// from array
value := config.String("arr1.0")
fmt.Print(value) // "val1"
// from map
value := config.String("map1.key")
fmt.Print(value) // "val2"
```
- Setting new value
```go
// set value
config.Set("name", "new name")
name = config.String("name")
fmt.Print(name) // "new name"
```
## Load from ENV
Support load ENV vars to config data.
- Support set value to sub key in map.
- eg: `{"DB_USERNAME": "db.username"}` value will set to `username` in `db`
```go
// os env: APP_NAME=config APP_DEBUG=true DB_USERNAME=someone
// load ENV info
config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug", "DB_USERNAME": "db.username"})
// read
config.Bool("app_debug") // true
config.String("app_name") // "config"
```
## Load from flags
Support simple CLI flags parameter parsing, load to config data.
- define format: `name:type:desc` OR `name:type` OR `name:desc` (type, desc is optional)
- `type` can set `flag` type. allow: `bool`, `int`, `string`(default)
- `desc` can set `flag` description
- `name` can be in key path format.
- eg: `db.username`, input: `--db.username=someone` values will be mapped to `username` of the `db` configuration
```go
// 'debug' flag is bool type
config.LoadFlags([]string{"env", "debug:bool"})
// can with flag desc message
config.LoadFlags([]string{"env:set the run env"})
config.LoadFlags([]string{"debug:bool:set debug mode"})
// can set value to map key. eg: myapp --map1.sub-key=val
config.LoadFlags([]string{"map1.sub-key"})
```
Examples:
```go
// flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val
// load flag info
keys := []string{
"name",
"env:set the run env",
"age:int",
"debug:bool:set debug mode",
"map1.sub-key",
}
err := config.LoadFlags(keys)
// read
config.String("name") // "inhere"
config.String("env") // "dev"
config.Int("age") // 99
config.Bool("debug") // true
config.Get("map1") // map[string]any{"sub-key":"val"}
```
## New config instance
You can create custom config instance
```go
// create new instance, will auto register JSON driver
myConf := config.New("my-conf")
// create empty instance
myConf := config.NewEmpty("my-conf")
// create and with some options
myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly)
```
## Listen config change
Now, you can add a hook func for listen config data change. then, you can do something like: write data to file
**Add hook func on create config**:
```go
hookFn := func(event string, c *Config) {
fmt.Println("fire the:", event)
}
c := NewWithOptions("test", config.WithHookFunc(hookFn))
// for global config
config.WithOptions(config.WithHookFunc(hookFn))
```
After that, when calling `LoadXXX, Set, SetData, ClearData` methods, it will output:
```text
fire the: load.data
fire the: set.value
fire the: set.data
fire the: clean.data
```
### Watch loaded config files
To listen for changes to loaded config files, and reload the config when it changes, you need to use the https://github.com/fsnotify/fsnotify library.
For usage, please refer to the example [./_example/watch_file.go](_examples/watch_file.go)
Also, you need to listen to the `reload.data` event:
```go
config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) {
if event == config.OnReloadData {
fmt.Println("config reloaded, you can do something ....")
}
}))
```
When the configuration changes, you can do related things, for example: rebind the configuration to your struct.
## Dump config data
> Can use `config.DumpTo()` export the configuration data to the specified `writer`, such as: buffer,file
**Dump to JSON file**
```go
buf := new(bytes.Buffer)
_, err := config.DumpTo(buf, config.JSON)
ioutil.WriteFile("my-config.json", buf.Bytes(), 0755)
```
**Dump pretty JSON**
You can set the default var `JSONMarshalIndent` or custom a new JSON driver.
```go
config.JSONMarshalIndent = " "
```
**Dump to YAML file**
```go
_, err := config.DumpTo(buf, config.YAML)
ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755)
```
## Available options
```go
// Options config options
type Options struct {
// parse env in string value. like: "${EnvName}" "${EnvName|default}"
ParseEnv bool
// ParseTime parses a duration string to time.Duration
// eg: 10s, 2m
ParseTime bool
// config is readonly. default is False
Readonly bool
// enable config data cache. default is False
EnableCache bool
// parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub`
ParseKey bool
// the delimiter char for split key path, if `FindByPath=true`. default is '.'
Delimiter byte
// default write format
DumpFormat string
// default input format
ReadFormat string
// DecoderConfig setting for binding data to struct
DecoderConfig *mapstructure.DecoderConfig
// HookFunc on data changed.
HookFunc HookFunc
// ParseDefault tag on binding data to struct. tag: default
ParseDefault bool
}
```
> **TIP**: please visit https://pkg.go.dev/github.com/gookit/config/v2#Options to see the latest options information
Examples for set options:
```go
config.WithOptions(config.WithTagName("mytag"))
config.WithOptions(func(opt *Options) {
opt.SetTagNames("config")
})
```
### Options: Parse default
Support parse default value by struct tag `default`, and support parse fields in sub struct.
> **NOTE**⚠️ If you want to parse a sub-struct, you need to set the `default:""` flag on the parent struct,
> otherwise the fields of the sub-struct will not be resolved.
```go
// add option: config.ParseDefault
c := config.New("test").WithOptions(config.ParseDefault)
// only set name
c.SetData(map[string]any{
"name": "inhere",
})
// age load from default tag
type User struct {
Age int `default:"30"`
Name string
Tags []int
}
user := &User{}
goutil.MustOk(c.Decode(user))
dump.Println(user)
```
**Output**:
```shell
&config_test.User {
Age: int(30),
Name: string("inhere"), #len=6
Tags: []int [ #len=0
],
},
```
## API Methods Refer
### Load Config
- `LoadOSEnvs(nameToKeyMap map[string]string)` Load data from os ENV
- `LoadData(dataSource ...any) (err error)` Load from struts or maps
- `LoadFlags(keys []string) (err error)` Load from CLI flags
- `LoadExists(sourceFiles ...string) (err error)`
- `LoadFiles(sourceFiles ...string) (err error)`
- `LoadFromDir(dirPath, format string) (err error)` Load custom format files from the given directory, the file name will be used as the key
- `LoadRemote(format, url string) (err error)`
- `LoadSources(format string, src []byte, more ...[]byte) (err error)`
- `LoadStrings(format string, str string, more ...string) (err error)`
- `LoadFilesByFormat(format string, sourceFiles ...string) (err error)`
- `LoadExistsByFormat(format string, sourceFiles ...string) error`
### Getting Values
- `Bool(key string, defVal ...bool) bool`
- `Int(key string, defVal ...int) int`
- `Uint(key string, defVal ...uint) uint`
- `Int64(key string, defVal ...int64) int64`
- `Ints(key string) (arr []int)`
- `IntMap(key string) (mp map[string]int)`
- `Float(key string, defVal ...float64) float64`
- `String(key string, defVal ...string) string`
- `Strings(key string) (arr []string)`
- `SubDataMap(key string) maputi.Data`
- `StringMap(key string) (mp map[string]string)`
- `Get(key string, findByPath ...bool) (value any)`
**Mapping data to struct:**
- `Decode(dst any) error`
- `BindStruct(key string, dst any) error`
- `MapOnExists(key string, dst any) error`
### Setting Values
- `Set(key string, val any, setByPath ...bool) (err error)`
### Useful Methods
- `Getenv(name string, defVal ...string) (val string)`
- `AddDriver(driver Driver)`
- `Data() map[string]any`
- `SetData(data map[string]any)` set data to override the Config.Data
- `Exists(key string, findByPath ...bool) bool`
- `DumpTo(out io.Writer, format string) (n int64, err error)`
## Run Tests
```bash
go test -cover
// contains all sub-folder
go test -cover ./...
```
## Projects using config
Check out these projects, which use https://github.com/gookit/config :
- https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell.
- [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby)
## Gookit packages
- [gookit/ini](https://github.com/gookit/ini) Go config management, use INI files
- [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP
- [gookit/gcli](https://github.com/gookit/gcli) build CLI application, tool library, running CLI commands
- [gookit/event](https://github.com/gookit/event) Lightweight event manager and dispatcher implements by Go
- [gookit/cache](https://github.com/gookit/cache) Generic cache use and cache manager for golang. support File, Memory, Redis, Memcached.
- [gookit/config](https://github.com/gookit/config) Go config management. support JSON, YAML, TOML, INI, HCL, ENV and Flags
- [gookit/color](https://github.com/gookit/color) A command-line color library with true color support, universal API methods and Windows support
- [gookit/filter](https://github.com/gookit/filter) Provide filtering, sanitizing, and conversion of golang data
- [gookit/validate](https://github.com/gookit/validate) Use for data validation and filtering. support Map, Struct, Form data
- [gookit/goutil](https://github.com/gookit/goutil) Some utils for the Go: string, array/slice, map, format, cli, env, filesystem, test and more
- More, please see https://github.com/gookit
## See also
- Ini parser [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser)
- Properties parser [gookit/properties](https://github.com/gookit/properties)
- Json5 parser
- [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5)
- [titanous/json5](https://github.com/titanous/json5)
- Json parser
- [goccy/go-json](https://github.com/goccy/go-json)
- [json-iterator/go](https://github.com/json-iterator/go)
- Yaml parser
- [goccy/go-yaml](https://github.com/goccy/go-yaml)
- [go-yaml/yaml](https://github.com/go-yaml/yaml)
- Toml parser [go toml](https://github.com/BurntSushi/toml)
- Data merge [mergo](https://github.com/imdario/mergo)
- Map structure [mapstructure](https://github.com/mitchellh/mapstructure)
## License
**MIT**
+545
View File
@@ -0,0 +1,545 @@
# Config
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
[![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config)
[![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions)
[![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config)
[![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](https://pkg.go.dev/github.com/gookit/config/v2)
`config` - 简洁、功能完善的 Go 应用程序配置管理工具库
> **[EN README](README.md)**
## 功能简介
- 支持多种格式: `JSON`(默认), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags`
- `JSON` 内容支持注释,可以设置解析时清除注释
- 其他驱动都是按需使用,不使用的不会加载编译到应用中
- 支持多个文件、多数据加载
- 支持从 OS ENV 变量数据加载配置
- 支持从远程 URL 加载配置数据
- 支持从命令行参数(`flags`)设置配置数据
- 数据自动覆盖合并,加载多份数据时将按`key`自动合并
- 支持丰富的自定义选项设置
- `Readonly` 支持设置配置数据只读
- `EnableCache` 支持设置配置数据缓存
- `ParseEnv` 支持获取时自动解析string值里的ENV变量(`shell: ${SHELL}` -> `shell: /bin/zsh`)
- `ParseDefault` 支持在绑定数据到结构体时解析默认值 (tag: `default:"def_value"`, 配合`ParseEnv`也支持ENV变量)
- `ParseTime` 支持绑定数据到struct时自动转换 `10s`,`2m``time.Duration`
- 完整选项设置请查看 `config.Options`
- 支持将全部或部分配置数据绑定到结构体 `config.BindStruct("key", &s)`
- 支持通过结构体标签 `default` 解析并设置默认值. eg: `default:"def_value"`
- 支持从 ENV 初始化设置字段值 `default:"${APP_ENV | dev}"`
- 支持通过 `.` 分隔符来按路径获取子级值,也支持自定义分隔符。 e.g `map.key` `arr.2`
- 支持在配置数据更改时触发事件
- 可用事件: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data`
- 简洁的使用API `Get` `Int` `Uint` `Int64` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ...
- 完善的单元测试(code coverage > 95%)
## 只使用INI
如果你仅仅想用INI来做简单配置管理,推荐使用 [gookit/ini](https://github.com/gookit/ini)
### 加载 .env 文件
`gookit/ini`: 提供一个子包 `dotenv`,支持从文件(eg `.env`)中导入数据到ENV
```shell
go get github.com/gookit/ini/v2/dotenv
```
## GoDoc
- [godoc for github](https://godoc.org/github.com/gookit/config)
## 安装包
```bash
go get github.com/gookit/config/v2
```
## 快速使用
这里使用yaml格式内容作为示例(`testdata/yml_other.yml`):
```yaml
name: app2
debug: false
baseKey: value2
shell: ${SHELL}
envKey1: ${NotExist|defValue}
map1:
key: val2
key2: val20
arr1:
- val1
- val21
```
### 载入数据
> 示例代码请看 [_examples/yaml.go](_examples/yaml.go):
```go
package main
import (
"github.com/gookit/config/v2"
"github.com/gookit/config/v2/yaml"
)
// go run ./examples/yaml.go
func main() {
// 设置选项支持ENV变量解析:当获取的值为string类型时,会尝试解析其中的ENV变量
config.WithOptions(config.ParseEnv)
// 添加驱动程序以支持yaml内容解析(除了JSON是默认支持,其他的则是按需使用)
config.AddDriver(yaml.Driver)
// 加载配置,可以同时传入多个文件
err := config.LoadFiles("testdata/yml_base.yml")
if err != nil {
panic(err)
}
// fmt.Printf("config data: \n %#v\n", config.Data())
// 加载更多文件
err = config.LoadFiles("testdata/yml_other.yml")
// 也可以一次性加载多个文件
// err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml")
if err != nil {
panic(err)
}
}
```
**使用提示**:
- 可以使用 `WithOptions()` 添加更多额外选项功能. 例如: `ParseEnv`, `ParseDefault`
- 可以使用 `AddDriver()` 添加需要使用的格式驱动器(`json` 是默认加载的的,无需添加)
- 然后就可以使用 `LoadFiles()` `LoadStrings()` 等方法加载配置数据
- 可以传入多个文件,也可以调用多次
- 多次加载的数据会自动按key进行合并处理
### 绑定数据到结构体
> 注意:结构体默认的绑定映射tag是 `mapstructure`,可以通过设置 `Options.TagName` 来更改它
```go
type User struct {
Age int `mapstructure:"age"`
Key string `mapstructure:"key"`
UserName string `mapstructure:"user_name"`
Tags []int `mapstructure:"tags"`
}
user := User{}
err = config.BindStruct("user", &user)
fmt.Println(user.UserName) // inhere
```
**更改结构标签名称**
```go
config.WithOptions(func(opt *Options) {
options.DecoderConfig.TagName = "config"
})
// use custom tag name.
type User struct {
Age int `config:"age"`
Key string `config:"key"`
UserName string `config:"user_name"`
Tags []int `config:"tags"`
}
user := User{}
err = config.Decode(&user)
```
将所有配置数据绑定到结构:
```go
config.Decode(&myConf)
// 也可以
config.BindStruct("", &myConf)
```
> `config.MapOnExists` 与 `BindStruct` 一样,但仅当 key 存在时才进行映射绑定
### 快速获取数据
```go
// 获取整型
age := config.Int("age")
fmt.Print(age) // 100
// 获取布尔值
val := config.Bool("debug")
fmt.Print(val) // true
// 获取字符串
name := config.String("name")
fmt.Print(name) // inhere
// 获取字符串数组
arr1 := config.Strings("arr1")
fmt.Printf("%#v", arr1) // []string{"val1", "val21"}
// 获取字符串KV映射
val := config.StringMap("map1")
fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"}
// 值包含ENV变量
value := config.String("shell")
fmt.Print(value) // /bin/zsh
// 通过key路径获取值
// from array
value := config.String("arr1.0")
fmt.Print(value) // "val1"
// from map
value := config.String("map1.key")
fmt.Print(value) // "val2"
```
### 设置新的值
```go
// set value
config.Set("name", "new name")
// get
name = config.String("name")
fmt.Print(name) // new name
```
## 加载配置文件
- `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件
- `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic
> **TIP**: 更多加载方式请查看 `config.Load*` 相关方法
## 从ENV载入数据
`LoadOSEnvs` 支持从环境变量中读取数据,并解析为配置数据。格式为 `ENV_NAME: config_key`
- `config_key` 可以是 key path 格式。 eg: `{"DB_USERNAME": "db.username"}` 值将会映射到 `db` 配置的 `username`
```go
// os env: APP_NAME=config APP_DEBUG=true
// load ENV info
config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug"})
// read
config.Bool("app_debug") // true
config.String("app_name") // "config"
```
## 从命令行参数载入数据
支持简单的从命令行 `flag` 参数解析,加载数据。
- 配置参数格式为 `name:type:desc` OR `name:type` OR `name:desc` (type, desc 是可选的)
- `type` 可以设置 `flag` 的类型,支持 `bool`, `int`, `string`(默认)
- `desc` 可以设置 `flag` 的描述信息
- `name` 可以是 key path 格式。 eg: `db.username`, input: `--db.username=someone` 值将会映射到 `db` 配置的 `username`
```go
// 'debug' flag is bool type
config.LoadFlags([]string{"env", "debug:bool"})
// can with flag desc message
config.LoadFlags([]string{"env:set the run env"})
config.LoadFlags([]string{"debug:bool:set debug mode"})
// can set value to map key. eg: myapp --map1.sub-key=val
config.LoadFlags([]string{"map1.sub-key"})
```
Examples:
```go
// flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val
// load flag info
keys := []string{
"name",
"env:set the run env",
"age:int",
"debug:bool:set debug mode",
"map1.sub-key",
}
err := config.LoadFlags(keys)
// read
config.String("name") // "inhere"
config.String("env") // "dev"
config.Int("age") // 99
config.Bool("debug") // true
config.Get("map1") // map[string]any{"sub-key":"val"}
```
## 创建自定义实例
您可以创建自定义配置实例:
```go
// create new instance, will auto register JSON driver
myConf := config.New("my-conf")
// create empty instance
myConf := config.NewEmpty("my-conf")
// create and with some options
myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly)
```
## 监听配置更改
现在,您可以添加一个钩子函数来监听配置数据更改。然后,您可以执行一些自定义操作, 例如:将数据写入文件
**在创建配置时添加钩子函数**:
```go
hookFn := func(event string, c *Config) {
fmt.Println("fire the:", event)
}
c := NewWithOptions("test", WithHookFunc(hookFn))
// for global config
config.WithOptions(WithHookFunc(hookFn))
```
**之后**, 当调用 `LoadXXX, Set, SetData, ClearData` 等方法时, 就会输出:
```text
fire the: load.data
fire the: set.value
fire the: set.data
fire the: clean.data
```
### 监听载入的配置文件变动
想要监听载入的配置文件变动,并在变动时重新加载配置,你需要使用 https://github.com/fsnotify/fsnotify 库。
使用方法可以参考示例 [./_example/watch_file.go](_examples/watch_file.go)
同时,你需要监听 `reload.data` 事件:
```go
config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) {
if event == config.OnReloadData {
fmt.Println("config reloaded, you can do something ....")
}
}))
```
当配置发生变化并重新加载后,你可以做相关的事情,例如:重新绑定配置到你的结构体。
## 导出配置到文件
> 可以使用 `config.DumpTo(out io.Writer, format string)` 将整个配置数据导出到指定的writer, 比如 buffer,file。
**示例:导出为JSON文件**
```go
buf := new(bytes.Buffer)
_, err := config.DumpTo(buf, config.JSON)
ioutil.WriteFile("my-config.json", buf.Bytes(), 0755)
```
**示例:导出格式化的JSON**
可以设置默认变量 `JSONMarshalIndent` 的值 或 自定义新的 JSON 驱动程序。
```go
config.JSONMarshalIndent = " "
```
**示例:导出为YAML文件**
```go
_, err := config.DumpTo(buf, config.YAML)
ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755)
```
## 可用选项
```go
// Options config options
type Options struct {
// parse env in string value. like: "${EnvName}" "${EnvName|default}"
ParseEnv bool
// ParseTime parses a duration string to time.Duration
// eg: 10s, 2m
ParseTime bool
// config is readonly. default is False
Readonly bool
// enable config data cache. default is False
EnableCache bool
// parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub`
ParseKey bool
// the delimiter char for split key, when `FindByPath=true`. default is '.'
Delimiter byte
// default write format. default is JSON
DumpFormat string
// default input format. default is JSON
ReadFormat string
// DecoderConfig setting for binding data to struct
DecoderConfig *mapstructure.DecoderConfig
// HookFunc on data changed.
HookFunc HookFunc
// ParseDefault tag on binding data to struct. tag: default
ParseDefault bool
}
```
> **提示**: 访问 https://pkg.go.dev/github.com/gookit/config/v2#Options 查看最新的选项信息
Examples for set options:
```go
config.WithOptions(config.WithTagName("mytag"))
config.WithOptions(func(opt *Options) {
opt.SetTagNames("config")
})
```
### 选项: 解析默认值
NEW: 支持通过结构标签 `default` 解析并设置默认值,支持嵌套解析处理。
> 注意 ⚠️ 如果想要解析子结构体字段,需要对父结构体设置 `default:""` 标记,否则不会解析子结构体的字段。
```go
// add option: config.ParseDefault
c := config.New("test").WithOptions(config.ParseDefault)
// only set name
c.SetData(map[string]any{
"name": "inhere",
})
// age load from default tag
type User struct {
Age int `default:"30"`
Name string
Tags []int
}
user := &User{}
goutil.MustOk(c.Decode(user))
dump.Println(user)
```
**Output**:
```shell
&config_test.User {
Age: int(30),
Name: string("inhere"), #len=6
Tags: []int [ #len=0
],
},
```
## API方法参考
### 载入配置
- `LoadData(dataSource ...any) (err error)` 从struct或map加载数据
- `LoadFlags(keys []string) (err error)` 从命令行参数载入数据
- `LoadOSEnvs(nameToKeyMap map[string]string)` 从ENV载入配置数据
- `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件
- `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic
- `LoadFromDir(dirPath, format string) (err error)` 从给定目录里加载自定格式的文件,文件名会作为 key
- `LoadRemote(format, url string) (err error)` 从远程 URL 加载配置数据
- `LoadSources(format string, src []byte, more ...[]byte) (err error)` 从给定格式的字节数据加载配置
- `LoadStrings(format string, str string, more ...string) (err error)` 从给定格式的字符串配置里加载配置数据
- `LoadFilesByFormat(format string, sourceFiles ...string) (err error)` 从给定格式的文件加载配置
- `LoadExistsByFormat(format string, sourceFiles ...string) error` 从给定格式的文件加载配置,会忽略不存在的文件
### 获取值
- `Bool(key string, defVal ...bool) bool`
- `Int(key string, defVal ...int) int`
- `Uint(key string, defVal ...uint) uint`
- `Int64(key string, defVal ...int64) int64`
- `Ints(key string) (arr []int)`
- `IntMap(key string) (mp map[string]int)`
- `Float(key string, defVal ...float64) float64`
- `String(key string, defVal ...string) string`
- `Strings(key string) (arr []string)`
- `StringMap(key string) (mp map[string]string)`
- `SubDataMap(key string) maputi.Data`
- `Get(key string, findByPath ...bool) (value any)`
**将数据映射到结构体:**
- `BindStruct(key string, dst any) error`
- `MapOnExists(key string, dst any) error`
### 设置值
- `Set(key string, val any, setByPath ...bool) (err error)`
### 有用的方法
- `Getenv(name string, defVal ...string) (val string)`
- `AddDriver(driver Driver)`
- `Data() map[string]any`
- `Exists(key string, findByPath ...bool) bool`
- `DumpTo(out io.Writer, format string) (n int64, err error)`
- `SetData(data map[string]any)` 设置数据以覆盖 `Config.Data`
## 单元测试
```bash
go test -cover
// contains all sub-folder
go test -cover ./...
```
## 使用Config的项目
看看这些使用了 https://github.com/gookit/config 的项目:
- https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell.
- [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby)
## Gookit 工具包
- [gookit/ini](https://github.com/gookit/ini) INI配置读取管理,支持多文件加载,数据覆盖合并, 解析ENV变量, 解析变量引用
- [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP
- [gookit/gcli](https://github.com/gookit/gcli) Go的命令行应用,工具库,运行CLI命令,支持命令行色彩,用户交互,进度显示,数据格式化显示
- [gookit/event](https://github.com/gookit/event) Go实现的轻量级的事件管理、调度程序库, 支持设置监听器的优先级, 支持对一组事件进行监听
- [gookit/cache](https://github.com/gookit/cache) 通用的缓存使用包装库,通过包装各种常用的驱动,来提供统一的使用API
- [gookit/config](https://github.com/gookit/config) Go应用配置管理,支持多种格式(JSON, YAML, TOML, INI, HCL, ENV, Flags),多文件加载,远程文件加载,数据合并
- [gookit/color](https://github.com/gookit/color) CLI 控制台颜色渲染工具库, 拥有简洁的使用API,支持16色,256色,RGB色彩渲染输出
- [gookit/filter](https://github.com/gookit/filter) 提供对Golang数据的过滤,净化,转换
- [gookit/validate](https://github.com/gookit/validate) Go通用的数据验证与过滤库,使用简单,内置大部分常用验证、过滤器
- [gookit/goutil](https://github.com/gookit/goutil) Go 的一些工具函数,格式化,特殊处理,常用信息获取等
- 更多请查看 https://github.com/gookit
## 相关包
- Ini 解析 [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser)
- Properties 解析 [gookit/properties](https://github.com/gookit/properties)
- Yaml 解析 [go-yaml](https://github.com/go-yaml/yaml)
- Toml 解析 [go toml](https://github.com/BurntSushi/toml)
- 数据合并 [mergo](https://github.com/imdario/mergo)
- 映射数据到结构体 [mapstructure](https://github.com/mitchellh/mapstructure)
- JSON5 解析
- [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5)
- [titanous/json5](https://github.com/titanous/json5)
## License
**MIT**
+5
View File
@@ -0,0 +1,5 @@
# TODO
- remote `etcd` `consul`
- watch changed config files and reload
- [x] set default value on binding struct. use tag `defalut`
+304
View File
@@ -0,0 +1,304 @@
/*
Package config is a go config management implement. support YAML,TOML,JSON,INI,HCL format.
Source code and other details for the project are available at GitHub:
https://github.com/gookit/config
JSON format content example:
{
"name": "app",
"debug": false,
"baseKey": "value",
"age": 123,
"envKey": "${SHELL}",
"envKey1": "${NotExist|defValue}",
"map1": {
"key": "val",
"key1": "val1",
"key2": "val2"
},
"arr1": [
"val",
"val1",
"val2"
],
"lang": {
"dir": "res/lang",
"defLang": "en",
"allowed": {
"en": "val",
"zh-CN": "val2"
}
}
}
Usage please see example(more example please see examples folder in the lib):
*/
package config
import (
"fmt"
"sync"
)
// There are supported config format
const (
Ini = "ini"
Hcl = "hcl"
Yml = "yml"
JSON = "json"
Yaml = "yaml"
Toml = "toml"
Prop = "properties"
)
const (
// default delimiter
defaultDelimiter byte = '.'
// default struct tag name for binding data to struct
defaultStructTag = "mapstructure"
// struct tag name for set default-value on binding data
defaultValueTag = "default"
)
// internal vars
// type intArr []int
type strArr []string
// type intMap map[string]int
type strMap map[string]string
// This is a default config manager instance
var dc = New("default")
// Config structure definition
type Config struct {
// save the latest error, will clear after read.
err error
// config instance name
name string
lock sync.RWMutex
// config options
opts *Options
// all config data
data map[string]any
// loaded config files records
loadedUrls []string
loadedFiles []string
driverNames []string
// driver alias to name map.
aliasMap map[string]string
reloading bool
// TODO Deprecated decoder and encoder, use driver instead
// drivers map[string]Driver
// decoders["toml"] = func(blob []byte, v any) (err error){}
// decoders["yaml"] = func(blob []byte, v any) (err error){}
decoders map[string]Decoder
encoders map[string]Encoder
// cache on got config data
intCache map[string]int
strCache map[string]string
// iArrCache map[string]intArr TODO cache it
// iMapCache map[string]intMap
sArrCache map[string]strArr
sMapCache map[string]strMap
}
// New config instance with custom options, default with JSON driver
func New(name string, opts ...OptionFn) *Config {
return NewEmpty(name, opts...).WithDriver(JSONDriver)
}
// NewGeneric create generic config instance with custom options.
//
// - default add options: ParseEnv, ParseDefault, ParseTime
func NewGeneric(name string, opts ...OptionFn) *Config {
return NewEmpty(name, ParseEnv, ParseDefault, ParseTime).WithOptions(opts...).WithDriver(JSONDriver)
}
// NewEmpty create config instance with custom options
func NewEmpty(name string, opts ...OptionFn) *Config {
c := &Config{
name: name,
opts: newDefaultOption(),
data: make(map[string]any),
// don't add any drivers
encoders: map[string]Encoder{},
decoders: map[string]Decoder{},
aliasMap: make(map[string]string),
}
return c.WithOptions(opts...)
}
// NewWith create config instance, and you can call some init func
func NewWith(name string, fn func(c *Config)) *Config {
return New(name).With(fn)
}
// NewWithOptions config instance. alias of New()
func NewWithOptions(name string, opts ...OptionFn) *Config {
return New(name).WithOptions(opts...)
}
// Default get the default instance
func Default() *Config { return dc }
/*************************************************************
* config drivers
*************************************************************/
// WithDriver set multi drivers at once.
func WithDriver(drivers ...Driver) { dc.WithDriver(drivers...) }
// WithDriver set multi drivers at once.
func (c *Config) WithDriver(drivers ...Driver) *Config {
for _, driver := range drivers {
c.AddDriver(driver)
}
return c
}
// AddDriver set a decoder and encoder driver for a format.
func AddDriver(driver Driver) { dc.AddDriver(driver) }
// AddDriver set a decoder and encoder driver for a format.
func (c *Config) AddDriver(driver Driver) {
format := driver.Name()
if len(driver.Aliases()) > 0 {
for _, alias := range driver.Aliases() {
c.aliasMap[alias] = format
}
}
c.driverNames = append(c.driverNames, format)
c.decoders[format] = driver.GetDecoder()
c.encoders[format] = driver.GetEncoder()
}
// HasDecoder has decoder
func (c *Config) HasDecoder(format string) bool {
format = c.resolveFormat(format)
_, ok := c.decoders[format]
return ok
}
// HasEncoder has encoder
func (c *Config) HasEncoder(format string) bool {
format = c.resolveFormat(format)
_, ok := c.encoders[format]
return ok
}
// DelDriver delete driver of the format
func (c *Config) DelDriver(format string) {
format = c.resolveFormat(format)
delete(c.decoders, format)
delete(c.encoders, format)
}
/*************************************************************
* helper methods
*************************************************************/
// Name get config name
func (c *Config) Name() string { return c.name }
// AddAlias add alias for a format(driver name)
func AddAlias(format, alias string) { dc.AddAlias(format, alias) }
// AddAlias add alias for a format(driver name)
//
// Example:
//
// config.AddAlias("ini", "conf")
func (c *Config) AddAlias(format, alias string) {
c.aliasMap[alias] = format
}
// AliasMap get alias map
func (c *Config) AliasMap() map[string]string { return c.aliasMap }
// Error get last error, will clear after read.
func (c *Config) Error() error {
err := c.err
c.err = nil
return err
}
// IsEmpty of the config
func (c *Config) IsEmpty() bool {
return len(c.data) == 0
}
// LoadedUrls get loaded urls list
func (c *Config) LoadedUrls() []string { return c.loadedUrls }
// LoadedFiles get loaded files name
func (c *Config) LoadedFiles() []string { return c.loadedFiles }
// DriverNames get loaded driver names
func (c *Config) DriverNames() []string { return c.driverNames }
// Reset data and caches
func Reset() { dc.ClearAll() }
// ClearAll data and caches
func ClearAll() { dc.ClearAll() }
// ClearAll data and caches
func (c *Config) ClearAll() {
c.ClearData()
c.ClearCaches()
c.aliasMap = make(map[string]string)
// options
c.opts.Readonly = false
}
// ClearData clear data
func (c *Config) ClearData() {
c.fireHook(OnCleanData)
c.data = make(map[string]any)
c.loadedUrls = []string{}
c.loadedFiles = []string{}
}
// ClearCaches clear caches
func (c *Config) ClearCaches() {
if c.opts.EnableCache {
c.intCache = nil
c.strCache = nil
c.sMapCache = nil
c.sArrCache = nil
}
}
/*************************************************************
* helper methods
*************************************************************/
// fire hook
func (c *Config) fireHook(name string) {
if c.opts.HookFunc != nil {
c.opts.HookFunc(name, c)
}
}
// record error
func (c *Config) addError(err error) {
c.err = err
}
// format and record error
func (c *Config) addErrorf(format string, a ...any) {
c.err = fmt.Errorf(format, a...)
}
+162
View File
@@ -0,0 +1,162 @@
package config
import (
"encoding/json"
"github.com/gookit/goutil/jsonutil"
)
// Driver interface.
// TODO refactor: rename GetDecoder() to Decode(), rename GetEncoder() to Encode()
type Driver interface {
Name() string
Aliases() []string // alias format names, use for resolve format name
GetDecoder() Decoder
GetEncoder() Encoder
}
// DriverV2 interface.
type DriverV2 interface {
Name() string // driver name, also is format name.
Aliases() []string // alias format names, use for resolve format name
Decode(blob []byte, v any) (err error)
Encode(v any) (out []byte, err error)
}
// Decoder for decode yml,json,toml format content
type Decoder func(blob []byte, v any) (err error)
// Encoder for decode yml,json,toml format content
type Encoder func(v any) (out []byte, err error)
// StdDriver struct
type StdDriver struct {
name string
aliases []string
decoder Decoder
encoder Encoder
}
// NewDriver new std driver instance.
func NewDriver(name string, dec Decoder, enc Encoder) *StdDriver {
return &StdDriver{name: name, decoder: dec, encoder: enc}
}
// WithAliases set aliases for driver
func (d *StdDriver) WithAliases(aliases ...string) *StdDriver {
d.aliases = aliases
return d
}
// WithAlias add alias for driver
func (d *StdDriver) WithAlias(alias string) *StdDriver {
d.aliases = append(d.aliases, alias)
return d
}
// Name of driver
func (d *StdDriver) Name() string { return d.name }
// Aliases format name of driver
func (d *StdDriver) Aliases() []string {
return d.aliases
}
// Decode of driver
func (d *StdDriver) Decode(blob []byte, v any) (err error) {
return d.decoder(blob, v)
}
// Encode of driver
func (d *StdDriver) Encode(v any) ([]byte, error) {
return d.encoder(v)
}
// GetDecoder of driver
func (d *StdDriver) GetDecoder() Decoder {
return d.decoder
}
// GetEncoder of driver
func (d *StdDriver) GetEncoder() Encoder {
return d.encoder
}
/*************************************************************
* JSON driver
*************************************************************/
var (
// JSONAllowComments support write comments on json file.
JSONAllowComments = true
// JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data.
//
// Deprecated: please use JSONDriver.MarshalIndent
JSONMarshalIndent string
)
// JSONDecoder for json decode
var JSONDecoder Decoder = func(data []byte, v any) (err error) {
JSONDriver.ClearComments = JSONAllowComments
return JSONDriver.Decode(data, v)
}
// JSONEncoder for json encode
var JSONEncoder Encoder = func(v any) (out []byte, err error) {
JSONDriver.MarshalIndent = JSONMarshalIndent
return JSONDriver.Encode(v)
}
// JSONDriver instance fot json
var JSONDriver = &jsonDriver{
driverName: JSON,
ClearComments: JSONAllowComments,
MarshalIndent: JSONMarshalIndent,
}
// jsonDriver for json format content
type jsonDriver struct {
driverName string
// ClearComments before parse JSON string.
ClearComments bool
// MarshalIndent if not empty, will use json.MarshalIndent for encode data.
MarshalIndent string
}
// Name of the driver
func (d *jsonDriver) Name() string {
return d.driverName
}
// Aliases of the driver
func (d *jsonDriver) Aliases() []string {
return nil
}
// Decode for the driver
func (d *jsonDriver) Decode(data []byte, v any) error {
if d.ClearComments {
str := jsonutil.StripComments(string(data))
return json.Unmarshal([]byte(str), v)
}
return json.Unmarshal(data, v)
}
// GetDecoder for the driver
func (d *jsonDriver) GetDecoder() Decoder {
return d.Decode
}
// Encode for the driver
func (d *jsonDriver) Encode(v any) (out []byte, err error) {
if len(d.MarshalIndent) > 0 {
return json.MarshalIndent(v, "", d.MarshalIndent)
}
return json.Marshal(v)
}
// GetEncoder for the driver
func (d *jsonDriver) GetEncoder() Encoder {
return d.Encode
}
+183
View File
@@ -0,0 +1,183 @@
package config
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"github.com/go-viper/mapstructure/v2"
"github.com/gookit/goutil/structs"
)
// Decode all config data to the dst ptr
//
// Usage:
//
// myConf := &MyConf{}
// config.Decode(myConf)
func Decode(dst any) error { return dc.Decode(dst) }
// Decode all config data to the dst ptr.
//
// It's equals:
//
// c.Structure("", dst)
func (c *Config) Decode(dst any) error {
return c.Structure("", dst)
}
// MapStruct alias method of the 'Structure'
//
// Usage:
//
// dbInfo := &Db{}
// config.MapStruct("db", dbInfo)
func MapStruct(key string, dst any) error { return dc.MapStruct(key, dst) }
// MapStruct alias method of the 'Structure'
func (c *Config) MapStruct(key string, dst any) error { return c.Structure(key, dst) }
// BindStruct alias method of the 'Structure'
func BindStruct(key string, dst any) error { return dc.BindStruct(key, dst) }
// BindStruct alias method of the 'Structure'
func (c *Config) BindStruct(key string, dst any) error { return c.Structure(key, dst) }
// MapOnExists mapping data to the dst structure only on key exists.
func MapOnExists(key string, dst any) error {
return dc.MapOnExists(key, dst)
}
// MapOnExists mapping data to the dst structure only on key exists.
//
// - Support ParseEnv on mapping
// - Support ParseDefault on mapping
func (c *Config) MapOnExists(key string, dst any) error {
err := c.Structure(key, dst)
if err != nil && err == ErrNotFound {
return nil
}
return err
}
// Structure get config data and binding to the dst structure.
//
// - Support ParseEnv on mapping
// - Support ParseDefault on mapping
//
// Usage:
//
// dbInfo := Db{}
// config.Structure("db", &dbInfo)
func (c *Config) Structure(key string, dst any) (err error) {
var data any
// binding all data on key is empty.
if key == "" {
// fix: if c.data is nil, don't need to apply map structure
if len(c.data) == 0 {
// init default value by tag: default
if c.opts.ParseDefault {
err = structs.InitDefaults(dst, func(opt *structs.InitOptions) {
opt.ParseEnv = c.opts.ParseEnv
opt.ParseTime = c.opts.ParseTime // add ParseTime support on parse default value
})
}
return
}
data = c.data
} else {
// binding sub-data of the config
var ok bool
data, ok = c.GetValue(key)
if !ok {
return ErrNotFound
}
}
// map structure from data
bindConf := c.opts.makeDecoderConfig()
// set result struct ptr
bindConf.Result = dst
decoder, err := mapstructure.NewDecoder(bindConf)
if err == nil {
if err = decoder.Decode(data); err != nil {
return err
}
}
// init default value by tag: default
if c.opts.ParseDefault {
err = structs.InitDefaults(dst, func(opt *structs.InitOptions) {
opt.ParseEnv = c.opts.ParseEnv
opt.ParseTime = c.opts.ParseTime
})
}
return err
}
// ToJSON string, will ignore error
func (c *Config) ToJSON() string {
buf := &bytes.Buffer{}
_, err := c.DumpTo(buf, JSON)
if err != nil {
return ""
}
return buf.String()
}
// WriteTo a writer
func WriteTo(out io.Writer) (int64, error) { return dc.WriteTo(out) }
// WriteTo Write out config data representing the current state to a writer.
func (c *Config) WriteTo(out io.Writer) (n int64, err error) {
return c.DumpTo(out, c.opts.DumpFormat)
}
// DumpTo a writer and use format
func DumpTo(out io.Writer, format string) (int64, error) { return dc.DumpTo(out, format) }
// DumpTo use the format(json,yaml,toml) dump config data to a writer
func (c *Config) DumpTo(out io.Writer, format string) (n int64, err error) {
var ok bool
var encoder Encoder
format = c.resolveFormat(format)
if encoder, ok = c.encoders[format]; !ok {
err = errors.New("not exists/register encoder for the format: " + format)
return
}
// is empty
if len(c.data) == 0 {
return
}
// encode data to string
encoded, err := encoder(c.data)
if err != nil {
return
}
// write content to out
num, _ := fmt.Fprintln(out, string(encoded))
return int64(num), nil
}
// DumpToFile use the format(json,yaml,toml) dump config data to a writer
func (c *Config) DumpToFile(fileName string, format string) (err error) {
fsFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
f, err := os.OpenFile(fileName, fsFlags, os.ModePerm)
if err != nil {
return err
}
_, err = c.DumpTo(f, format)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
+524
View File
@@ -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
}
+224
View File
@@ -0,0 +1,224 @@
package config
import (
"strings"
"dario.cat/mergo"
"github.com/go-viper/mapstructure/v2"
"github.com/gookit/goutil"
)
// there are some event names for config data changed.
const (
OnSetValue = "set.value"
OnSetData = "set.data"
OnLoadData = "load.data"
OnReloadData = "reload.data"
OnCleanData = "clean.data"
)
// HookFunc on config data changed.
type HookFunc func(event string, c *Config)
// Options config options
type Options struct {
// ParseEnv parse env in string value and default value. default: false
//
// - like: "${EnvName}" "${EnvName|default}"
ParseEnv bool
// ParseTime parses a duration string to `time.Duration`. default: false
//
// eg: 10s, 2m
ParseTime bool
// ParseDefault tag on binding data to struct. default: false
//
// - tag: default
//
// NOTE: If you want to parse a substruct, you need to set the `default:""` flag on the struct,
// otherwise the fields that will not resolve to it will not be resolved.
ParseDefault bool
// Readonly config is readonly. default: false
Readonly bool
// EnableCache enable config data cache. default: false
EnableCache bool
// ParseKey support key path, allow finding value by key path. default: true
//
// - eg: 'key.sub' will find `map[key]sub`
ParseKey bool
// TagName tag name for binding data to struct
//
// Deprecated: please set tag name by DecoderConfig, or use SetTagName()
TagName string
// Delimiter the delimiter char for split key path, on `ParseKey=true`.
//
// - default is '.'
Delimiter byte
// DumpFormat default write format. default is 'json'
DumpFormat string
// ReadFormat default input format. default is 'json'
ReadFormat string
// DecoderConfig setting for binding data to struct. such as: TagName
DecoderConfig *mapstructure.DecoderConfig
// MergeOptions settings for merge two data
MergeOptions []func(*mergo.Config)
// HookFunc on data changed. you can do something...
HookFunc HookFunc
// WatchChange bool
}
// OptionFn option func
type OptionFn func(*Options)
func newDefaultOption() *Options {
return &Options{
ParseKey: true,
TagName: defaultStructTag,
Delimiter: defaultDelimiter,
// for export
DumpFormat: JSON,
ReadFormat: JSON,
// struct decoder config
DecoderConfig: newDefaultDecoderConfig(""),
MergeOptions: []func(*mergo.Config){
mergo.WithOverride,
mergo.WithTypeCheck,
},
}
}
func newDefaultDecoderConfig(tagName string) *mapstructure.DecoderConfig {
if tagName == "" {
tagName = defaultStructTag
}
return &mapstructure.DecoderConfig{
// tag name for binding struct
TagName: tagName,
// will auto convert string to int/uint
WeaklyTypedInput: true,
}
}
// 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
}
func (o *Options) makeDecoderConfig() *mapstructure.DecoderConfig {
var bindConf *mapstructure.DecoderConfig
if o.DecoderConfig == nil {
bindConf = newDefaultDecoderConfig(o.TagName)
} else {
// copy new config for each binding.
copyConf := *o.DecoderConfig
bindConf = &copyConf
// compatible with previous settings opts.TagName
if bindConf.TagName == "" {
bindConf.TagName = o.TagName
}
}
// add hook on decode value to struct
if bindConf.DecodeHook == nil && o.shouldAddHookFunc() {
bindConf.DecodeHook = ValDecodeHookFunc(o.ParseEnv, o.ParseTime)
}
return bindConf
}
/*************************************************************
* config setting
*************************************************************/
// WithTagName set tag name for export to struct
func WithTagName(tagName string) func(*Options) {
return func(opts *Options) {
opts.SetTagName(tagName)
}
}
// ParseEnv set parse env value
func ParseEnv(opts *Options) { opts.ParseEnv = true }
// ParseTime set parse time string.
func ParseTime(opts *Options) { opts.ParseTime = true }
// ParseDefault tag value on binding data to struct.
func ParseDefault(opts *Options) { opts.ParseDefault = true }
// Readonly set readonly
func Readonly(opts *Options) { opts.Readonly = true }
// Delimiter set delimiter char
func Delimiter(sep byte) func(*Options) {
return func(opts *Options) {
opts.Delimiter = sep
}
}
// SaveFileOnSet set hook func, will panic on save error
func SaveFileOnSet(fileName string, format string) func(options *Options) {
return func(opts *Options) {
opts.HookFunc = func(event string, c *Config) {
if strings.HasPrefix(event, "set.") {
goutil.PanicErr(c.DumpToFile(fileName, format))
}
}
}
}
// WithHookFunc set hook func
func WithHookFunc(fn HookFunc) func(*Options) {
return func(opts *Options) {
opts.HookFunc = fn
}
}
// EnableCache set readonly
func EnableCache(opts *Options) { opts.EnableCache = true }
// WithOptions with options
func WithOptions(opts ...OptionFn) { dc.WithOptions(opts...) }
// WithOptions apply some options
func (c *Config) WithOptions(opts ...OptionFn) *Config {
if !c.IsEmpty() {
panic("config: Cannot set options after data has been loaded")
}
// apply options
for _, opt := range opts {
opt(c.opts)
}
return c
}
// GetOptions get options
func GetOptions() *Options { return dc.Options() }
// Options get
func (c *Config) Options() *Options {
return c.opts
}
// With apply some options
func (c *Config) With(fn func(c *Config)) *Config {
fn(c)
return c
}
// Readonly disable set data to config.
//
// Usage:
//
// config.LoadFiles(a, b, c)
// config.Readonly()
func (c *Config) Readonly() {
c.opts.Readonly = true
}
+664
View File
@@ -0,0 +1,664 @@
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)
// check slice index
if err != nil || len(typeData) < i {
return false
}
case []string: // is array(is from Set)
i, err := strconv.Atoi(k)
if err != nil || len(typeData) < i {
return false
}
case []any: // is array(load from file)
i, err := strconv.Atoi(k)
if err != nil || len(typeData) < i {
return false
}
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
func (c *Config) Duration(key string, defVal ...time.Duration) time.Duration {
value, exist := c.tryInt64(key)
if !exist && len(defVal) > 0 {
return defVal[0]
}
return time.Duration(value)
}
// 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{}
}
+158
View File
@@ -0,0 +1,158 @@
package config
import (
"os"
"reflect"
"strings"
"github.com/go-viper/mapstructure/v2"
"github.com/gookit/goutil/envutil"
"github.com/gookit/goutil/reflects"
)
// ValDecodeHookFunc returns a mapstructure.DecodeHookFunc
// that parse ENV var, and more custom parse
func ValDecodeHookFunc(parseEnv, parseTime bool) mapstructure.DecodeHookFunc {
return func(f reflect.Type, t reflect.Type, data any) (any, error) {
if f.Kind() != reflect.String {
return data, nil
}
var err error
str := data.(string)
if parseEnv {
// https://docs.docker.com/compose/environment-variables/env-file/
str, err = envutil.ParseOrErr(str)
if err != nil {
return nil, err
}
}
if len(str) < 2 {
return str, nil
}
// feat: support parse time or duration string. eg: 10s
if parseTime && str[0] > '0' && str[0] <= '9' {
return reflects.ToTimeOrDuration(str, t)
}
return str, nil
}
}
// resolve format, check is alias
func (c *Config) resolveFormat(f string) string {
if name, ok := c.aliasMap[f]; ok {
return name
}
return f
}
/*************************************************************
* Deprecated methods
*************************************************************/
// SetDecoder add/set a format decoder
//
// Deprecated: please use driver instead
func SetDecoder(format string, decoder Decoder) {
dc.SetDecoder(format, decoder)
}
// SetDecoder set decoder
//
// Deprecated: please use driver instead
func (c *Config) SetDecoder(format string, decoder Decoder) {
format = c.resolveFormat(format)
c.decoders[format] = decoder
}
// SetDecoders set decoders
//
// Deprecated: please use driver instead
func (c *Config) SetDecoders(decoders map[string]Decoder) {
for format, decoder := range decoders {
c.SetDecoder(format, decoder)
}
}
// SetEncoder set a encoder for the format
//
// Deprecated: please use driver instead
func SetEncoder(format string, encoder Encoder) {
dc.SetEncoder(format, encoder)
}
// SetEncoder set a encoder for the format
//
// Deprecated: please use driver instead
func (c *Config) SetEncoder(format string, encoder Encoder) {
format = c.resolveFormat(format)
c.encoders[format] = encoder
}
// SetEncoders set encoders
//
// Deprecated: please use driver instead
func (c *Config) SetEncoders(encoders map[string]Encoder) {
for format, encoder := range encoders {
c.SetEncoder(format, encoder)
}
}
/*************************************************************
* helper methods/functions
*************************************************************/
// LoadENVFiles load
// func LoadENVFiles(filePaths ...string) error {
// return dotenv.LoadFiles(filePaths...)
// }
// GetEnv get os ENV value by name
func GetEnv(name string, defVal ...string) (val string) {
return Getenv(name, defVal...)
}
// Getenv get os ENV value by name. like os.Getenv, but support default value
//
// Notice:
// - Key is not case-sensitive when getting
func Getenv(name string, defVal ...string) (val string) {
if val = os.Getenv(name); val != "" {
return
}
if len(defVal) > 0 {
val = defVal[0]
}
return
}
func parseVarNameAndType(key string) (string, string, string) {
var desc string
typ := "string"
key = strings.Trim(key, "-")
// can set var type: int, uint, bool
if strings.IndexByte(key, ':') > 0 {
list := strings.SplitN(key, ":", 3)
key, typ = list[0], list[1]
if len(list) == 3 {
desc = list[2]
}
// if type is not valid and has multi words, as desc message.
if _, ok := validTypes[typ]; !ok {
if desc == "" && strings.ContainsRune(typ, ' ') {
desc = typ
}
typ = "string"
}
}
return key, typ, desc
}
// format key
func formatKey(key, sep string) string {
return strings.Trim(strings.TrimSpace(key), sep)
}
+65
View File
@@ -0,0 +1,65 @@
package config
import (
"errors"
"strings"
"github.com/gookit/goutil/maputil"
)
// some common errors definitions
var (
ErrReadonly = errors.New("the config instance in 'readonly' mode")
ErrKeyIsEmpty = errors.New("the config key is cannot be empty")
ErrNotFound = errors.New("this key does not exist in the config")
)
// SetData for override the Config.Data
func SetData(data map[string]any) {
dc.SetData(data)
}
// SetData for override the Config.Data
func (c *Config) SetData(data map[string]any) {
c.lock.Lock()
c.data = data
c.lock.Unlock()
c.fireHook(OnSetData)
}
// Set value by key. setByPath default is true
func Set(key string, val any, setByPath ...bool) error {
return dc.Set(key, val, setByPath...)
}
// Set a value by key string. setByPath default is true
func (c *Config) Set(key string, val any, setByPath ...bool) (err error) {
if c.opts.Readonly {
return ErrReadonly
}
c.lock.Lock()
defer c.lock.Unlock()
sep := c.opts.Delimiter
if key = formatKey(key, string(sep)); key == "" {
return ErrKeyIsEmpty
}
defer c.fireHook(OnSetValue)
if strings.IndexByte(key, sep) == -1 {
c.data[key] = val
return
}
// disable set by path.
if len(setByPath) > 0 && !setByPath[0] {
c.data[key] = val
return
}
// set by path
keys := strings.Split(key, string(sep))
return maputil.SetByKeys(&c.data, keys, val)
}
+20
View File
@@ -0,0 +1,20 @@
/*
Package yaml is a driver use YAML format content as config source
Usage please see example:
*/
package yaml
import (
"github.com/goccy/go-yaml"
"github.com/gookit/config/v2"
)
// Decoder the yaml content decoder
var Decoder config.Decoder = yaml.Unmarshal
// Encoder the yaml content encoder
var Encoder config.Encoder = yaml.Marshal
// Driver for yaml
var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml)