## Configuration ## What does this library? This library provides a simple way to load configuration from different sources. It supports: * [x] Environment variables * [x] Command line flags * [x] Configuration files * [x] JSON * [x] YAML * [x] TOML * [x] Properties * [x] Configuration from a struct * [x] HTTP API (get and set configuration) * [x] Monitor File changes ## Installation ```shell go get gitlab.schukai.com/oss/libraries/go/application/configuration ``` **Note:** This library uses [Go Modules](https://github.com/golang/go/wiki/Modules) to manage dependencies. ## Usage ### Initialize A new configuration is created using the `configuration.New()` function. The passed structure is used type for the configuration. The values are taken as default values of the configuration. ```go package main import ( "fmt" "os" "gitlab.schukai.com/oss/libraries/go/application/configuration" ) func main(){ config := struct { Host string Port int }{ Host: "localhost", Port: 8080, } c := configuration.New(config) fmt.Println(c.Config().Host) fmt.Println(c.Config().Port) } ``` Configuration values can come from different sources. The order of sources is important. #### Environment variables With the `Environment` function, you can load configuration from environment variables. ```go package main import ( "fmt" "os" "gitlab.schukai.com/oss/libraries/go/application/configuration" ) func main(){ config := struct { Host string `env:"HOST"` }{ Host: "localhost", } // Set value os.Setenv("HOST", "www.example.com") s := configuration.New(config) fmt.Println(s.Config().Host) // localhost s.InitFromEnv("") // no prefix fmt.Println(s.Config().Host) // www.example.com } ``` #### Command line flags Obviously, you can also load configuration from command line flags. This library supports the standard library [flag](https://golang.org/pkg/flag/) package. ```go package main import ( "fmt" "os" "flag" "gitlab.schukai.com/oss/libraries/go/application/configuration" ) func main(){ config := struct { Host string `flag:"host"` }{ Host: "localhost", } // Set value flag.String("host", "www.example.com", "help message for host flag") flag.Parse() s := configuration.New(config) s.InitFromFlagSet(flag.CommandLine) fmt.Println(s.Config().Host) // www.example.com } ``` Do you want to allow the user to specify a configuration via the command line, so you can use the `AddFileFromFlagSet` function. This function expects a `flag.FlagSet`. ### Import files and streams You can load configuration from files and streams. With the `Import()` function you import files and streams. The specified files are loaded first, and then the streams. The configurations are merged. If a value is already set, it is overwritten by the specified value. So if a value is set in `etc` and in the more specific File in the user home directory, the value is taken from the user home directory. ```go package main import ( "fmt" "os" "flag" "gitlab.schukai.com/oss/libraries/go/application/configuration" ) func main() { config := struct { Host string }{ Host: "localhost", } c := configuration.New(config) c.SetMnemonic("my-app") c.SetDefaultDirectories() c.Import() fmt.Println(c.Config().Host) } ``` The configuration would then be looked for in the following places: * ~/config.yaml (working directory) * ~/.config/my-app/config.yaml * /etc/my-app/config.yaml #### Configuration files Configuration files are certainly the most common way to define configurations. These can be in different formats. The following formats are currently supported: JSON, YAML, TOML and Properties. With files, the approach is slightly different. Here the function `AddDirectory()` first, specify directories in which to search for the file. The file is then searched for in the order of the directories. If the file is found is loaded. The helper function `AddWorkingDirectory()` adds the current working directory. With `AddEtcDirectory()` the directory `etc` is added under Unix. `AddUserConfigDirectory()` adds the directory `~.config` on Unix. It will uses the `os.UserConfigDir()` method. Thus, it is possible to use the directory can also be used on other operating systems. The `SetDefaultDirectories()` method sets the paths to the default values. Become internal the paths with `AddWorkingDirectory()`, `AddEtcDirectory()` and `AddUserConfigDirectory()` set. The directory structure can be specified directly with the `SetDirectories()` function. This overwrites the directory structure specified with `AddDirectory()`. The filename of the configuration file is specified with `SetFileName()`. The file name is searched for with the extension `.json`, `.yaml`, `.yml`, `.toml` or `.properties`. If a format is specified with the `SetFileFormat()` method, only files with searched for this ending. As an extra, you can specify your own file system with `SetFilesystem()`. This is useful for testing. Furthermore, the `AddFile()` function can be used to add a file directly. This makes sense if you are given a file as a parameter or if you don't want to have a multi-level structure. **Watch files** If you want to watch configuration files for changes, you can use the `Watch()` function. With the function `StopWatch()` you can stop watching the files. The function returns a bool channel on which the status of the watch is reported. If the watch has ended, a true is sent over the channel. If an error occurs, a false is sent. #### Streams The configuration can also be loaded from a stream. This is useful if you want to load the configuration from other sources. For example, from a database. With the `AddReader()` function, you can add a stream to the configuration. The stream is then searched for in the order of the streams. If the stream is found is loaded. ### HTTP API The configuration can also be changed via HTTP. This is useful if you want to change the configuration at runtime. For example, if you would like to change the configuration of a service. With a Get request, the configuration is returned as the format specified in the `Accept` header. If no format is specified, the configuration is returned in JSON format. With a Post request, the configuration can be changed. The configuration is then taken from the body of the request. The format is determined by the `Content-Type` header. If no format is specified, the configuration is taken from the JSON format. ```go config := struct { Host string } s := New(config) mux := http.NewServeMux() mux.HandleFunc("/config", s.ServeHTTP) ``` There is also a middleware to get access to the configuration. ```go config := struct { Host string } s := New(config) mux := http.NewServeMux() mux.Use(s.Middleware) ``` The configuration can then be accessed via the `config` context. ```go func handler(w http.ResponseWriter, r *http.Request) { config := r.Context().Value("config").(struct { Host string }) fmt.Println(config.Host) } ``` ### On change If you want to be notified when the configuration changes, you can use the `OnChange()` function. This function takes a callback function as a parameter. The following program gives the following output `Change from localhost to www.example.com`. ```go package main import ( "fmt" "os" "flag" "gitlab.schukai.com/oss/libraries/go/application/configuration" ) func main() { config := struct { Host string }{ Host: "localhost", } s := configuration.New(config) closeChan := make(chan bool) s.OnChange(func(event configuration.ChangeEvent) { log := event.Changlog msg := fmt.Sprintf("Change from %s to %s", log[0].From, log[0].To) fmt.Println(msg) closeChan <- true }) c := s.Config() c.Host = "www.example.com" s.SetConfig(c) // Wait for change select { case <-closeChan: } } ``` ### Error handling If an error occurs, it is returned by the function `Errors()`. The errors can be handled as usual. The `HasErrors()` function can be used to check whether errors have occurred. ## Contributing Merge requests are welcome. For major changes, please open an issue first to discuss what you would like to change. **Please make sure to update tests as appropriate.** Versioning is done with [SemVer](https://semver.org/). Changelog is generated with [git-chglog](https://github.com/git-chglog/git-chglog#git-chglog) Commit messages should follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification. Messages are started with a type, which is one of the following: - **feat**: A new feature - **fix**: A bug fix - **doc**: Documentation only changes - **refactor**: A code change that neither fixes a bug nor adds a feature - **perf**: A code change that improves performance - **test**: Adding missing or correcting existing tests - **chore**: Other changes that don't modify src or test files The footer would be used for a reference to an issue or a breaking change. A commit that has a footer `BREAKING CHANGE:`, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in semantic versioning). A BREAKING CHANGE can be part of commits of any type. the following is an example of a commit message: ```text feat: add 'extras' field ``` ## License [AGPL-3.0](https://choosealicense.com/licenses/agpl-3.0/)