Initial QSfera import
This commit is contained in:
@@ -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
@@ -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
@@ -0,0 +1,559 @@
|
||||
# Config
|
||||
|
||||

|
||||
[](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
|
||||
[](https://travis-ci.org/gookit/config)
|
||||
[](https://github.com/gookit/config/actions)
|
||||
[](https://coveralls.io/github/gookit/config?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/gookit/config)
|
||||
[](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
@@ -0,0 +1,545 @@
|
||||
# Config
|
||||
|
||||

|
||||
[](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings)
|
||||
[](https://travis-ci.org/gookit/config)
|
||||
[](https://github.com/gookit/config/actions)
|
||||
[](https://coveralls.io/github/gookit/config?branch=master)
|
||||
[](https://goreportcard.com/report/github.com/gookit/config)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 = ©Conf
|
||||
|
||||
// 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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user