diff --git a/.gitignore b/.gitignore index ea34f98..ca9bf51 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ +# Example test related files +example* diff --git a/README.md b/README.md index 10afca5..f5a5a12 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ This package was made, to easily get needed settings from a file. -- Supported file types are: **'json'** and **'yaml'**. +- Supported file types are: **'json'**, **'yaml'** and **'ini'**. - The configuration keys are **case insensitive**. This package uses [github.com/spf13/viper](https://github.com/spf13/viper) @@ -20,8 +20,10 @@ This package uses [github.com/spf13/viper](https://github.com/spf13/viper) * [Initialize settings from a given content](#initialize-settings-from-a-given-content) * [Get all keys from the settings](#get-all-keys-from-the-settings) * [Get all settings](#get-all-settings) - * [Add prefix](#add-prefix) + * [Add a sub tree](#add-a-sub-tree) * [Type assertions](#type-assertions) + * [Reload the settings data manually](#reload-the-settings-data-manually) + * [Automatic reload the settings data in the background](#automatic-reload-the-settings-data-in-the-background) ## Example usage @@ -62,7 +64,7 @@ other: string: 'text' bool: true ` -sm := settings.New(NewFromSource) +sm := settings.NewFromContent(content) ``` [Back to top](#table-of-contents) @@ -79,12 +81,14 @@ other: string: 'text' bool: true ` -sm := settings.New(NewFromSource) +sm := settings.NewFromContent(content) keys, err := sm.GetAllKeys() if err != nil { log.Fatal(err) } +// Output: +// 2020/02/02 15:53:41 allSettings map[other:map[content:map[bool:true int:1 string:text]]] log.Println("keys", keys) // Output: @@ -105,7 +109,7 @@ other: string: 'text' bool: true ` -sm := settings.New(NewFromSource) +sm := settings.NewFromContent(content) allSettings, err := sm.GetAllSettings() if err != nil { log.Fatal(err) @@ -119,10 +123,10 @@ log.Println("allSettings", allSettings) [Back to top](#table-of-contents) -### Add prefix +### Add a sub tree Returning a new settings instance representing a sub tree of this instance. -AddPrefix is case-insensitive for a key. +SubTree is case-insensitive for a key. ```go var content = ` @@ -132,15 +136,16 @@ a: value: 1 ` -sm := settings.NewFromSource(content) +sm := settings.NewFromContent(content) -sm = sm.AddPrefix("a.b") +sm = sm.SubTree("a.b") intValue, err := sm.GetInt("c.value") if err != nil { log.Fatal(err) } - +// Output: +// 2020/02/02 15:52:55 allSettings 1 log.Println("allSettings", intValue) ``` @@ -165,3 +170,116 @@ timeDurationKey, err := sm.GetDuration("time.duration.key") ``` [Back to top](#table-of-contents) + +### Reload the settings data manually + +Re-read the settings data by calling the Reload function. + +```go +content := ` +config: + config_key: config_value` + +err := ioutil.WriteFile("./example/settings/example_config.yaml", []byte(content), os.ModePerm) +if err != nil { + log.Fatal(err) +} + +sm := settings.New("./example/settings/example_config.yaml") + +v, err := sm.Get("config.config_key") +if err != nil { + log.Fatal(err) +} + +// Output: +// 2020/02/02 15:31:58 config_value +log.Println(v) + +content = strings.ReplaceAll(content, "config_key: config_value", "foo: bar") +err = ioutil.WriteFile("./example/settings/example_config.yaml", []byte(content), os.ModePerm) +if err != nil { + log.Fatal(err) +} + +// Reload the configuration ... +sm.Reload() + +v, err = sm.Get("config.foo") +if err != nil { + log.Fatal(err) +} + +// Output: +// 2020/02/02 15:31:58 bar +log.Println(v) +``` + +[Back to top](#table-of-contents) + +### Automatic reload the settings data in the background + +AutoReload is watching for settings file changes in the background and reloads configuration if needed. + +```go +content := ` +config: + config_key: config_value` + +err := ioutil.WriteFile("./example/settings/example_config.yaml", []byte(content), os.ModePerm) +if err != nil { + log.Fatal(err) +} + +sm := settings.New("./example/settings/example_config.yaml") + +// Activate the automatic reload function ... +sm.AutoReload() + +v, err := sm.Get("config.config_key") +if err != nil { + log.Fatal(err) +} + +// Output: +// 2020/02/02 15:42:49 config_value +log.Println(v) + +content = strings.ReplaceAll(content, "config_key: config_value", "foo: bar") +err = ioutil.WriteFile("./example/settings/example_config.yaml", []byte(content), os.ModePerm) +if err != nil { + log.Fatal(err) +} + +time.Sleep(5 * time.Millisecond) + +v, err = sm.Get("config.foo") +if err != nil { + log.Fatal(err) +} + +// Output: +// 2020/02/02 15:42:49 settings.AutoReload settings reloaded +// 2020/02/02 15:42:49 bar +log.Println(v) + +content = strings.ReplaceAll(content, "foo: bar", "config_key: config_value") +err = ioutil.WriteFile("./example/settings/example_config.yaml", []byte(content), os.ModePerm) +if err != nil { + log.Fatal(err) +} + +time.Sleep(5 * time.Millisecond) + +v, err = sm.Get("config.config_key") +if err != nil { + log.Fatal(err) +} + +// Output: +// 2020/02/02 15:42:49 settings.AutoReload settings reloaded +// 2020/02/02 15:42:49 config_value +log.Println(v) +``` + +[Back to top](#table-of-contents) diff --git a/cast_example_test.go b/cast_example_test.go new file mode 100644 index 0000000..9d4494e --- /dev/null +++ b/cast_example_test.go @@ -0,0 +1,263 @@ +package settings_test + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + "github.com/takattila/settings-manager" +) + +func ExampleSettings_Get() { + file := "example_app1.yaml" + content := `{ "app": { "string": "value", "int": 1 } }` + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.Get("app.string") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + v, err = sm.Get("app.int") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // Output: + // value + // 1 +} + +func ExampleSettings_GetBool() { + file := "example_app1.yaml" + content := "app:\n bool: true" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetBool("app.bool") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("value: %t, type: %T", v, v)) + + // Output: value: true, type: bool +} + +func ExampleSettings_GetFloat64() { + file := "example_app1.yaml" + content := "app:\n float64: 123131232132113211564564456" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetFloat64("app.float64") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("value: %b, type: %T", v, v)) + + // Output: value: 7167181007803488p+34, type: float64 +} + +func ExampleSettings_GetInt() { + file := "example_app1.yaml" + content := "app:\n int: 100" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetInt("app.int") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("value: %d, type: %T", v, v)) + + // Output: value: 100, type: int +} + +func ExampleSettings_GetIntSlice() { + file := "example_app1.yaml" + content := `{ "app": { "int_slice": [ 1, 2 ] } }` + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetIntSlice("app.int_slice") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("value: %d, type: %T", v, v)) + + // Output: value: [1 2], type: []int +} + +func ExampleSettings_GetString() { + file := "example_app1.yaml" + content := "app:\n string: some text" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetString("app.string") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("value: %s, type: %T", v, v)) + + // Output: value: some text, type: string +} + +func ExampleSettings_GetStringMap() { + file := "example_app1.yaml" + content := `{ "app": { "string_map": { "one": "value1", "two": "value2" } } }` + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetStringMap("app.string_map") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("type: %T", v)) + fmt.Println(fmt.Sprintf("app.string_map.one: %s", v["one"])) + fmt.Println(fmt.Sprintf("app.string_map.two: %s", v["two"])) + + // Output: + // type: map[string]interface {} + // app.string_map.one: value1 + // app.string_map.two: value2 +} + +func ExampleSettings_GetStringMapString() { + file := "example_app1.yaml" + content := `{ "app": { "string_map": { "one": "value1", "two": "value2" } } }` + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetStringMapString("app.string_map") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("type: %T", v)) + fmt.Println(fmt.Sprintf("app.string_map.one: %s", v["one"])) + fmt.Println(fmt.Sprintf("app.string_map.two: %s", v["two"])) + + // Output: + // type: map[string]string + // app.string_map.one: value1 + // app.string_map.two: value2 +} + +func ExampleSettings_GetStringSlice() { + file := "example_app1.yaml" + content := `{ "app": { "string_slice": [ "value1", "value2" ] } }` + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetStringSlice("app.string_slice") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("app.string_slice: %s, type: %T", v, v)) + + // Output: + // app.string_slice: [value1 value2], type: []string +} + +func ExampleSettings_GetTime() { + file := "example_app1.yaml" + content := "time:\n seconds: 10" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetTime("time.seconds") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("time.seconds: %d, type: %T", v.Second(), v)) + + // Output: + // time.seconds: 10, type: time.Time +} + +func ExampleSettings_GetDuration() { + file := "example_app1.yaml" + content := "time:\n duration: 10" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.GetDuration("time.duration") + if err != nil { + log.Fatal(err) + } + + fmt.Println(fmt.Sprintf("time.duration: %d, type: %T", v, v)) + + // Output: + // time.duration: 10, type: time.Duration +} diff --git a/go.mod b/go.mod index 2fbb46b..84226d3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/takattila/settings-manager go 1.13 require ( + github.com/fsnotify/fsnotify v1.4.7 + github.com/go-chi/chi v4.0.3+incompatible github.com/spf13/viper v1.6.2 github.com/stretchr/testify v1.4.0 gopkg.in/yaml.v2 v2.2.8 diff --git a/go.sum b/go.sum index e2444b3..d4af4de 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= +github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= diff --git a/helpers.go b/helpers.go index 2f54888..7783333 100644 --- a/helpers.go +++ b/helpers.go @@ -5,14 +5,32 @@ import ( "encoding/json" "fmt" "io/ioutil" + "log" "os" "path/filepath" "reflect" "strings" + "github.com/fsnotify/fsnotify" + "gopkg.in/yaml.v2" ) +type supportedExtension string + +const ( + jsonExtension supportedExtension = ".json" + yamlExtensionLong supportedExtension = ".yaml" + yamlExtensionShort supportedExtension = ".yml" +) + +var triggerReload = func(s *Settings) { + s.Data.OnConfigChange(func(in fsnotify.Event) { + s.Reload() + log.Println("settings.AutoReload", "settings reloaded") + }) +} + func (s *Settings) load(settingsFile string) *Settings { if isDirectory(settingsFile) { for _, file := range listFilesUnderDirectory(settingsFile) { @@ -26,10 +44,12 @@ func (s *Settings) load(settingsFile string) *Settings { return &Settings{Error: err} } s.Data.SetConfigType(getExtensionByFileName(settingsFile)) + err = s.Data.MergeConfig(bytes.NewBuffer(b)) if err != nil { return &Settings{Error: err} } + s.appendFileName(settingsFile) } return s } @@ -95,7 +115,9 @@ func (s *Settings) checkIntSlice(key string) error { func listFilesUnderDirectory(dir string) (files []string) { _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if !isDirectory(path) { - files = append(files, path) + if supportedExtension(filepath.Ext(path)).validateExtension() { + files = append(files, path) + } } return nil }) @@ -128,3 +150,30 @@ func isDirectory(path string) bool { } return false } + +func (s *Settings) appendFileName(fileName string) { + s.fileNames = append(s.fileNames, filepath.Clean(fileName)) + s.fileNames = makeUniqueSlice(s.fileNames) +} + +func makeUniqueSlice(s []string) []string { + unique := make(map[string]bool, len(s)) + us := make([]string, len(unique)) + for _, elem := range s { + if len(elem) != 0 { + if !unique[elem] { + us = append(us, elem) + unique[elem] = true + } + } + } + return us +} + +func (e supportedExtension) validateExtension() bool { + switch e { + case jsonExtension, yamlExtensionLong, yamlExtensionShort: + return true + } + return false +} diff --git a/helpers_test.go b/helpers_test.go index b4709d4..7dddd36 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -1,12 +1,14 @@ package settings import ( + "bufio" "fmt" "io/ioutil" "log" "os" "reflect" "testing" + "time" "github.com/spf13/viper" "github.com/stretchr/testify/suite" @@ -18,7 +20,26 @@ type ( } ) -func (u unitHelpersSuite) TestHelpersInit() { +func (u unitHelpersSuite) TestTriggerReload() { + initTestOk() + + sm := New(testYamlFilePAth) + sm.Data.SetConfigFile(testYamlFilePAth) + sm.Data.WatchConfig() + + saveFileHelper(u, testYamlFilePAth, testYamlContent) + triggerReload(sm) + + time.Sleep(10 * time.Millisecond) + + v, err := sm.Get("service.name") + u.Equal(nil, err) + u.Equal("ExampleService", v) + + resetTest() +} + +func (u unitHelpersSuite) TestLoad() { initTestOk() err := os.Chmod(testYamlFilePAth, os.ModeExclusive) @@ -33,7 +54,7 @@ func (u unitHelpersSuite) TestHelpersInit() { resetTest() } -func (u unitHelpersSuite) TestHelpersCheckErrors() { +func (u unitHelpersSuite) TestCheckErrors() { initTestOk() s := Settings{} @@ -45,7 +66,7 @@ func (u unitHelpersSuite) TestHelpersCheckErrors() { resetTest() } -func (u unitHelpersSuite) TestHelpersCheckType() { +func (u unitHelpersSuite) TestCheckType() { initTestOk() s := Settings{} @@ -57,7 +78,7 @@ func (u unitHelpersSuite) TestHelpersCheckType() { resetTest() } -func (u unitHelpersSuite) TestHelpersCheck() { +func (u unitHelpersSuite) TestCheck() { initTestOk() s := Settings{} @@ -69,7 +90,7 @@ func (u unitHelpersSuite) TestHelpersCheck() { resetTest() } -func (u unitHelpersSuite) TestHelpersIsDirectory() { +func (u unitHelpersSuite) TestIsDirectory() { initTest() chk := isDirectory(testFilePAth) @@ -84,10 +105,46 @@ func (u unitHelpersSuite) TestHelpersIsDirectory() { resetTest() } +func (u unitHelpersSuite) TestValidateExtension() { + ext := jsonExtension + supported := ext.validateExtension() + u.Equal(true, supported) + + ext = yamlExtensionLong + supported = ext.validateExtension() + u.Equal(true, supported) + + ext = yamlExtensionShort + supported = ext.validateExtension() + u.Equal(true, supported) + + ext = ".ini" + supported = ext.validateExtension() + u.Equal(false, supported) +} + func TestHelperUnitSuite(t *testing.T) { suite.Run(t, new(unitHelpersSuite)) } +func saveFileHelper(u unitHelpersSuite, path, content string) { + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + u.Equal(nil, err) + + defer func() { + _ = file.Close() + }() + + // new writer w/ default 4096 buffer size + w := bufio.NewWriter(file) + + _, err = w.WriteString(content + "\n") + u.Equal(nil, err) + + err = w.Flush() + u.Equal(nil, err) +} + func initTest() { resetTest() initTestOk() @@ -206,10 +263,10 @@ environment: testYamlContentOther = ` other: - content: - int: 1 - string: 'text' - bool: true + content: + int: 1 + string: 'text' + bool: true ` testJSONContent = ` @@ -343,11 +400,11 @@ other: testYamlFilePAth = "./settings/test.yaml" - testJsonlFilePAth = "./settings/test.json" + testJsonFilePAth = "./settings/test.json" - testJsonlFileOtherPAth = "./settings/other.json" + testJsonFileOtherPAth = "./settings/other.json" - testYamllFileOtherPAth = "./settings/other.yaml" + testYamlFileOtherPAth = "./settings/other.yaml" testDirPath = "./settings" ) diff --git a/settings.go b/settings.go index 4f19fa8..4f78faa 100644 --- a/settings.go +++ b/settings.go @@ -1,5 +1,5 @@ // This package was made, to easily get needed settings from a file. -// Supported file types are: **'*.json'** and **'*.yaml'**. +// Supported file types are: json and yaml. // // This package uses https://github.com/spf13/viper: Copyright © 2014 Steve Francia . package settings @@ -7,13 +7,23 @@ package settings import ( "bytes" "fmt" + "sync" + "github.com/fsnotify/fsnotify" "github.com/spf13/viper" ) +type fsNotify struct { + watcher *fsnotify.Watcher + error error +} + type Settings struct { - Data *viper.Viper - Error error + Data *viper.Viper + Error error + content string + fileNames []string + mux sync.Mutex } // New initializes settings from a file or from multiple files under given directory. @@ -23,13 +33,13 @@ func New(settingsFile string) *Settings { return s.load(settingsFile) } -// NewFromSource initializes settings from a given content. -func NewFromSource(content string) *Settings { - s := &Settings{} +// NewFromContent initializes settings from a given content. +func NewFromContent(content string) *Settings { + s := &Settings{content: content} s.Data = viper.New() ext := getExtensionByContent(content) if ext == "unsupported" { - return &Settings{Error: fmt.Errorf("settings.NewFromSource :: unsupported content type")} + return &Settings{Error: fmt.Errorf("settings.NewFromContent :: unsupported content type")} } s.Data.SetConfigType(ext) _ = s.Data.ReadConfig(bytes.NewBuffer([]byte(content))) @@ -58,9 +68,45 @@ func (s *Settings) GetAllSettings() (map[string]interface{}, error) { return s.Data.AllSettings(), nil } -// AddPrefix returns a new settings instance representing a sub tree of this instance. -// AddPrefix is case-insensitive for a key. -func (s *Settings) AddPrefix(prefix string) *Settings { +// SubTree returns a new settings instance representing a sub tree of this instance. +// SubTree is case-insensitive for a key. +func (s *Settings) SubTree(prefix string) *Settings { s.Data = s.Data.Sub(prefix) return s } + +// GetSettingsFileNames returns the name of all settings files, whence settings manager was initialized. +func (s *Settings) GetSettingsFileNames() ([]string, error) { + if s.Error != nil { + return nil, fmt.Errorf("settings.GetSettingsFileNames :: %s", s.Error) + } + return s.fileNames, nil +} + +// Reload once it's called, will re-read the settings data. +func (s *Settings) Reload() { + s.mux.Lock() + + content := s.content + s.Data = viper.New() + + if content != "" { + s.Data = NewFromContent(content).Data + } + + for _, fileName := range s.fileNames { + s.Data = s.load(fileName).Data + } + + s.mux.Unlock() +} + +// AutoReload watching for settings file changes in the background +// and reloads configuration if needed. +func (s *Settings) AutoReload() { + for _, fileName := range s.fileNames { + s.Data.SetConfigFile(fileName) + s.Data.WatchConfig() + triggerReload(s) + } +} diff --git a/settings_example_test.go b/settings_example_test.go new file mode 100644 index 0000000..a993018 --- /dev/null +++ b/settings_example_test.go @@ -0,0 +1,258 @@ +package settings_test + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + "time" + + "github.com/takattila/settings-manager" +) + +func ExampleNew() { + file := "example_config.yaml" + content := "config:\n key: value" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + AllSettings, err := sm.GetAllSettings() + if err != nil { + log.Fatal(err) + } + + fmt.Println(AllSettings) + + // Output: map[config:map[key:value]] +} + +func ExampleNewFromContent() { + content := "config:\n key: value" + + sm := settings.NewFromContent(content) + + AllSettings, err := sm.GetAllSettings() + if err != nil { + log.Fatal(err) + } + + fmt.Println(AllSettings) + + // Output: map[config:map[key:value]] +} + +func ExampleSettings_Merge() { + file1 := "example_app1.yaml" + content1 := "app1:\n key: value1" + + err := ioutil.WriteFile(file1, []byte(content1), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + file2 := "example_app2.yaml" + content2 := "app2:\n key: value2" + + err = ioutil.WriteFile(file2, []byte(content2), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file1).Merge(file2) + + k, err := sm.Get("app1.key") + if err != nil { + log.Fatal(err) + } + + fmt.Println(k) + + k, err = sm.Get("app2.key") + if err != nil { + log.Fatal(err) + } + + fmt.Println(k) + + // Output: + // value1 + // value2 +} + +func ExampleSettings_GetAllKeys() { + content := "config:\n key: value" + + sm := settings.NewFromContent(content) + keys, err := sm.GetAllKeys() + if err != nil { + log.Fatal(err) + } + + fmt.Println(keys) + + // Output: [config.key] +} + +func ExampleSettings_GetAllSettings() { + content := "config:\n key: value" + + sm := settings.NewFromContent(content) + AllSettings, err := sm.GetAllSettings() + if err != nil { + log.Fatal(err) + } + + fmt.Println(AllSettings) + + // Output: map[config:map[key:value]] +} + +func ExampleSettings_SubTree() { + content := `{ "config": { "sub": { "tree": "value" } } }` + + sm := settings.NewFromContent(content) + sm = sm.SubTree("config.sub") + + v, err := sm.Get("tree") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // Output: value +} + +func ExampleSettings_GetSettingsFileNames() { + file1 := "example_app1.yaml" + content1 := "app1:\n key: value1" + + err := ioutil.WriteFile(file1, []byte(content1), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + file2 := "example_app2.yaml" + content2 := "app2:\n key: value2" + + err = ioutil.WriteFile(file2, []byte(content2), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file1).Merge(file2) + + files, err := sm.GetSettingsFileNames() + if err != nil { + log.Fatal(err) + } + + fmt.Println(files) + + // Output: [example_app1.yaml example_app2.yaml] +} + +func ExampleSettings_Reload() { + file := "example_config.yaml" + content := "config:\n key: value" + + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + v, err := sm.Get("config.key") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + content = strings.ReplaceAll(content, "key: value", "foo: bar") + err = ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + // Reload the configuration ... + sm.Reload() + + v, err = sm.Get("config.foo") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // Output: + // value + // bar +} + +func ExampleSettings_AutoReload() { + file := "example_config.yaml" + content := "config:\n key: value" + + // Save content + err := ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + sm := settings.New(file) + + // Activate the automatic reload function ... + sm.AutoReload() + + v, err := sm.Get("config.key") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // First update of the content + content = strings.ReplaceAll(content, "key: value", "foo: bar") + err = ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + time.Sleep(5 * time.Millisecond) + + v, err = sm.Get("config.foo") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // Second update of the content + content = strings.ReplaceAll(content, "foo: bar", "key: value") + err = ioutil.WriteFile(file, []byte(content), os.ModePerm) + if err != nil { + log.Fatal(err) + } + + time.Sleep(5 * time.Millisecond) + + v, err = sm.Get("config.key") + if err != nil { + log.Fatal(err) + } + + fmt.Println(v) + + // Output: + // value + // bar + // value +} diff --git a/settings_test.go b/settings_test.go index 4a4b34c..33ce9be 100644 --- a/settings_test.go +++ b/settings_test.go @@ -1,10 +1,14 @@ package settings import ( + "bufio" "fmt" "io/ioutil" "os" + "strings" + "sync" "testing" + "time" "github.com/stretchr/testify/suite" ) @@ -18,14 +22,14 @@ type ( func (u unitConfSuite) TestInitFromSource() { initTestOk() - sm := NewFromSource(testYamlContent) + sm := NewFromContent(testYamlContent) u.Equal(nil, sm.Error) - sm = NewFromSource(testJSONContent) + sm = NewFromContent(testJSONContent) u.Equal(nil, sm.Error) - sm = NewFromSource(testBadYamlContent) - u.Equal("settings.NewFromSource :: unsupported content type", fmt.Sprint(sm.Error)) + sm = NewFromContent(testBadYamlContent) + u.Equal("settings.NewFromContent :: unsupported content type", fmt.Sprint(sm.Error)) resetTest() } @@ -45,10 +49,10 @@ func (u unitConfSuite) TestInit() { func (u unitConfSuite) TestMerge() { initTestOk() - err := ioutil.WriteFile(testJsonlFileOtherPAth, []byte(testJSONContentOther), os.ModePerm) + err := ioutil.WriteFile(testJsonFileOtherPAth, []byte(testJSONContentOther), os.ModePerm) u.Equal(nil, err) - sm := New(testYamlFilePAth).Merge(testJsonlFileOtherPAth) + sm := New(testYamlFilePAth).Merge(testJsonFileOtherPAth) u.Equal(nil, sm.Error) v, err := sm.Get("other.content.string") @@ -66,10 +70,10 @@ func (u unitConfSuite) TestGetAllKeys() { err := os.Mkdir(testDirPath, os.ModePerm) u.Equal(nil, err) - err = ioutil.WriteFile(testYamllFileOtherPAth, []byte(testYamlContentOther), os.ModePerm) + err = ioutil.WriteFile(testYamlFileOtherPAth, []byte(testYamlContentOther), os.ModePerm) u.Equal(nil, err) - sm := New(testYamllFileOtherPAth) + sm := New(testYamlFileOtherPAth) u.Equal(nil, sm.Error) v, err := sm.GetAllKeys() @@ -95,10 +99,10 @@ func (u unitConfSuite) TestGetAllSettings() { err := os.Mkdir(testDirPath, os.ModePerm) u.Equal(nil, err) - err = ioutil.WriteFile(testJsonlFileOtherPAth, []byte(testJSONContentOther), os.ModePerm) + err = ioutil.WriteFile(testJsonFileOtherPAth, []byte(testJSONContentOther), os.ModePerm) u.Equal(nil, err) - sm := New(testJsonlFileOtherPAth) + sm := New(testJsonFileOtherPAth) u.Equal(nil, sm.Error) v, err := sm.GetAllSettings() @@ -126,13 +130,115 @@ func (u unitConfSuite) TestGetAllSettings() { func (u unitConfSuite) TestAddPrefix() { initTestOk() - s, err := New(testYamlFilePAth).AddPrefix("a.b").GetAllSettings() + s, err := New(testYamlFilePAth).SubTree("a.b").GetAllSettings() u.Equal(nil, err) u.Equal(map[string]interface{}{"c": []interface{}{1, 2, 0, 4}}, s) resetTest() } +func (u unitConfSuite) TestGetSettingsFileNames() { + initTestOk() + + fileNames, err := New(testYamlFilePAth).GetSettingsFileNames() + u.Equal(nil, err) + u.Equal([]string{"settings/test.yaml"}, fileNames) + + err = os.Chmod(testYamlFilePAth, os.ModeExclusive) + u.Equal(nil, err) + + _, err = New(testYamlFilePAth).GetSettingsFileNames() + u.Equal(`settings.GetSettingsFileNames :: open ./settings/test.yaml: permission denied`, fmt.Sprint(err)) + + err = os.Chmod(testYamlFilePAth, os.ModePerm) + u.Equal(nil, err) + + resetTest() +} + +func (u unitConfSuite) TestReload() { + initTest() + + sm := New(testYamlFilePAth) + + oldValue := `name: ExampleService` + newValue := `name: NewApp` + + content := strings.ReplaceAll(testYamlContent, oldValue, newValue) + saveFile(u, testYamlFilePAth, content) + + sm.Reload() + + v, err := sm.Get("service.name") + u.Equal(nil, err) + u.Equal("NewApp", v) + + resetTest() +} + +func (u unitConfSuite) TestAutoReload() { + initTest() + + err := ioutil.WriteFile(testYamlFileOtherPAth, []byte(testYamlContentOther), os.ModePerm) + u.Equal(nil, err) + + wg := sync.WaitGroup{} + wg.Add(1) + + tmpTriggerReload := triggerReload + triggerReload = func(s *Settings) { + go func() { + for { + s.Reload() + time.Sleep(10 * time.Millisecond) + + wg.Done() + return + } + }() + } + + sm := NewFromContent(testYamlContent).Merge(testYamlFileOtherPAth) + sm.AutoReload() + + oldValue := `int: 1` + newValue := `int: 1000` + + content := strings.ReplaceAll(testYamlContentOther, oldValue, newValue) + saveFile(u, testYamlFileOtherPAth, content) + wg.Wait() + + v, err := sm.Get("other.content.int") + u.Equal(nil, err) + u.Equal(1000, v) + + v, err = sm.Get("service.name") + u.Equal(nil, err) + u.Equal("ExampleService", v) + + resetTest() + + triggerReload = tmpTriggerReload +} + +func saveFile(u unitConfSuite, path, content string) { + file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + u.Equal(nil, err) + + defer func() { + _ = file.Close() + }() + + // new writer w/ default 4096 buffer size + w := bufio.NewWriter(file) + + _, err = w.WriteString(content + "\n") + u.Equal(nil, err) + + err = w.Flush() + u.Equal(nil, err) +} + func TestConfUnitSuite(t *testing.T) { suite.Run(t, new(unitConfSuite)) }