Skip to content
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
32 changes: 16 additions & 16 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,24 @@ jobs:
path-to-profile: ./build/test-all.cover


macos:
runs-on: macos-latest
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.25
cache: false
# macos:
# runs-on: macos-latest
# steps:
# - name: Set up Go
# uses: actions/setup-go@v5
# with:
# go-version: 1.25
# cache: false

- name: Checkout code
uses: actions/checkout@v4
# - name: Checkout code
# uses: actions/checkout@v4

- name: Build and tests
env:
BASH_COMPAT: 3.2
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
LANG: en_US.UTF-8
run: make all
# - name: Build and tests
# env:
# BASH_COMPAT: 3.2
# CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
# LANG: en_US.UTF-8
# run: make all


release:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.19
0.0.20
16 changes: 12 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ 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 {
//nolint:errorlint // wrapping error hard to test. // TODO: improve tests.
return fmt.Errorf("%w - %s [%s]: %v", ErrConfig, message, context, err)
return fmt.Errorf("%w - %s [%s]: %w", ErrConfig, message, context, err)
}

// Config common application configuration.
Expand Down Expand Up @@ -113,8 +112,17 @@ func (r *Reader[C]) SetDefaultConfig(
r.SetDefault("info.platform", info.Platform)
r.SetDefault("info.compiler", info.Compiler)

reflect.NewTagWalker("default", "mapstructure", zero).
Walk(key, config, r.SetDefault)
err := reflect.NewTagWalker("default", "mapstructure",
zero, r.SetDefault).Walk(key, config)
if err != nil {
err := NewErrConfig("creating defaults", "", err)
logrus.WithFields(logrus.Fields{
"key": key, "config": config,
}).WithError(err).Warn("creating defaults")
if r.GetBool("viper.panic.defaults") {
panic(err)
}
}

return r
}
Expand Down
207 changes: 143 additions & 64 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,123 +3,202 @@ package config_test
import (
"errors"
"fmt"
"reflect"
"strconv"
"testing"

"github.com/go-viper/mapstructure/v2"
"github.com/ory/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tkrop/go-config/config"
"github.com/tkrop/go-config/info"
"github.com/tkrop/go-config/internal/filepath"
ireflect "github.com/tkrop/go-config/internal/reflect"
"github.com/tkrop/go-testing/mock"
ref "github.com/tkrop/go-testing/reflect"
"github.com/tkrop/go-testing/test"
)

var configPaths = []string{filepath.Normalize(".")}

func newConfig(
env, level string, setup func(info *info.Info),
) *config.Config {
reader := config.NewReader[config.Config]("TC", "test")
config := reader.GetConfig("test-helper")

if setup != nil {
setup(config.Info)
}

config.Env = env
config.Log.Level = level

return config
}

type ConfigParams struct {
setenv func(test.Test)
setup func(*config.Reader[config.Config])
expect mock.SetupFunc
expectEnv string
expectLogLevel string
setup mock.SetupFunc
reader func(test.Test) *config.Reader[config.Config]
expect *config.Config
}

var configTestCases = map[string]ConfigParams{
"default config without file": {
expectEnv: "prod",
expectLogLevel: "info",
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test")
},
expect: newConfig("prod", "info", nil),
},

"default config with file": {
setup: func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
})
},
expectEnv: "prod",
expectLogLevel: "debug",
expect: newConfig("prod", "debug", func(info *info.Info) {
info.Path = "github.com/tkrop/go-config"
}),
},

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

"read config with overriding func": {
setup: func(r *config.Reader[config.Config]) {
r.SetDefault("log.level", "trace")
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.SetDefault("log.level", "trace")
})
},
expectEnv: "prod",
expectLogLevel: "trace",
expect: newConfig("prod", "trace", nil),
},

"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",
setup: test.Panic(config.NewErrConfig("loading file", "test",
ref.NewBuilder[viper.ConfigFileNotFoundError]().
Set("locations", fmt.Sprintf("%s", configPaths)).
Set("name", "test").Build())),
Set("name", "test").Build()).Error()),
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.SetDefault("viper.panic.load", true)
})
},
},

"panic after unmarshal 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", fmt.Errorf("decoding failed due to the following error(s):\n\n%v",
"panic after unmarshal failure next": {
setup: test.Panic(config.NewErrConfig("unmarshal config",
"test", fmt.Errorf(
"decoding failed due to the following error(s):\n\n%v",
"'Info.Dirty' cannot parse value as 'bool': "+
"strconv.ParseBool: invalid syntax",
))),
"strconv.ParseBool: invalid syntax")).Error()),
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
r.SetDefault("viper.panic.unmarshal", true)
r.SetDefault("info.dirty", "5s")
})
},
},
// TODO: Improve error wrapping test for unmarshal failure.
"panic after unmarshal failure next": {
setup: func(r *config.Reader[config.Config]) {
r.AddConfigPath("fixtures")
r.SetDefault("viper.panic.unmarshal", true)
r.SetDefault("info.dirty", "5s")

"error on default config with invalid tag": {
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.SetDefaultConfig("", &struct {
Field []string `default:"invalid"`
}{}, false)
})
},
expect: newConfig("prod", "info", nil),
},

"panic on default config with invalid tag": {
setup: test.Panic(config.NewErrConfig("creating defaults", "",
ireflect.NewErrTagWalker("yaml parsing", "field", "invalid",
errors.New("yaml: unmarshal errors:\n line 1: "+
"cannot unmarshal !!str `invalid` into []string"))).
Error()),
reader: func(_ test.Test) *config.Reader[config.Config] {
return config.NewReader[config.Config]("TC", "test").
SetDefaults(func(r *config.Reader[config.Config]) {
r.SetDefault("viper.panic.defaults", true)
r.SetDefaultConfig("", &struct {
Field []string `default:"invalid"`
}{}, false)
})
},
expect: test.Panic(config.NewErrConfig("unmarshal config",
"test", fmt.Errorf("decoding failed due to the following error(s):\n\n%w",
errors.Join(ref.NewBuilder[*mapstructure.DecodeError]().
Set("name", "Info.Dirty").
Set("err", &mapstructure.ParseError{
Expected: reflect.ValueOf(true), Value: "5s",
Err: &strconv.NumError{Func: "ParseBool", Num: "5s"},
}).Build())))),
},
}

func TestConfig(t *testing.T) {
test.Map(t, configTestCases).
Filter(test.Not(test.Pattern[ConfigParams](
"panic-after-unmarshal-failure-next"))).
RunSeq(func(t test.Test, param ConfigParams) {
// Given
mock.NewMocks(t).Expect(param.expect)
if param.setenv != nil {
param.setenv(t)
}
reader := config.NewReader[config.Config]("TC", "test").
SetDefaults(param.setup)
mock.NewMocks(t).Expect(param.setup)

// When
reader := param.reader(t)
config := reader.LoadConfig("test")

// Then
assert.Equal(t, param.expect, config)
})
}

// TODO: improve test to provide meaningful insights about defaults.
type AnyConfigParams struct {
// setup mock.SetupFunc
config any
expect any
}

type object struct {
A any
B any
C any
D []int
}

var anyConfigParams = map[string]AnyConfigParams{
"read struct tag": {
config: &struct {
S *object `default:"{a: <default>, d: [1,2,3]}"`
}{S: &object{}},
expect: &struct {
S *object `default:"{a: <default>, d: [1,2,3]}"`
}{S: &object{
A: "<default>",
D: []int{1, 2, 3},
}},
},
}

func TestAnyConfig(t *testing.T) {
test.Map(t, anyConfigParams).
RunSeq(func(t test.Test, param AnyConfigParams) {
// Given
// mock.NewMocks(t).Expect(param.setup)
reader := config.NewReader[any]("TC", "test")
reader.SetDefaultConfig("", param.config, true)

// When
reader.LoadConfig("test")
err := reader.Unmarshal(param.config)

// Then
assert.Equal(t, param.expectEnv, reader.GetString("env"))
assert.Equal(t, param.expectLogLevel, reader.GetString("log.level"))
require.NoError(t, err)
assert.Equal(t, param.expect, param.config)
})
}
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ module github.com/tkrop/go-config
go 1.25.4

require (
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/ory/viper v1.7.5
github.com/rs/zerolog v1.34.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tkrop/go-testing v0.0.41
github.com/tkrop/go-testing v0.0.44
go.uber.org/mock v0.6.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/cespare/xxhash v1.1.0 // indirect
github.com/dgraph-io/ristretto v0.0.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.1 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
Expand All @@ -40,6 +40,6 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
golang.org/x/term v0.36.0
golang.org/x/text v0.30.0 // indirect
golang.org/x/term v0.37.0
golang.org/x/text v0.31.0 // indirect
)
Loading
Loading