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: clean up and improve (#8) #8

Merged
merged 1 commit into from
Sep 22, 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
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
Loading