diff --git a/.travis.yml b/.travis.yml index da041be..7d9dfca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go dist: xenial go: -# - '1.10' mod v2 is not support +# - '1.10' go mod v2 is not support - '1.11' - '1.12' @@ -14,4 +14,4 @@ before_install: script: # - go test -v -cover - - $HOME/gopath/bin/goveralls -service=travis-ci + - $HOME/gopath/bin/goveralls -v -service=travis-ci diff --git a/README.md b/README.md index c412d82..1bbc185 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Golang application config manage tool library. - Support for loading configuration data from remote URLs - Support for setting configuration data from command line arguments(`flags`) - 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 get sub value by 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` ... @@ -187,6 +188,17 @@ config.Bool("app_debug") // true config.String("app_name") // "config" ``` +## Bind data to structure + +```go + user := struct { + Age int + Kye string + Tags []int + }{} + err = BindStruct("user", &user) +``` + ## API Methods Refer ### Load Config diff --git a/README.zh-CN.md b/README.zh-CN.md index 64f0ed2..9344585 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -6,7 +6,7 @@ [![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config) -功能完善的Golang应用程序配置管理工具库。 +简洁、功能完善的Golang应用程序配置管理工具库 > **[EN README](README.md)** @@ -18,8 +18,9 @@ - 支持多个文件、多数据加载 - 支持从 OS ENV 变量数据加载配置 - 支持从远程 URL 加载配置数据 -- 支持从命令行参数(flags)设置配置数据 +- 支持从命令行参数(`flags`)设置配置数据 - 支持数据覆盖合并,加载多份数据时将按key自动合并 +- 支持将全部或部分配置数据绑定到结构体 `config.BindStruct("key", &s)` - 支持通过 `.` 分隔符来按路径获取子级值。 e.g `map.key` `arr.2` - 支持解析ENV变量名称。 like `shell: ${SHELL}` -> `shell: /bin/zsh` - 简洁的使用API `Get` `Int` `Uint` `Int64` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ... @@ -140,6 +141,17 @@ name = config.String("name") fmt.Print(name) // new name ``` +### 绑定数据到结构体 + +```go + user := struct { + Age int + Kye string + Tags []int + }{} + err = BindStruct("user", &user) +``` + ## API方法参考 ### 载入配置 @@ -178,6 +190,7 @@ fmt.Print(name) // new name - `Data() map[string]interface{}` - `Exists(key string, findByPath ...bool) bool` - `DumpTo(out io.Writer, format string) (n int64, err error)` +- `BindStruct(key string, dst interface{}) error` ## 单元测试 diff --git a/config.go b/config.go index 4c5b2e7..bba82d0 100644 --- a/config.go +++ b/config.go @@ -369,8 +369,9 @@ func GetEnv(name string, defVal ...string) (val string) { } // 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) { - // name = strings.ToUpper(name) if val = os.Getenv(name); val != "" { return } diff --git a/config_test.go b/config_test.go index 3ca844a..d225e78 100644 --- a/config_test.go +++ b/config_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/gookit/config/v2/dotnev" + "github.com/gookit/goutil/testutil" "github.com/stretchr/testify/assert" ) @@ -171,14 +171,14 @@ func TestBasic(t *testing.T) { } func TestGetEnv(t *testing.T) { - _ = dotnev.LoadFromMap(map[string]string{ - "app_name": "config", - "app_debug": "true", + testutil.MockEnvValues(map[string]string{ + "APP_NAME": "config", + "APP_DEBUG": "true", + }, func() { + assert.Equal(t, "config", Getenv("APP_NAME")) + assert.Equal(t, "true", Getenv("APP_DEBUG")) + assert.Equal(t, "defVal", GetEnv("not-exsit", "defVal")) }) - - assert.Equal(t, "config", Getenv("APP_NAME")) - assert.Equal(t, "true", Getenv("APP_DEBUG")) - assert.Equal(t, "defVal", GetEnv("not-exsit", "defVal")) } func TestSetDecoderEncoder(t *testing.T) { diff --git a/dotnev/dotenv.go b/dotnev/dotenv.go index 069a280..0b292bd 100644 --- a/dotnev/dotenv.go +++ b/dotnev/dotenv.go @@ -36,7 +36,7 @@ func LoadedData() map[string]string { // ClearLoaded clear the previously set ENV value func ClearLoaded() { for key := range loadedData { - _= os.Unsetenv(key) + _ = os.Unsetenv(key) } // reset @@ -44,7 +44,7 @@ func ClearLoaded() { } // DontUpperEnvKey dont change key to upper on set ENV -func DontUpperEnvKey() { +func DontUpperEnvKey() { UpperEnvKey = false } diff --git a/dotnev/dotenv_test.go b/dotnev/dotenv_test.go index 3ff9fcb..96c942e 100644 --- a/dotnev/dotenv_test.go +++ b/dotnev/dotenv_test.go @@ -46,7 +46,7 @@ func TestLoadFromMap(t *testing.T) { assert.Equal(t, "", os.Getenv("DONT_ENV_TEST")) err := LoadFromMap(map[string]string{ - "DONT_ENV_TEST": "blog", + "DONT_ENV_TEST": "blog", "dont_env_test1": "val1", "dont_env_test2": "23", }) diff --git a/export_test.go b/export_test.go index 176484b..4deae0c 100644 --- a/export_test.go +++ b/export_test.go @@ -36,3 +36,78 @@ func TestExport(t *testing.T) { _, err = c.DumpTo(buf, JSON) at.Nil(err) } + +func TestConfig_Structure(t *testing.T) { + st := assert.New(t) + + cfg := Default() + cfg.ClearAll() + + err := cfg.LoadStrings(JSON, `{ +"age": 28, +"name": "inhere", +"sports": ["pingPong", "跑步"] +}`) + + st.Nil(err) + + user := &struct { + Age int // always float64 from JSON + Name string + Sports []string + }{} + // map all data + err = MapStruct("", user) + st.Nil(err) + + st.Equal(28, user.Age) + st.Equal("inhere", user.Name) + st.Equal("pingPong", user.Sports[0]) + + // map some data + err = cfg.LoadStrings(JSON, `{ +"sec": { + "key": "val", + "age": 120, + "tags": [12, 34] +} +}`) + st.Nil(err) + + some := struct { + Age int + Kye string + Tags []int + }{} + err = BindStruct("sec", &some) + st.Nil(err) + st.Equal(120, some.Age) + st.Equal(12, some.Tags[0]) + cfg.ClearAll() + + // custom data + cfg = New("test") + err = cfg.LoadData(map[string]interface{}{ + "key": "val", + "age": 120, + "tags": []int{12, 34}, + }) + st.NoError(err) + + s1 := struct { + Age int + Kye string + Tags []int + }{} + err = cfg.BindStruct("", &s1) + st.Nil(err) + st.Equal(120, s1.Age) + st.Equal(12, s1.Tags[0]) + + // key not exist + err = cfg.BindStruct("not-exist", &s1) + st.Error(err) + st.Equal("this key does not exist in the config", err.Error()) + + cfg.ClearAll() +} diff --git a/go.mod b/go.mod index 73f088d..87c3921 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/imdario/mergo v0.3.7 github.com/json-iterator/go v1.1.7 github.com/kr/pretty v0.1.0 // indirect + github.com/mitchellh/mapstructure v1.1.2 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/stretchr/testify v1.3.0 diff --git a/json_driver.go b/json_driver.go index 904be04..14e9ea9 100644 --- a/json_driver.go +++ b/json_driver.go @@ -32,17 +32,17 @@ type jsonDriver struct { ClearComments bool } -// Name +// Name of the driver func (d *jsonDriver) Name() string { return d.name } -// GetDecoder for json +// GetDecoder for the driver func (d *jsonDriver) GetDecoder() Decoder { return JSONDecoder } -// GetEncoder for json +// GetEncoder for the driver func (d *jsonDriver) GetEncoder() Encoder { return JSONEncoder } diff --git a/load.go b/load.go index b93d01a..cf4190f 100644 --- a/load.go +++ b/load.go @@ -74,17 +74,13 @@ func (c *Config) LoadRemote(format, url string) (err error) { } // LoadOSEnv load data from OS ENV -func LoadOSEnv(keys []string, upKeyOnGetenv bool) { dc.LoadOSEnv(keys, upKeyOnGetenv) } +func LoadOSEnv(keys []string) { dc.LoadOSEnv(keys) } // LoadOSEnv load data from os ENV -func (c *Config) LoadOSEnv(keys []string, upKeyOnGetenv bool) { +func (c *Config) LoadOSEnv(keys []string) { for _, key := range keys { - envKey := key - if upKeyOnGetenv { - envKey = strings.ToUpper(key) - } - - val := os.Getenv(envKey) + // os.Getenv() Key is not case sensitive + val := os.Getenv(key) _ = c.Set(key, val) } } diff --git a/load_test.go b/load_test.go index 680fa91..f6b9767 100644 --- a/load_test.go +++ b/load_test.go @@ -5,7 +5,7 @@ import ( "runtime" "testing" - "github.com/gookit/config/v2/dotnev" + "github.com/gookit/goutil/testutil" "github.com/stretchr/testify/assert" ) @@ -180,14 +180,20 @@ func TestLoadFlags(t *testing.T) { func TestLoadOSEnv(t *testing.T) { ClearAll() - _ = dotnev.LoadFromMap(map[string]string{ - "app_name": "config", + testutil.MockEnvValues(map[string]string{ + "APP_NAME": "config", "app_debug": "true", + "test_env0": "val0", + "TEST_ENV1": "val1", + }, func() { + assert.Equal(t, "", String("test_env0")) + + LoadOSEnv([]string{"app_name", "app_debug", "test_env0"}) + assert.True(t, Bool("app_debug")) + assert.Equal(t, "config", String("app_name")) + assert.Equal(t, "val0", String("test_env0")) + assert.Equal(t, "", String("test_env1")) }) - LoadOSEnv([]string{"app_name", "app_debug"}) - assert.True(t, Bool("app_debug")) - assert.Equal(t, "config", String("app_name")) - ClearAll() } diff --git a/read.go b/read.go index 01a2e52..accda3a 100644 --- a/read.go +++ b/read.go @@ -11,7 +11,7 @@ import ( var ( errInvalidKey = errors.New("invalid config key string") - // errNotFound = errors.New("this key does not exist in the configuration data") + errNotFound = errors.New("this key does not exist in the config") ) // Exists key exists check @@ -531,7 +531,7 @@ func (c *Config) StringMap(key string) (mp map[string]string) { for k, v := range typeData { mp[k] = fmt.Sprintf("%v", v) } - case map[interface{}]interface{}: // if decode from yaml + case map[interface{}]interface{}: // decode from yaml mp = make(map[string]string) for k, v := range typeData { sk := fmt.Sprintf("%v", k) @@ -551,43 +551,3 @@ func (c *Config) StringMap(key string) (mp map[string]string) { } return } - -// MapStruct alias method of the 'Structure' -func MapStruct(key string, v interface{}) error { return dc.Structure(key, v) } - -// MapStruct alias method of the 'Structure' -func (c *Config) MapStruct(key string, v interface{}) (err error) { - return c.Structure(key, v) -} - -// Structure get config data and map to a structure. -// usage: -// dbInfo := Db{} -// config.Structure("db", &dbInfo) -func (c *Config) Structure(key string, v interface{}) (err error) { - var ok bool - var data interface{} - - // map all data - if key == "" { - ok = true - data = c.data - } else { - data, ok = c.GetValue(key) - } - - if ok { - blob, err := JSONEncoder(data) - if err != nil { - return err - } - - err = JSONDecoder(blob, v) - } - return -} - -// format key -func formatKey(key string) string { - return strings.Trim(strings.TrimSpace(key), ".") -} diff --git a/read_test.go b/read_test.go index 01577e5..bdea483 100644 --- a/read_test.go +++ b/read_test.go @@ -424,53 +424,3 @@ func TestParseEnv(t *testing.T) { ris.Equal("abc/${ SecondEnv }", cfg.String("ekey4")) }) } - -func TestConfig_MapStructure(t *testing.T) { - st := assert.New(t) - - cfg := Default() - cfg.ClearAll() - - err := cfg.LoadStrings(JSON, `{ -"age": 28, -"name": "inhere", -"sports": ["pingPong", "跑步"] -}`) - - st.Nil(err) - - user := &struct { - Age int - Name string - Sports []string - }{} - // map all - err = MapStruct("", user) - st.Nil(err) - - st.Equal(28, user.Age) - st.Equal("inhere", user.Name) - st.Equal("pingPong", user.Sports[0]) - - // map some - err = cfg.LoadStrings(JSON, `{ -"sec": { - "key": "val", - "age": 120, - "tags": [12, 34] -} -}`) - st.Nil(err) - - some := struct { - Age int - Kye string - Tags []int - }{} - err = cfg.MapStruct("sec", &some) - st.Nil(err) - st.Equal(120, some.Age) - st.Equal(12, some.Tags[0]) - - cfg.ClearAll() -} diff --git a/write.go b/write.go index 6e60f2c..5289e00 100644 --- a/write.go +++ b/write.go @@ -9,6 +9,11 @@ import ( "github.com/imdario/mergo" ) +var ( + readonlyErr = errors.New("the config instance in 'readonly' mode") + keyIsEmptyErr = errors.New("the config key is cannot be empty") +) + // Set val by key func Set(key string, val interface{}, setByPath ...bool) error { return dc.Set(key, val, setByPath...) @@ -18,8 +23,7 @@ func Set(key string, val interface{}, setByPath ...bool) error { func (c *Config) Set(key string, val interface{}, setByPath ...bool) (err error) { // if is readonly if c.opts.Readonly { - err = errors.New("the config instance in 'readonly' mode") - return + return readonlyErr } // open lock @@ -28,8 +32,7 @@ func (c *Config) Set(key string, val interface{}, setByPath ...bool) (err error) key = formatKey(key) if key == "" { - err = errors.New("the config key is cannot be empty") - return + return keyIsEmptyErr } // is top key