Skip to content

Commit

Permalink
feat(config): #13 - config handling using koanf package (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
DOO-DEV authored Nov 28, 2023
1 parent ea82e62 commit 5e50685
Show file tree
Hide file tree
Showing 8 changed files with 640 additions and 1 deletion.
1 change: 1 addition & 0 deletions config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Empty file removed config/.gitkeep
Empty file.
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package config

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

import (
"os"
"reflect"
"strings"
"testing"

"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"
)

type structConfig struct {
Debug bool `koanf:"debug"`
MultiWordVar string `koanf:"multi_word_var"`
DB db `koanf:"db"`
}

type db struct {
Host string `koanf:"host"`
Username string `koanf:"username"`
Password string `koanf:"password"`
MultiWordNestedVar string `koanf:"multi_word_nested_var"`
NestedMultiWordConfig nestedMultiWordConfig `koanf:"nested_multi_word_config"`
}

type nestedMultiWordConfig struct {
DownHere string `koanf:"down_here"`
}

const (
prefix = "ORMUS_"
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(".")

testStruct := structConfig{
Debug: false,
MultiWordVar: "Im complex in default",
DB: db{
Host: "localhost",
Username: "hossein",
Password: "1234",
MultiWordNestedVar: "Oh this is too long",
},
}

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

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

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

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

ymlConfigTest := []byte(`debug: false
multi_word_var: "I'm complex in config.yml"
db:
host: "localhost"
username: "ali"
password: "passwd"
multi_word_nested_var: "WHAT??"`)

ymlFile, _ := os.Create("test.yml")
defer ymlFile.Close()
defer os.Remove("test.yml")
ymlFile.Write(ymlConfigTest)
// load configuration from yaml file
if err := k.Load(file.Provider("test.yml"), yaml.Parser()); err != nil {
t.Logf("error loading config from `config.yml` file: %s", err)
}

want := structConfig{
Debug: false,
MultiWordVar: "I'm complex in config.yml",
DB: db{
Host: "localhost",
Username: "ali",
Password: "passwd",
MultiWordNestedVar: "WHAT??",
},
}

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

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

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

os.Setenv("ORMUS_DEBUG", "false")
os.Setenv("ORMUS_MULTI_WORD_VAR", "this is multi word var")
os.Setenv("ORMUS_DB__HOST", "localhost")
os.Setenv("ORMUS_DB__USERNAME", "hossein")
os.Setenv("ORMUS_DB__PASSWORD", "1234")
os.Setenv("ORMUS_DB__MULTI_WORD_NESTED_VAR", "testing make it easy (:")
os.Setenv("ORMUS_DB__NESTED_MULTI_WORD_CONFIG__DOWN_HERE", "im here")

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

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

want := structConfig{
Debug: false,
MultiWordVar: "this is multi word var",
DB: db{
Host: "localhost",
Username: "hossein",
Password: "1234",
MultiWordNestedVar: "testing make it easy (:",
NestedMultiWordConfig: nestedMultiWordConfig{DownHere: "im here"},
},
}
if !reflect.DeepEqual(instance, want) {
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{}
}
85 changes: 84 additions & 1 deletion config/loader.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,86 @@
package config

// TODO: use Viper/Koanf - the config loader should support default values, json/yaml config files and finally overwrite them with environment variables
import (
"log"
"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"
)

const (
defaultPrefix = "ORMUS_"
defaultDelimiter = "."
defaultSeparator = "__"
defaultYamlFilePath = "config.yml"
)

var c Config

type Option struct {
Prefix string
Delimiter string
Separator string
YamlFilePath string
CallbackEnv func(string) string
}

// our environment variables must prefix with `ORMUS_`
// for nested env should use `__` aka: ORMUS_DB__HOST.
func defaultCallbackEnv(source string) string {
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 {
log.Fatalf("error loading default config: %s", err)
}

// load configuration from yaml file
if err := k.Load(file.Provider(defaultYamlFilePath), yaml.Parser()); err != nil {
log.Printf("error loading config from `config.yml` file: %s", err)
}

// load from environment variable
if err := k.Load(env.Provider(defaultPrefix, defaultDelimiter, defaultCallbackEnv), nil); err != nil {
log.Printf("error loading environment variables: %s", err)
}

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

func C() Config {
return c
}

func New(opt Option) Config {
k := koanf.New(opt.Separator)

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

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

if err := k.Load(env.Provider(opt.Prefix, opt.Delimiter, opt.CallbackEnv), nil); err != nil {
log.Printf("error loading environment variables: %s", err)
}

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

return c
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ require (
)

require golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
require github.com/knadh/koanf v1.5.0

require (
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 5e50685

Please sign in to comment.