Skip to content

Commit 16856c0

Browse files
committed
Introduce config.FromEnv()
1 parent 420fbff commit 16856c0

File tree

4 files changed

+126
-0
lines changed

4 files changed

+126
-0
lines changed

config/config.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package config
33
import (
44
stderrors "errors"
55
"fmt"
6+
"github.com/caarlos0/env/v11"
67
"github.com/creasty/defaults"
78
"github.com/goccy/go-yaml"
89
"github.com/jessevdk/go-flags"
@@ -50,6 +51,28 @@ func FromYAMLFile(name string, v Validator) error {
5051
return nil
5152
}
5253

54+
// EnvOptions is a type alias for [env.Options], so that only this package needs to import [env].
55+
type EnvOptions = env.Options
56+
57+
// FromEnv parses environment variables and stores the result in the value pointed to by v.
58+
// If v is nil or not a pointer, FromEnv returns an [ErrInvalidArgument] error.
59+
func FromEnv(v Validator, options EnvOptions) error {
60+
rv := reflect.ValueOf(v)
61+
if rv.Kind() != reflect.Ptr || rv.IsNil() {
62+
return errors.Wrapf(ErrInvalidArgument, "non-nil pointer expected, got %T", v)
63+
}
64+
65+
if err := defaults.Set(v); err != nil {
66+
return errors.Wrap(err, "can't set config defaults")
67+
}
68+
69+
if err := env.ParseWithOptions(v, options); err != nil {
70+
return errors.Wrap(err, "can't parse environment variables")
71+
}
72+
73+
return errors.Wrap(v.Validate(), "invalid configuration")
74+
}
75+
5376
// ParseFlags parses CLI flags and stores the result
5477
// in the value pointed to by v. If v is nil or not a pointer,
5578
// ParseFlags returns an [ErrInvalidArgument] error.

config/config_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package config
2+
3+
import (
4+
"errors"
5+
"github.com/stretchr/testify/require"
6+
"os"
7+
"reflect"
8+
"testing"
9+
)
10+
11+
type simpleValidator struct {
12+
Foo int `env:"FOO"`
13+
}
14+
15+
func (sv simpleValidator) Validate() error {
16+
if sv.Foo == 42 {
17+
return nil
18+
} else {
19+
return errors.New("invalid value")
20+
}
21+
}
22+
23+
type nonStructValidator int
24+
25+
func (nonStructValidator) Validate() error {
26+
return nil
27+
}
28+
29+
type defaultValidator struct {
30+
Foo int `env:"FOO" default:"42"`
31+
}
32+
33+
func (defaultValidator) Validate() error {
34+
return nil
35+
}
36+
37+
type prefixValidator struct {
38+
Nested simpleValidator `envPrefix:"PREFIX_"`
39+
}
40+
41+
func (prefixValidator) Validate() error {
42+
return nil
43+
}
44+
45+
func TestFromEnv(t *testing.T) {
46+
subtests := []struct {
47+
name string
48+
env map[string]string
49+
options EnvOptions
50+
io Validator
51+
error bool
52+
}{
53+
{"nil", nil, EnvOptions{}, nil, true},
54+
{"nonptr", nil, EnvOptions{}, simpleValidator{}, true},
55+
{"nilptr", nil, EnvOptions{}, (*simpleValidator)(nil), true},
56+
{"defaulterr", nil, EnvOptions{}, new(nonStructValidator), true},
57+
{"parseeerr", map[string]string{"FOO": "bar"}, EnvOptions{}, &simpleValidator{}, true},
58+
{"invalid", map[string]string{"FOO": "23"}, EnvOptions{}, &simpleValidator{}, true},
59+
{"simple", map[string]string{"FOO": "42"}, EnvOptions{}, &simpleValidator{42}, false},
60+
{"default", nil, EnvOptions{}, &defaultValidator{42}, false},
61+
{"override", map[string]string{"FOO": "23"}, EnvOptions{}, &defaultValidator{23}, false},
62+
{"prefix", map[string]string{"PREFIX_FOO": "42"}, EnvOptions{Prefix: "PREFIX_"}, &simpleValidator{42}, false},
63+
{"nested", map[string]string{"PREFIX_FOO": "42"}, EnvOptions{}, &prefixValidator{simpleValidator{42}}, false},
64+
}
65+
66+
allEnv := make(map[string]struct{})
67+
for _, st := range subtests {
68+
for k := range st.env {
69+
allEnv[k] = struct{}{}
70+
}
71+
}
72+
73+
for _, st := range subtests {
74+
t.Run(st.name, func(t *testing.T) {
75+
for k := range allEnv {
76+
require.NoError(t, os.Unsetenv(k))
77+
}
78+
79+
for k, v := range st.env {
80+
require.NoError(t, os.Setenv(k, v))
81+
}
82+
83+
var actual Validator
84+
if vActual := reflect.ValueOf(st.io); vActual != (reflect.Value{}) {
85+
if vActual.Kind() == reflect.Ptr && !vActual.IsNil() {
86+
vActual = reflect.New(vActual.Type().Elem())
87+
}
88+
89+
actual = vActual.Interface().(Validator)
90+
}
91+
92+
if err := FromEnv(actual, st.options); st.error {
93+
require.Error(t, err)
94+
} else {
95+
require.NoError(t, err)
96+
require.Equal(t, st.io, actual)
97+
}
98+
})
99+
}
100+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/icinga/icinga-go-library
33
go 1.22
44

55
require (
6+
github.com/caarlos0/env/v11 v11.1.0
67
github.com/creasty/defaults v1.7.0
78
github.com/go-sql-driver/mysql v1.8.1
89
github.com/goccy/go-yaml v1.12.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
44
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
55
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
66
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
7+
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
8+
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
79
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
810
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
911
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=

0 commit comments

Comments
 (0)