## 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/)