Skip to content

Commit

Permalink
Introduce config.FromEnv()
Browse files Browse the repository at this point in the history
  • Loading branch information
Al2Klimov committed Aug 5, 2024
1 parent 420fbff commit 16856c0
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 0 deletions.
23 changes: 23 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
stderrors "errors"
"fmt"
"github.com/caarlos0/env/v11"
"github.com/creasty/defaults"
"github.com/goccy/go-yaml"
"github.com/jessevdk/go-flags"
Expand Down Expand Up @@ -50,6 +51,28 @@ func FromYAMLFile(name string, v Validator) error {
return nil
}

// EnvOptions is a type alias for [env.Options], so that only this package needs to import [env].
type EnvOptions = env.Options

// FromEnv parses environment variables and stores the result in the value pointed to by v.
// If v is nil or not a pointer, FromEnv returns an [ErrInvalidArgument] error.
func FromEnv(v Validator, options EnvOptions) error {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return errors.Wrapf(ErrInvalidArgument, "non-nil pointer expected, got %T", v)
}

if err := defaults.Set(v); err != nil {
return errors.Wrap(err, "can't set config defaults")
}

if err := env.ParseWithOptions(v, options); err != nil {
return errors.Wrap(err, "can't parse environment variables")
}

return errors.Wrap(v.Validate(), "invalid configuration")
}

// ParseFlags parses CLI flags and stores the result
// in the value pointed to by v. If v is nil or not a pointer,
// ParseFlags returns an [ErrInvalidArgument] error.
Expand Down
100 changes: 100 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package config

import (
"errors"
"github.com/stretchr/testify/require"
"os"
"reflect"
"testing"
)

type simpleValidator struct {
Foo int `env:"FOO"`
}

func (sv simpleValidator) Validate() error {
if sv.Foo == 42 {
return nil
} else {
return errors.New("invalid value")
}
}

type nonStructValidator int

func (nonStructValidator) Validate() error {
return nil
}

type defaultValidator struct {
Foo int `env:"FOO" default:"42"`
}

func (defaultValidator) Validate() error {
return nil
}

type prefixValidator struct {
Nested simpleValidator `envPrefix:"PREFIX_"`
}

func (prefixValidator) Validate() error {
return nil
}

func TestFromEnv(t *testing.T) {
subtests := []struct {
name string
env map[string]string
options EnvOptions
io Validator
error bool
}{
{"nil", nil, EnvOptions{}, nil, true},
{"nonptr", nil, EnvOptions{}, simpleValidator{}, true},
{"nilptr", nil, EnvOptions{}, (*simpleValidator)(nil), true},
{"defaulterr", nil, EnvOptions{}, new(nonStructValidator), true},
{"parseeerr", map[string]string{"FOO": "bar"}, EnvOptions{}, &simpleValidator{}, true},
{"invalid", map[string]string{"FOO": "23"}, EnvOptions{}, &simpleValidator{}, true},
{"simple", map[string]string{"FOO": "42"}, EnvOptions{}, &simpleValidator{42}, false},
{"default", nil, EnvOptions{}, &defaultValidator{42}, false},
{"override", map[string]string{"FOO": "23"}, EnvOptions{}, &defaultValidator{23}, false},
{"prefix", map[string]string{"PREFIX_FOO": "42"}, EnvOptions{Prefix: "PREFIX_"}, &simpleValidator{42}, false},
{"nested", map[string]string{"PREFIX_FOO": "42"}, EnvOptions{}, &prefixValidator{simpleValidator{42}}, false},
}

allEnv := make(map[string]struct{})
for _, st := range subtests {
for k := range st.env {
allEnv[k] = struct{}{}
}
}

for _, st := range subtests {
t.Run(st.name, func(t *testing.T) {
for k := range allEnv {
require.NoError(t, os.Unsetenv(k))
}

for k, v := range st.env {
require.NoError(t, os.Setenv(k, v))
}

var actual Validator
if vActual := reflect.ValueOf(st.io); vActual != (reflect.Value{}) {
if vActual.Kind() == reflect.Ptr && !vActual.IsNil() {
vActual = reflect.New(vActual.Type().Elem())
}

actual = vActual.Interface().(Validator)
}

if err := FromEnv(actual, st.options); st.error {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, st.io, actual)
}
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/icinga/icinga-go-library
go 1.22

require (
github.com/caarlos0/env/v11 v11.1.0
github.com/creasty/defaults v1.7.0
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-yaml v1.12.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/caarlos0/env/v11 v11.1.0 h1:a5qZqieE9ZfzdvbbdhTalRrHT5vu/4V1/ad1Ka6frhI=
github.com/caarlos0/env/v11 v11.1.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
Expand Down

0 comments on commit 16856c0

Please sign in to comment.