Skip to content

Commit

Permalink
feat: clean up and improve (#8)
Browse files Browse the repository at this point in the history
Signed-off-by: Tronje Krop <[email protected]>
  • Loading branch information
Tronje Krop authored Sep 22, 2024
1 parent 8e48841 commit 9eead42
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 139 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ In [`go-config`][go-config] you simply create your config as an extension of
the config provided in this package as a base line as follows:

```go
// Import for config prototype and config reader.
import "github.com/tkrop/go-config/config"

// Config root element for configuration.
type Config struct {
config.Config `mapstructure:",squash"`
Expand Down Expand Up @@ -130,11 +133,10 @@ in the `SetSubDefaults` method as follows:
## Logger setup

The [`go-config`][go-config] framework supports to set up a [logrus][logrus]
`Logger`_out-of-the-box as follows:
`Logger`_out-of-the-box using the following two approaches:

```go
config := config.New("<prefix>", "<app-name>", &Config{}).
LoadConfig("main")
config := config.SetupLogger(logger)

logger := config.Log.Setup(log.New())
```
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.3
0.0.4
73 changes: 44 additions & 29 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
package config

import (
"errors"
"fmt"
"os"
"path"
"runtime"
"strings"

"github.com/spf13/viper"
Expand All @@ -16,6 +15,15 @@ import (
"github.com/tkrop/go-config/log"
)

// ErrConfig is a common error to indicate a configuration error.
var ErrConfig = errors.New("config")

// NewErrConfig is a convenience method to create a new config error with the
// given context wrapping the original error.
func NewErrConfig(message, context string, err error) error {
return fmt.Errorf("%w - %s [%s]: %w", ErrConfig, message, context, err)
}

// Config common application configuration.
type Config struct {
// Env contains the execution environment, e.g. local, prod, test.
Expand All @@ -26,6 +34,13 @@ type Config struct {
Log *log.Config
}

// SetupLogger is a convenience method to setup the logger.
func (c *Config) SetupLogger(logger *log.Logger) *Config {
c.Log.Setup(logger)

return c
}

// Reader common config reader based on viper.
type Reader[C any] struct {
*viper.Viper
Expand All @@ -36,8 +51,7 @@ type Reader[C any] struct {
// with the environment specific suffix for loading the config file in `yaml`
// format.
func GetEnvName(prefix string, name string) string {
env := strings.ToLower(os.Getenv(prefix + "_ENV"))
if env != "" {
if env := strings.ToLower(os.Getenv(prefix + "_ENV")); env != "" {
return fmt.Sprintf("%s-%s", name, env)
}
return name
Expand All @@ -54,22 +68,15 @@ func New[C any](
Viper: viper.New(),
}

r.AutomaticEnv()
r.AllowEmptyEnv(true)
r.SetEnvPrefix(prefix)
r.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
r.SetConfigName(GetEnvName(prefix, name))
r.SetConfigType("yaml")
r.AddConfigPath(".")
r.SetEnvPrefix(prefix)
r.AutomaticEnv()
r.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
r.AllowEmptyEnv(true)

r.SetSubDefaults("", config, true)

// Determine parent directory of this code file.
if _, filename, _, ok := runtime.Caller(1); ok {
filepath := path.Join(path.Dir(filename), "../..")
r.AddConfigPath(filepath)
}

return r
}

Expand Down Expand Up @@ -122,13 +129,16 @@ func (r *Reader[C]) SetDefault(key string, value any) {

// ReadConfig is a convenience method to read the environment specific config
// file to extend the default config. The context is used to distinguish
// different calls in case of a panic by failures while loading the config
// file.
// different calls in case of a failure loading the config file.
func (r *Reader[C]) ReadConfig(context string) *Reader[C] {
err := r.ReadInConfig()
if err != nil {
log.WithError(err).Errorf("failed to load config [%s]", context)
panic(fmt.Errorf("failed to load config [%s]: %w", context, err))
if err := r.ReadInConfig(); err != nil {
err := NewErrConfig("loading file", context, err)
log.WithFields(log.Fields{
"context": context,
}).WithError(err).Warn("no config file found")
if r.GetBool("viper.panic.load") {
panic(err)
}
}

return r
Expand All @@ -140,23 +150,28 @@ func (r *Reader[C]) ReadConfig(context string) *Reader[C] {
// the config.
func (r *Reader[C]) GetConfig(context string) *C {
config := new(C)
err := r.Unmarshal(config)
if err != nil {
log.WithError(err).Errorf("failed to unmarshal config [%s]", context)
panic(fmt.Errorf("failed to unmarshal config [%s]: %w", context, err))
if err := r.Unmarshal(config); err != nil {
err := NewErrConfig("unmarshal config", context, err)
log.WithFields(log.Fields{
"context": context,
}).WithError(err).Error("unmarshal config")
if r.GetBool("viper.panic.unmarshal") {
panic(err)
}
}

log.WithFields(log.Fields{
"config": config,
}).Debugf("config loaded [%s]", context)
"context": context,
"config": config,
}).Debugf("config loaded")

return config
}

// LoadConfig is a convenience method to load the environment specific config
// file and returns the config. The context is used to distinguish different
// calls in case of a panic created by failures while loading the config file
// and umarshalling the config.
// calls in case of a panic created by failures loading the config file or
// umarshalling the config.
func (r *Reader[C]) LoadConfig(context string) *C {
return r.ReadConfig(context).GetConfig(context)
}
168 changes: 96 additions & 72 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,99 +1,123 @@
package config_test

import (
"fmt"
"testing"

"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tkrop/go-config/config"
"github.com/tkrop/go-config/internal/filepath"
"github.com/tkrop/go-config/log"
"github.com/tkrop/go-testing/mock"
"github.com/tkrop/go-testing/test"
)

func TestReadConfig(t *testing.T) {
t.Parallel()

// Given
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.Reader[config.Config]) {
c.AddConfigPath("fixtures")
})
var configPaths = []string{filepath.Normalize(".")}

// When
config := reader.LoadConfig(t.Name())

// Than
assert.Equal(t, "prod", config.Env)
assert.Equal(t, log.DefaultLogLevel, config.Log.Level)
type testConfigParam struct {
setenv func(test.Test)
setup func(*config.Reader[config.Config])
expect mock.SetupFunc
expectEnv string
expectLogLevel string
}

func TestReadConfig_UnmarshalFailure(t *testing.T) {
t.Parallel()

type Config struct {
config.Config `mapstructure:",squash"`
Content int
}

// Given
defer func() { _ = recover() }()
reader := config.New("TC", "test", &Config{}).
SetDefaults(func(c *config.Reader[Config]) {
c.AddConfigPath("fixtures")
})

// When
_ = reader.LoadConfig(t.Name())

// Then
require.Fail(t, "no panic after unmarschal failure")
var testConfigParams = map[string]testConfigParam{
"default config without file": {
expectEnv: "prod",
expectLogLevel: "info",
},

"default config with file": {
setup: func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
},
expectEnv: "prod",
expectLogLevel: "debug",
},

"read config with overriding env": {
setenv: func(t test.Test) {
t.Setenv("TC_ENV", "test")
t.Setenv("TC_LOG_LEVEL", "trace")
},
setup: func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
},
expectEnv: "test",
expectLogLevel: "trace",
},

"read config with overriding func": {
setup: func(r *config.Reader[config.Config]) {
r.SetDefault("log.level", "trace")
},
expectEnv: "prod",
expectLogLevel: "trace",
},

"panic after file not found": {
setup: func(r *config.Reader[config.Config]) {
r.SetDefault("viper.panic.load", true)
},
expect: test.Panic(config.NewErrConfig("loading file", "test",
test.Error(viper.ConfigFileNotFoundError{}).Set("name", "test").
Set("locations", fmt.Sprintf("%s", configPaths)).
Get("").(error))),
},

"panic after unmarschal failure": {
setup: func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
r.SetDefault("viper.panic.unmarshal", true)
r.SetDefault("info.dirty", "5s")
},
expect: test.Panic(config.NewErrConfig("unmarshal config",
"test", &mapstructure.Error{
Errors: []string{"cannot parse 'Info.Dirty' as bool: " +
"strconv.ParseBool: parsing \"5s\": invalid syntax"},
})),
},
}

func TestReadConfig_FileNotFound(t *testing.T) {
// Given
defer func() { _ = recover() }()
t.Setenv("TC_ENV", "other")
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.Reader[config.Config]) {
c.AddConfigPath("fixtures")
})

// When
_ = reader.LoadConfig(t.Name())

// Then
require.Fail(t, "no panic after missing config file")
}

func TestReadConfig_OverridingEnv(t *testing.T) {
// Given
t.Setenv("TC_LOG_LEVEL", "trace")
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.Reader[config.Config]) {
c.AddConfigPath("fixtures")
func TestConfig(t *testing.T) {
test.Map(t, testConfigParams).
RunSeq(func(t test.Test, param testConfigParam) {
// Given
mock.NewMocks(t).Expect(param.expect)
if param.setenv != nil {
param.setenv(t)
}
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(param.setup)

// When
reader.LoadConfig("test")

// Then
assert.Equal(t, param.expectEnv, reader.GetString("env"))
assert.Equal(t, param.expectLogLevel, reader.GetString("log.level"))
})

// When
config := reader.LoadConfig(t.Name())

// Then
assert.Equal(t, "prod", config.Env)
assert.Equal(t, "trace", config.Log.Level)
}

func TestReadConfig_OverridingFunc(t *testing.T) {
func TestSetupLogger(t *testing.T) {
t.Parallel()

// Given
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.Reader[config.Config]) {
c.SetDefault("log.level", "trace")
})
logger := log.New()
config := config.New("TC", "test", &config.Config{}).
SetDefaults(func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
r.SetDefault("log.level", "trace")
}).
GetConfig(t.Name())

// When
config := reader.GetConfig(t.Name())
config.SetupLogger(logger)

// Then
assert.Equal(t, "prod", config.Env)
assert.Equal(t, "trace", config.Log.Level)
assert.Equal(t, log.TraceLevel, logger.GetLevel())
}
2 changes: 1 addition & 1 deletion config/fixtures/test.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
log:
level: info
level: debug
caller: false

info:
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ go 1.23.1

require (
github.com/golang/mock v1.6.0
github.com/mitchellh/mapstructure v1.5.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/tkrop/go-testing v0.0.13
github.com/tkrop/go-testing v0.0.16
gopkg.in/yaml.v3 v3.0.1
)

Expand All @@ -16,7 +17,6 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
Expand Down
Loading

0 comments on commit 9eead42

Please sign in to comment.