Skip to content

Commit

Permalink
Merge branch 'feature/log-the-name-of-environment-variable' into 'mas…
Browse files Browse the repository at this point in the history
…ter'

feat: log the names of environment variables

See merge request backend/envconf!8
  • Loading branch information
komod committed Aug 17, 2020
2 parents a9f4731 + 76f8d93 commit 330b697
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 9 deletions.
38 changes: 29 additions & 9 deletions envconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,43 @@ import (
// Warning:
// 1. Fields without env tag will be ignored.
// 2. Duplicated field tags will be assigned with the same environment variable.
func Load(prefix string, out interface{}) {
val := reflect.ValueOf(out).Elem()
loader := loader{
usedKeys: map[string]struct{}{},
func Load(prefix string, out interface{}, opts ...Option) {
loader := loader{}
loader.envStatuses = map[string]*EnvStatus{}
loader.handleEnvironmentVariables = func(map[string]*EnvStatus) {}
for _, opt := range opts {
opt(&loader)
}

val := reflect.ValueOf(out).Elem()
loader.loadStruct(strings.ToUpper(prefix), &val)
}

var stringSliceType = reflect.TypeOf([]string{})
loader.handleEnvironmentVariables(loader.envStatuses)
}

type loader struct {
usedKeys map[string]struct{}
handleEnvironmentVariables func(map[string]*EnvStatus)
envStatuses map[string]*EnvStatus
}

func (l *loader) useKey(name string) error {
if _, ok := l.usedKeys[name]; ok {
if _, ok := l.envStatuses[name]; ok {
return fmt.Errorf("Duplicated key %v", name)
}
l.usedKeys[name] = struct{}{}
l.envStatuses[name] = &EnvStatus{}
return nil
}

type EnvStatus struct {
Loaded bool
}

func (e *EnvStatus) SetLoaded() {
e.Loaded = true
}

var stringSliceType = reflect.TypeOf([]string{})

func (l *loader) loadField(name string, out *reflect.Value) {
kind := out.Kind()
if kind == reflect.Struct {
Expand Down Expand Up @@ -134,6 +149,7 @@ func (l *loader) loadString(name string, out *reflect.Value) {
if !found {
return
}
l.envStatuses[name].SetLoaded()

out.SetString(data)
}
Expand All @@ -143,6 +159,7 @@ func (l *loader) loadInt(name string, out *reflect.Value) {
if !found {
return
}
l.envStatuses[name].SetLoaded()

d, err := strconv.ParseInt(data, 10, 64)
if err != nil {
Expand All @@ -157,6 +174,7 @@ func (l *loader) loadUint(name string, out *reflect.Value) {
if !found {
return
}
l.envStatuses[name].SetLoaded()

d, err := strconv.ParseUint(data, 10, 64)
if err != nil {
Expand All @@ -171,6 +189,7 @@ func (l *loader) loadBool(name string, out *reflect.Value) {
if !found {
return
}
l.envStatuses[name].SetLoaded()

isTrue := data == "true" || data == "TRUE" || data == "True" || data == "1"
isFalse := data == "false" || data == "FALSE" || data == "False" || data == "0"
Expand All @@ -187,6 +206,7 @@ func (l *loader) loadStringSlice(name string, out *reflect.Value) {
if !found {
return
}
l.envStatuses[name].SetLoaded()

strListRaw := strings.Split(data, ",")
strList := []string{}
Expand Down
42 changes: 42 additions & 0 deletions envconf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type EmbeddedConfig struct {

func assertEqual(t testing.TB, fieldName string, expected, received interface{}) {
if !reflect.DeepEqual(expected, received) {
t.Helper()
t.Errorf("%v is not loaded correctly.\nExpecting: %v %v\nReceived: %v %v",
fieldName,
reflect.TypeOf(expected), expected,
Expand All @@ -45,6 +46,7 @@ func assertEqual(t testing.TB, fieldName string, expected, received interface{})
}
func assertPanic(t testing.TB) {
if r := recover(); r == nil {
t.Helper()
t.Errorf("this test should panic")
}
}
Expand Down Expand Up @@ -101,6 +103,18 @@ func TestTaggedUnsupportedTypeShouldPanic(t *testing.T) {
Load("FAIL", &invalid)
}

func TestUnsupportedSliceShouldPanic(t *testing.T) {
defer assertPanic(t)

type Invalid struct {
Unsupported []map[string]string
}

os.Setenv("FAIL_UNSUPPORTED", "[]")
var invalid Invalid
Load("FAIL", &invalid)
}

func TestInvalidIntShouldPanic(t *testing.T) {
defer assertPanic(t)

Expand Down Expand Up @@ -299,3 +313,31 @@ func TestDuplicatedKeysBetweenStructs(t *testing.T) {
}{}
Load("TEST", &config)
}

func TestLogger(t *testing.T) {
os.Setenv("TEST_INTEGER", "-3")
os.Setenv("TEST_UNSIGNED_INTEGER", "3")

result := map[string]*EnvStatus{}
config := struct {
String string `env:"string"`
Integer int `env:"integer"`
Struct struct {
Unsigned uint `env:"unsigned_integer"`
Bool bool `env:"bool"`
StrSlice []string `env:"string_slice"`
} `env:",inline"`
}{}
Load("TEST", &config, CustomHandleEnvVarsOption(func(status map[string]*EnvStatus) {
result = status
}))

expected := map[string]*EnvStatus{
"TEST_STRING": {false},
"TEST_INTEGER": {true},
"TEST_UNSIGNED_INTEGER": {true},
"TEST_BOOL": {false},
"TEST_STRING_SLICE": {false},
}
assertEqual(t, "", expected, result)
}
14 changes: 14 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package envconf

type Option func(*loader)

// CustomHandleEnvVarsOption creates an option that calls `cb` with a map
// indicating the environment variables checked by envconf.
//
// The map keys are the environment variable names that are checked by envconf,
// and the values present whether the corresponding environment variables are set.
func CustomHandleEnvVarsOption(cb func(map[string]*EnvStatus)) Option {
return func(l *loader) {
l.handleEnvironmentVariables = cb
}
}
27 changes: 27 additions & 0 deletions options/predefined.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package options

import (
"encoding/json"
"io"
"log"

"gitlab.rayark.com/backend/envconf"
)

// MakeJSONLogger will log the status of environment variables used by given structure.
// The message is in JSON format.
func MakeJSONLogger(w io.Writer) func(map[string]*envconf.EnvStatus) {
return func(status map[string]*envconf.EnvStatus) {
logger := log.New(w, "", 0)

result := map[string]interface{}{}
result["message"] = "envconf: show environment variables used by configuration and whether they are set"
result["environment-variables"] = status
b, err := json.MarshalIndent(result, "", " ")
if err != nil {
logger.Printf("envconf encounters an error: %v", err.Error())
return
}
logger.Printf(string(b))
}
}
26 changes: 26 additions & 0 deletions options/predefined_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package options_test

import (
"bytes"
"os"
"testing"

"gitlab.rayark.com/backend/envconf"
"gitlab.rayark.com/backend/envconf/options"
)

func TestLogger(t *testing.T) {
os.Setenv("TEST_INTEGER", "-3")
os.Setenv("TEST_UNSIGNED_INTEGER", "3")

config := struct {
String string `env:"string"`
Integer int `env:"integer"`
}{}
buf := bytes.NewBuffer(nil)
envconf.Load("TEST", &config, envconf.CustomHandleEnvVarsOption(options.MakeJSONLogger(buf)))

if buf.String() == "" {
t.Errorf("failed to log environment variable status")
}
}

0 comments on commit 330b697

Please sign in to comment.