Skip to content

Commit fd205f9

Browse files
committed
fix: config default tags (#40)
Signed-off-by: Tronje Krop <[email protected]>
1 parent 501e146 commit fd205f9

File tree

9 files changed

+735
-373
lines changed

9 files changed

+735
-373
lines changed

.github/workflows/build.yaml

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,24 @@ jobs:
2626
path-to-profile: ./build/test-all.cover
2727

2828

29-
macos:
30-
runs-on: macos-latest
31-
steps:
32-
- name: Set up Go
33-
uses: actions/setup-go@v5
34-
with:
35-
go-version: 1.25
36-
cache: false
29+
# macos:
30+
# runs-on: macos-latest
31+
# steps:
32+
# - name: Set up Go
33+
# uses: actions/setup-go@v5
34+
# with:
35+
# go-version: 1.25
36+
# cache: false
3737

38-
- name: Checkout code
39-
uses: actions/checkout@v4
38+
# - name: Checkout code
39+
# uses: actions/checkout@v4
4040

41-
- name: Build and tests
42-
env:
43-
BASH_COMPAT: 3.2
44-
CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
45-
LANG: en_US.UTF-8
46-
run: make all
41+
# - name: Build and tests
42+
# env:
43+
# BASH_COMPAT: 3.2
44+
# CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
45+
# LANG: en_US.UTF-8
46+
# run: make all
4747

4848

4949
release:

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.0.19
1+
0.0.20

config/config.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ var ErrConfig = errors.New("config")
2222
// NewErrConfig is a convenience method to create a new config error with the
2323
// given context wrapping the original error.
2424
func NewErrConfig(message, context string, err error) error {
25-
//nolint:errorlint // wrapping error hard to test. // TODO: improve tests.
26-
return fmt.Errorf("%w - %s [%s]: %v", ErrConfig, message, context, err)
25+
return fmt.Errorf("%w - %s [%s]: %w", ErrConfig, message, context, err)
2726
}
2827

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

116-
reflect.NewTagWalker("default", "mapstructure", zero).
117-
Walk(key, config, r.SetDefault)
115+
err := reflect.NewTagWalker("default", "mapstructure",
116+
zero, r.SetDefault).Walk(key, config)
117+
if err != nil {
118+
err := NewErrConfig("creating defaults", "", err)
119+
logrus.WithFields(logrus.Fields{
120+
"key": key, "config": config,
121+
}).WithError(err).Warn("creating defaults")
122+
if r.GetBool("viper.panic.defaults") {
123+
panic(err)
124+
}
125+
}
118126

119127
return r
120128
}

config/config_test.go

Lines changed: 143 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,123 +3,202 @@ package config_test
33
import (
44
"errors"
55
"fmt"
6-
"reflect"
7-
"strconv"
86
"testing"
97

10-
"github.com/go-viper/mapstructure/v2"
118
"github.com/ory/viper"
129
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1311

1412
"github.com/tkrop/go-config/config"
13+
"github.com/tkrop/go-config/info"
1514
"github.com/tkrop/go-config/internal/filepath"
15+
ireflect "github.com/tkrop/go-config/internal/reflect"
1616
"github.com/tkrop/go-testing/mock"
1717
ref "github.com/tkrop/go-testing/reflect"
1818
"github.com/tkrop/go-testing/test"
1919
)
2020

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

23+
func newConfig(
24+
env, level string, setup func(info *info.Info),
25+
) *config.Config {
26+
reader := config.NewReader[config.Config]("TC", "test")
27+
config := reader.GetConfig("test-helper")
28+
29+
if setup != nil {
30+
setup(config.Info)
31+
}
32+
33+
config.Env = env
34+
config.Log.Level = level
35+
36+
return config
37+
}
38+
2339
type ConfigParams struct {
24-
setenv func(test.Test)
25-
setup func(*config.Reader[config.Config])
26-
expect mock.SetupFunc
27-
expectEnv string
28-
expectLogLevel string
40+
setup mock.SetupFunc
41+
reader func(test.Test) *config.Reader[config.Config]
42+
expect *config.Config
2943
}
3044

3145
var configTestCases = map[string]ConfigParams{
3246
"default config without file": {
33-
expectEnv: "prod",
34-
expectLogLevel: "info",
47+
reader: func(_ test.Test) *config.Reader[config.Config] {
48+
return config.NewReader[config.Config]("TC", "test")
49+
},
50+
expect: newConfig("prod", "info", nil),
3551
},
3652

3753
"default config with file": {
38-
setup: func(r *config.Reader[config.Config]) {
39-
r.AddConfigPath("fixtures")
54+
reader: func(_ test.Test) *config.Reader[config.Config] {
55+
return config.NewReader[config.Config]("TC", "test").
56+
SetDefaults(func(r *config.Reader[config.Config]) {
57+
r.AddConfigPath("fixtures")
58+
})
4059
},
41-
expectEnv: "prod",
42-
expectLogLevel: "debug",
60+
expect: newConfig("prod", "debug", func(info *info.Info) {
61+
info.Path = "github.com/tkrop/go-config"
62+
}),
4363
},
4464

4565
"read config with overriding env": {
46-
setenv: func(t test.Test) {
66+
reader: func(t test.Test) *config.Reader[config.Config] {
4767
t.Setenv("TC_ENV", "test")
4868
t.Setenv("TC_LOG_LEVEL", "trace")
69+
return config.NewReader[config.Config]("TC", "test").
70+
SetDefaults(func(r *config.Reader[config.Config]) {
71+
r.AddConfigPath("fixtures")
72+
})
4973
},
50-
setup: func(r *config.Reader[config.Config]) {
51-
r.AddConfigPath("fixtures")
52-
},
53-
expectEnv: "test",
54-
expectLogLevel: "trace",
74+
expect: newConfig("test", "trace", nil),
5575
},
5676

5777
"read config with overriding func": {
58-
setup: func(r *config.Reader[config.Config]) {
59-
r.SetDefault("log.level", "trace")
78+
reader: func(_ test.Test) *config.Reader[config.Config] {
79+
return config.NewReader[config.Config]("TC", "test").
80+
SetDefaults(func(r *config.Reader[config.Config]) {
81+
r.SetDefault("log.level", "trace")
82+
})
6083
},
61-
expectEnv: "prod",
62-
expectLogLevel: "trace",
84+
expect: newConfig("prod", "trace", nil),
6385
},
6486

6587
"panic after file not found": {
66-
setup: func(r *config.Reader[config.Config]) {
67-
r.SetDefault("viper.panic.load", true)
68-
},
69-
expect: test.Panic(config.NewErrConfig("loading file", "test",
88+
setup: test.Panic(config.NewErrConfig("loading file", "test",
7089
ref.NewBuilder[viper.ConfigFileNotFoundError]().
7190
Set("locations", fmt.Sprintf("%s", configPaths)).
72-
Set("name", "test").Build())),
91+
Set("name", "test").Build()).Error()),
92+
reader: func(_ test.Test) *config.Reader[config.Config] {
93+
return config.NewReader[config.Config]("TC", "test").
94+
SetDefaults(func(r *config.Reader[config.Config]) {
95+
r.SetDefault("viper.panic.load", true)
96+
})
97+
},
7398
},
7499

75-
"panic after unmarshal failure": {
76-
setup: func(r *config.Reader[config.Config]) {
77-
r.AddConfigPath("fixtures")
78-
r.SetDefault("viper.panic.unmarshal", true)
79-
r.SetDefault("info.dirty", "5s")
80-
},
81-
expect: test.Panic(config.NewErrConfig("unmarshal config",
82-
"test", fmt.Errorf("decoding failed due to the following error(s):\n\n%v",
100+
"panic after unmarshal failure next": {
101+
setup: test.Panic(config.NewErrConfig("unmarshal config",
102+
"test", fmt.Errorf(
103+
"decoding failed due to the following error(s):\n\n%v",
83104
"'Info.Dirty' cannot parse value as 'bool': "+
84-
"strconv.ParseBool: invalid syntax",
85-
))),
105+
"strconv.ParseBool: invalid syntax")).Error()),
106+
reader: func(_ test.Test) *config.Reader[config.Config] {
107+
return config.NewReader[config.Config]("TC", "test").
108+
SetDefaults(func(r *config.Reader[config.Config]) {
109+
r.AddConfigPath("fixtures")
110+
r.SetDefault("viper.panic.unmarshal", true)
111+
r.SetDefault("info.dirty", "5s")
112+
})
113+
},
86114
},
87-
// TODO: Improve error wrapping test for unmarshal failure.
88-
"panic after unmarshal failure next": {
89-
setup: func(r *config.Reader[config.Config]) {
90-
r.AddConfigPath("fixtures")
91-
r.SetDefault("viper.panic.unmarshal", true)
92-
r.SetDefault("info.dirty", "5s")
115+
116+
"error on default config with invalid tag": {
117+
reader: func(_ test.Test) *config.Reader[config.Config] {
118+
return config.NewReader[config.Config]("TC", "test").
119+
SetDefaults(func(r *config.Reader[config.Config]) {
120+
r.SetDefaultConfig("", &struct {
121+
Field []string `default:"invalid"`
122+
}{}, false)
123+
})
124+
},
125+
expect: newConfig("prod", "info", nil),
126+
},
127+
128+
"panic on default config with invalid tag": {
129+
setup: test.Panic(config.NewErrConfig("creating defaults", "",
130+
ireflect.NewErrTagWalker("yaml parsing", "field", "invalid",
131+
errors.New("yaml: unmarshal errors:\n line 1: "+
132+
"cannot unmarshal !!str `invalid` into []string"))).
133+
Error()),
134+
reader: func(_ test.Test) *config.Reader[config.Config] {
135+
return config.NewReader[config.Config]("TC", "test").
136+
SetDefaults(func(r *config.Reader[config.Config]) {
137+
r.SetDefault("viper.panic.defaults", true)
138+
r.SetDefaultConfig("", &struct {
139+
Field []string `default:"invalid"`
140+
}{}, false)
141+
})
93142
},
94-
expect: test.Panic(config.NewErrConfig("unmarshal config",
95-
"test", fmt.Errorf("decoding failed due to the following error(s):\n\n%w",
96-
errors.Join(ref.NewBuilder[*mapstructure.DecodeError]().
97-
Set("name", "Info.Dirty").
98-
Set("err", &mapstructure.ParseError{
99-
Expected: reflect.ValueOf(true), Value: "5s",
100-
Err: &strconv.NumError{Func: "ParseBool", Num: "5s"},
101-
}).Build())))),
102143
},
103144
}
104145

105146
func TestConfig(t *testing.T) {
106147
test.Map(t, configTestCases).
107-
Filter(test.Not(test.Pattern[ConfigParams](
108-
"panic-after-unmarshal-failure-next"))).
109148
RunSeq(func(t test.Test, param ConfigParams) {
110149
// Given
111-
mock.NewMocks(t).Expect(param.expect)
112-
if param.setenv != nil {
113-
param.setenv(t)
114-
}
115-
reader := config.NewReader[config.Config]("TC", "test").
116-
SetDefaults(param.setup)
150+
mock.NewMocks(t).Expect(param.setup)
151+
152+
// When
153+
reader := param.reader(t)
154+
config := reader.LoadConfig("test")
155+
156+
// Then
157+
assert.Equal(t, param.expect, config)
158+
})
159+
}
160+
161+
// TODO: improve test to provide meaningful insights about defaults.
162+
type AnyConfigParams struct {
163+
// setup mock.SetupFunc
164+
config any
165+
expect any
166+
}
167+
168+
type object struct {
169+
A any
170+
B any
171+
C any
172+
D []int
173+
}
174+
175+
var anyConfigParams = map[string]AnyConfigParams{
176+
"read struct tag": {
177+
config: &struct {
178+
S *object `default:"{a: <default>, d: [1,2,3]}"`
179+
}{S: &object{}},
180+
expect: &struct {
181+
S *object `default:"{a: <default>, d: [1,2,3]}"`
182+
}{S: &object{
183+
A: "<default>",
184+
D: []int{1, 2, 3},
185+
}},
186+
},
187+
}
188+
189+
func TestAnyConfig(t *testing.T) {
190+
test.Map(t, anyConfigParams).
191+
RunSeq(func(t test.Test, param AnyConfigParams) {
192+
// Given
193+
// mock.NewMocks(t).Expect(param.setup)
194+
reader := config.NewReader[any]("TC", "test")
195+
reader.SetDefaultConfig("", param.config, true)
117196

118197
// When
119-
reader.LoadConfig("test")
198+
err := reader.Unmarshal(param.config)
120199

121200
// Then
122-
assert.Equal(t, param.expectEnv, reader.GetString("env"))
123-
assert.Equal(t, param.expectLogLevel, reader.GetString("log.level"))
201+
require.NoError(t, err)
202+
assert.Equal(t, param.expect, param.config)
124203
})
125204
}

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ module github.com/tkrop/go-config
33
go 1.25.4
44

55
require (
6-
github.com/go-viper/mapstructure/v2 v2.4.0
76
github.com/ory/viper v1.7.5
87
github.com/rs/zerolog v1.34.0
98
github.com/sirupsen/logrus v1.9.3
109
github.com/spf13/viper v1.21.0
1110
github.com/stretchr/testify v1.11.1
12-
github.com/tkrop/go-testing v0.0.41
11+
github.com/tkrop/go-testing v0.0.44
1312
go.uber.org/mock v0.6.0
1413
gopkg.in/yaml.v3 v3.0.1
1514
)
1615

1716
require (
1817
github.com/cespare/xxhash v1.1.0 // indirect
1918
github.com/dgraph-io/ristretto v0.0.1 // indirect
19+
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
2020
github.com/hashicorp/hcl v1.0.0 // indirect
2121
github.com/magiconair/properties v1.8.1 // indirect
2222
github.com/mitchellh/mapstructure v1.1.2 // indirect
@@ -40,6 +40,6 @@ require (
4040
github.com/spf13/cast v1.10.0 // indirect
4141
github.com/spf13/pflag v1.0.10 // indirect
4242
github.com/subosito/gotenv v1.6.0 // indirect
43-
golang.org/x/term v0.36.0
44-
golang.org/x/text v0.30.0 // indirect
43+
golang.org/x/term v0.37.0
44+
golang.org/x/text v0.31.0 // indirect
4545
)

0 commit comments

Comments
 (0)