Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(syntax-fa): config loader #9

Merged
merged 5 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,5 @@ jobs:
- name: Test
run: make test

- name: Format
run: make format

- name: Lint
run: make lint
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,5 @@ fabric.properties

*log.log
*log.json

*logs.json
2 changes: 2 additions & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
logger:
file_path: "logs/logs.json"
126 changes: 126 additions & 0 deletions config/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Configuration Module Documentation

This Go module provides a singleton configuration object `C()` that encapsulates various configurations essential for your application. The configurations are filled during the package initialization (`init()`) by loading from `config.yml`, `Default()`, and environment variables.

## Usage

### Installation

To use this, import the package in your code:

```go
import "your_module_path/config"
```

### Configuration Options

The package allows setting up configurations in three ways:

1. **Environment Variables**
2. **Default Settings via `Default()`**
3. **Initialization from `config.yml`**

### 1. Using Environment Variables

💡 Use this way for storing `important secrets` that should not be hard coded in repo or in `config.yml` file. For example, `DB_PASSWORD`, or `JWT_SECRET`.

Environment variables are used to configure the application at runtime. The variable names must start with `SYNTAX_`, and nested variables should use `__` as a separator (`SYNTAX_DB__HOST`).

Example setting environment variables:
```go
SYNTAX_LOGGER__FILE_PATH="config.yml" go run main.go
// ... set other environment variables ...
```

### 2. Initialization from `config.yml`

💡 Store variables which are `dependant to the environment` that code is running or the area, the variables that `change more frequent`.

The package supports loading configurations from a YAML file named `config.yml`. Ensure the YAML file is in the correct format.

Example `config.yml` file:
```yaml
debug: true
multi_word_var: "this is a multi-word var in YAML"
db:
username: syntax
password: youcannotbreakme
# ... other configurations ...
```

### 3. Default Settings via `Default()`

💡 Store variables which they have `the least probability of change`.
The `Default()` function in the package allows defining default configurations that act as fallbacks. This function should return a `Config` struct.

Example of defining default configurations:
```go
// Define default configurations
func Default() config.Config {
return config.Config{
// Define your default configurations here
Debug: true,
MultiWordVar: "default value",
Manager: manager.Config{
JWTConfig: auth.JwtConfig{
SecretKey: "the_most_secure_secret_of_all_time",
},
},
// ... other default configurations ...
}
}
```

### Accessing Configuration

#### Accessing the Configuration Singleton

To access the configuration object:

```go
// Get the configuration object
cfg := config.C()
```

### Adding New Configurations

For adding new configurations, update the `Config` struct in the package and ensure it is filled during the initialization process in the `init()` function. Following this, access the updated configuration using the `C()` function.

### Example

Here's an example demonstrating how to access the configuration object and add new configurations:

```go
package main

import (
"fmt"
"your_module_path/config"
)

func main() {
// Access the configuration object
loadedConfig := config.C()

// Access existing configurations
fmt.Println("Debug mode:", loadedConfig.Debug)
fmt.Println("Multi-word var:", loadedConfig.MultiWordVar)
// ... access other configurations ...

// Add new configurations (modify the Config struct in the package)
loadedConfig.NewConfig = "new value"

// Access the newly added configuration
fmt.Println("New Config:", loadedConfig.NewConfig)
}
```

### Important Notes

- The `Config` object, accessed via `C()`, encapsulates all configurations set during the package initialization.
- To add or modify configurations, update the `Config` struct and ensure it is filled during the initialization process (`init()`).
- The `C()` function returns a singleton instance of the configuration object filled by the `init()` function, consolidating settings from `config.yml`, `Default()`, and environment variables.

## Conclusion

This configuration module provides a convenient singleton object encapsulating configurations filled from `config.yml`, `Default()`, and environment variables during initialization. Use `C()` to access the configurations and extend the `Config` struct to incorporate additional settings as needed.
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package config

import "github.com/syntaxfa/syntax-backend/logger"

type Config struct {
Logger logger.Config `koanf:"logger"`
}
126 changes: 126 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package config_test

import (
"embed"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/structs"
"github.com/syntaxfa/syntax-backend/config"
"github.com/syntaxfa/syntax-backend/logger"
"os"
"reflect"
"strings"
"testing"
)

//go:embed test.yml
var configYML embed.FS

type dbConfig struct {
Host string `koanf:"host"`
Password string `koanf:"password"`
User string `koanf:"user"`
}

type structConfig struct {
Debug bool `koanf:"debug"`
FilePath string `koanf:"file_path"`
DB dbConfig `koanf:"db"`
}

const (
prefix = "SYNTAX_"
delimiter = "."
separator = "__"
)

func callBackEnv(source string) string {
base := strings.ToLower(strings.TrimPrefix(source, prefix))

return strings.ReplaceAll(base, separator, delimiter)
}

func TestLoadingDefaultConfigFromStruct(t *testing.T) {
k := koanf.New(delimiter)

testStruct := structConfig{
Debug: true,
FilePath: "config.yml",
DB: dbConfig{
Host: "localhost",
},
}

if err := k.Load(structs.Provider(testStruct, "koanf"), nil); err != nil {
t.Fatalf("error loading default config. error: %s", err.Error())
}

var c structConfig

if err := k.Unmarshal("", &c); err != nil {
t.Fatalf("error marshalling config. error: %s", err.Error())
}

if !reflect.DeepEqual(c, testStruct) {
t.Fatalf("expected: %+v, got: %+v", testStruct, c)
}
}

func TestLoadingConfigFromFile(t *testing.T) {
k := koanf.New(delimiter)

tmpFile, err := os.CreateTemp("", "test.yml")
defer tmpFile.Close()

data, err := configYML.ReadFile("test.yml")
if err != nil {
t.Fatalf("error load config from embedded file, error: %s", err.Error())
}

if _, err := tmpFile.Write(data); err != nil {
t.Fatalf("error write to tmpFile, error: %s", err.Error())
}

if err := k.Load(file.Provider(tmpFile.Name()), yaml.Parser()); err != nil {
t.Fatalf("error load config from file. error: %s", err.Error())
}

var c config.Config

if err := k.Unmarshal("", &c); err != nil {
t.Fatalf("error unmarshalling from file. error: %s", err.Error())
}

want := config.Config{
Logger: logger.Config{FilePath: "logs/logs.json"},
}

if !reflect.DeepEqual(want, c) {
t.Fatalf("expected: %+v, got: %+v", want, c)
}
}

func TestLoadConfigFromEnvironmentVariable(t *testing.T) {
k := koanf.New(".")

os.Setenv("SYNTAX_LOGGER__FILE_PATH", "new_file_path.yml")

if err := k.Load(env.Provider(prefix, delimiter, callBackEnv), nil); err != nil {
t.Fatalf("error environment variables, error: %s", err.Error())
}

var instance config.Config
if err := k.Unmarshal("", &instance); err != nil {
t.Fatalf("error unmarshalling config, error: %s", err.Error())
}

want := config.Config{
Logger: logger.Config{FilePath: "new_file_path.yml"},
}

if !reflect.DeepEqual(want, instance) {
t.Fatalf("expected: %+v, got: %+v", want, instance)
}
}
5 changes: 5 additions & 0 deletions config/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package config

func Default() Config {
return Config{}
}
92 changes: 92 additions & 0 deletions config/loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package config

import (
"fmt"
"strings"

"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/structs"
"github.com/syntaxfa/syntax-backend/logger"
)

// TODO - defaultYamlFilePath has some problem in testing environment ut have normal behavior in build environment.
/*
How can this problem be fixed?
In the test environment, our binary file is located in a different directory.
That's why it can't recognize the config.yml file. To solve this problem, we can embed the config file.
*/
const (
defaultPrefix = "SYNTAX_"
defaultDelimiter = "."
defaultSeparator = "__"
defaultYamlFilePath = "config.yml"
)

var c Config

type Options struct {
Prefix string
Delimiter string
Separator string
YamlFilePath string
CallBackEnV func(string) string
}

func defaultCallBackEnv(source string) string {
fmt.Println("touch default call back env")
base := strings.ToLower(strings.TrimPrefix(source, defaultPrefix))

return strings.ReplaceAll(base, defaultSeparator, defaultDelimiter)
}

func init() {
k := koanf.New(defaultDelimiter)

// load default configuration from Default function
if err := k.Load(structs.Provider(Default(), "koanf"), nil); err != nil {
logger.L().Error("error loading default config", "error", err.Error())
}

// load configuration from yaml file
if err := k.Load(file.Provider(defaultYamlFilePath), yaml.Parser()); err != nil {
logger.L().Error("error loading config from 'config.yml'", "file path", defaultYamlFilePath, "error", err.Error())
}

// load from environment variable
if err := k.Load(env.Provider(defaultPrefix, defaultDelimiter, defaultCallBackEnv), nil); err != nil {
logger.L().Error("error loading environment variables", "error", err.Error())
}

if err := k.Unmarshal("", &c); err != nil {
logger.L().Error("error unmarshalling config", "error", err.Error())
}
}

func C() *Config {
return &c
}

func New(opt Options) *Config {
k := koanf.New(opt.Delimiter)

if err := k.Load(structs.Provider(Default(), "koanf"), nil); err != nil {
logger.L().Error("error loading config from Default()", "error", err.Error())
}

if err := k.Load(file.Provider(opt.YamlFilePath), yaml.Parser()); err != nil {
logger.L().Error("error loading from `config.yml`", "error", err.Error())
}

if err := k.Load(env.Provider(opt.Prefix, opt.Delimiter, opt.CallBackEnV), nil); err != nil {
logger.L().Error("error loading from environment variables", "error", err.Error())
}

if err := k.Unmarshal("", &c); err != nil {
logger.L().Error("error unmarshalling config", "error", err.Error())
}

return &c
}
2 changes: 2 additions & 0 deletions config/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
logger:
file_path: "logs/logs.json"
Loading
Loading