Skip to content

Commit

Permalink
feat: add default config handling (#6)
Browse files Browse the repository at this point in the history
Signed-off-by: Tronje Krop <[email protected]>
  • Loading branch information
Tronje Krop committed Sep 19, 2024
1 parent fbeaa35 commit 7f2fd95
Show file tree
Hide file tree
Showing 8 changed files with 708 additions and 271 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.2
0.0.3
113 changes: 62 additions & 51 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/spf13/viper"

"github.com/tkrop/go-config/info"
"github.com/tkrop/go-config/internal/reflect"
"github.com/tkrop/go-config/log"
Expand All @@ -25,8 +26,8 @@ type Config struct {
Log *log.Config
}

// ConfigReader common config reader based on viper.
type ConfigReader[C any] struct {
// Reader common config reader based on viper.
type Reader[C any] struct {
*viper.Viper
}

Expand All @@ -42,90 +43,100 @@ func GetEnvName(prefix string, name string) string {
return name
}

// New creates a new config reader with the given prefix, name, and pointer to
// a config struct. The config struct is used to evaluate the default config
// values and names from the `default`-tags.
// New creates a new config reader with the given prefix, name, and config
// struct. The config struct is evaluate for the non-zero default config tags
// and values different from zero to initialize the default config.
func New[C any](
prefix, name string, config *C,
) *ConfigReader[C] {
c := &ConfigReader[C]{
) *Reader[C] {
r := &Reader[C]{
Viper: viper.New(),
}

c.SetConfigName(GetEnvName(prefix, name))
c.SetConfigType("yaml")
c.AddConfigPath(".")
c.SetEnvPrefix(prefix)
c.AutomaticEnv()
c.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
c.AllowEmptyEnv(true)
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), "../..")
c.AddConfigPath(filepath)
r.AddConfigPath(filepath)
}

// Set default values from config type.
reflect.NewTagWalker("default", nil).
Walk(config, "", c.setConfigDefault)

return c
}

// setConfigDefault sets the default value for the given path using the default
// tag value.
func (c *ConfigReader[C]) setConfigDefault(
_ reflect.Value, path, tag string,
) {
c.SetDefault(path, tag)
return r
}

// SetDefaults is a convenience method to configure the reader with defaults
// and standard values. It is also calling the provide function to customize
// values and add more defaults.
func (c *ConfigReader[C]) SetDefaults(
setup func(*ConfigReader[C]),
) *ConfigReader[C] {
info := info.GetDefault()
c.SetDefault("info.path", info.Path)
c.SetDefault("info.version", info.Version)
c.SetDefault("info.revision", info.Revision)
c.SetDefault("info.build", info.Build)
c.SetDefault("info.commit", info.Commit)
c.SetDefault("info.dirty", info.Dirty)
c.SetDefault("info.go", info.Go)
c.SetDefault("info.platform", info.Platform)
c.SetDefault("info.compiler", info.Compiler)

func (r *Reader[C]) SetDefaults(
setup func(*Reader[C]),
) *Reader[C] {
if setup != nil {
setup(c)
setup(r)
}

return c
return r
}

// SetSubDefaults is a convenience method to update the default values of a
// sub-section configured in the reader by using the given config struct. The
// config struct is scanned for `default`-tags and values to set the defaults.
// Depending on the `zero` flag the default values are either include setting
// zero values or ignoring them.
func (r *Reader[C]) SetSubDefaults(
key string, config any, zero bool,
) *Reader[C] {
info := info.GetDefault()
r.SetDefault("info.path", info.Path)
r.SetDefault("info.version", info.Version)
r.SetDefault("info.revision", info.Revision)
r.SetDefault("info.build", info.Build)
r.SetDefault("info.commit", info.Commit)
r.SetDefault("info.dirty", info.Dirty)
r.SetDefault("info.go", info.Go)
r.SetDefault("info.platform", info.Platform)
r.SetDefault("info.compiler", info.Compiler)

reflect.NewTagWalker("default", "mapstructure", zero).
Walk(key, config, r.SetDefault)

return r
}

// SetDefault is a convenience method to set the default value for the given
// key in the config reader and return the config reader.
func (r *Reader[C]) SetDefault(key string, value any) {
r.Viper.SetDefault(key, value)
}

// 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.
func (c *ConfigReader[C]) ReadConfig(context string) *ConfigReader[C] {
err := c.ReadInConfig()
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))
}

return c
return r
}

// GetConfig is a convenience method to return the config without loading the
// environment specific config file. The context is used to distinguish
// different calls in case of a panic created by failures while unmarschalling
// the config.
func (c *ConfigReader[C]) GetConfig(context string) *C {
func (r *Reader[C]) GetConfig(context string) *C {
config := new(C)
err := c.Unmarshal(config)
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))
Expand All @@ -142,6 +153,6 @@ func (c *ConfigReader[C]) GetConfig(context string) *C {
// 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.
func (c *ConfigReader[C]) LoadConfig(context string) *C {
return c.ReadConfig(context).GetConfig(context)
func (r *Reader[C]) LoadConfig(context string) *C {
return r.ReadConfig(context).GetConfig(context)
}
19 changes: 10 additions & 9 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ func TestReadConfig(t *testing.T) {

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

// When
Expand All @@ -29,6 +29,7 @@ func TestReadConfig(t *testing.T) {

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

type Config struct {
config.Config `mapstructure:",squash"`
Content int
Expand All @@ -37,8 +38,8 @@ func TestReadConfig_UnmarshalFailure(t *testing.T) {
// Given
defer func() { _ = recover() }()
reader := config.New("TC", "test", &Config{}).
SetDefaults(func(c *config.ConfigReader[Config]) {
c.AddConfigPath("../fixtures")
SetDefaults(func(c *config.Reader[Config]) {
c.AddConfigPath("fixtures")
})

// When
Expand All @@ -53,8 +54,8 @@ func TestReadConfig_FileNotFound(t *testing.T) {
defer func() { _ = recover() }()
t.Setenv("TC_ENV", "other")
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.ConfigReader[config.Config]) {
c.AddConfigPath("../fixtures")
SetDefaults(func(c *config.Reader[config.Config]) {
c.AddConfigPath("fixtures")
})

// When
Expand All @@ -68,8 +69,8 @@ func TestReadConfig_OverridingEnv(t *testing.T) {
// Given
t.Setenv("TC_LOG_LEVEL", "trace")
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.ConfigReader[config.Config]) {
c.AddConfigPath("../fixtures")
SetDefaults(func(c *config.Reader[config.Config]) {
c.AddConfigPath("fixtures")
})

// When
Expand All @@ -85,7 +86,7 @@ func TestReadConfig_OverridingFunc(t *testing.T) {

// Given
reader := config.New("TC", "test", &config.Config{}).
SetDefaults(func(c *config.ConfigReader[config.Config]) {
SetDefaults(func(c *config.Reader[config.Config]) {
c.SetDefault("log.level", "trace")
})

Expand Down
3 changes: 3 additions & 0 deletions fixtures/test.yaml → config/fixtures/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ log:
level: info
caller: false

info:
path: github.com/tkrop/go-config

content: |
Hello, World!
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/tkrop/go-config
go 1.23.1

require (
github.com/golang/mock v1.6.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
Expand All @@ -13,7 +14,6 @@ require (
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang/mock v1.6.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
Expand Down
Loading

0 comments on commit 7f2fd95

Please sign in to comment.