Skip to content
Open
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
26 changes: 19 additions & 7 deletions cleanenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,30 @@ func ReadConfig(path string, cfg interface{}) error {
return err
}

return readEnvVars(cfg, false)
return readEnvVars(cfg, false, os.LookupEnv)
}

// Lookup function that lookup env vars (like os.LookupEnv)
type Lookup func(string) (string, bool)

// ReadEnvFrom reads environment variables via lookup into the structure.
func ReadEnvFrom(cfg interface{}, lookup Lookup) error {
return readEnvVars(cfg, false, lookup)
}

// ReadEnv reads environment variables into the structure.
func ReadEnv(cfg interface{}) error {
return readEnvVars(cfg, false)
return ReadEnvFrom(cfg, os.LookupEnv)
}

// UpdateEnvFrom rereads (updates) environment variables in the structure via lookup function.
func UpdateEnvFrom(cfg interface{}, lookup Lookup) error {
return readEnvVars(cfg, true, lookup)
}

// UpdateEnv rereads (updates) environment variables in the structure.
func UpdateEnv(cfg interface{}) error {
return readEnvVars(cfg, true)
return UpdateEnvFrom(cfg, os.LookupEnv)
}

// parseFile parses configuration file according to its extension
Expand Down Expand Up @@ -262,7 +275,6 @@ type parseFunc func(*reflect.Value, string, *string) error

// Any specific supported struct can be added here
var validStructs = map[reflect.Type]parseFunc{

reflect.TypeOf(time.Time{}): func(field *reflect.Value, value string, layout *string) error {
var l string
if layout != nil {
Expand Down Expand Up @@ -337,7 +349,7 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {

// process nested structure (except of supported ones)
if fld := s.Field(idx); fld.Kind() == reflect.Struct {
//skip unexported
// skip unexported
if !fld.CanInterface() {
continue
}
Expand Down Expand Up @@ -412,7 +424,7 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
}

// readEnvVars reads environment variables to the provided configuration structure
func readEnvVars(cfg interface{}, update bool) error {
func readEnvVars(cfg interface{}, update bool, lookup Lookup) error {
metaInfo, err := readStructMetadata(cfg)
if err != nil {
return err
Expand All @@ -433,7 +445,7 @@ func readEnvVars(cfg interface{}, update bool) error {
var rawValue *string

for _, env := range meta.envList {
if value, ok := os.LookupEnv(env); ok {
if value, ok := lookup(env); ok {
rawValue = &value
break
}
Expand Down
101 changes: 92 additions & 9 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,88 @@ func (t *testUpdater) Update() error {
return t.err
}

func TestReadEnvVarsFrom(t *testing.T) {
lookup := func(key string) (string, bool) {
env := map[string]string{
"TEST_INTEGER": "-5",
"TEST_UNSINTEGER": "5",
"TEST_FLOAT": "5.5",
"TEST_BOOLEAN": "true",
"TEST_STRING": "test",
"TEST_DURATION": "1h5m10s",
"TEST_TIME": "2012-04-23T18:25:43.511Z",
// Location depends on the system, so we test it with time.UTC
"TEST_LOCATION": "UTC",
"TEST_ARRAYINT": "1,2,3",
"TEST_ARRAYSTRING": "a,b,c",
"TEST_MAPSTRINGINT": "a:1,b:2,c:3",
"TEST_MAPSTRINGSTRING": "a:x,b:y,c:z",
}
v, ok := env[key]
return v, ok
}
durationFunc := func(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
t.Fatal(err)
}
return d
}

timeFunc := func(s, l string) time.Time {
tm, err := time.Parse(l, s)
if err != nil {
t.Fatal(err)
}
return tm
}
type AllTypes struct {
Integer int64 `env:"TEST_INTEGER"`
UnsInteger uint64 `env:"TEST_UNSINTEGER"`
Float float64 `env:"TEST_FLOAT"`
Boolean bool `env:"TEST_BOOLEAN"`
String string `env:"TEST_STRING"`
Duration time.Duration `env:"TEST_DURATION"`
Time time.Time `env:"TEST_TIME"`
// Location depends on the system, so we test it with time.UTC
Location *time.Location `env:"TEST_LOCATION"`
ArrayInt []int `env:"TEST_ARRAYINT"`
ArrayString []string `env:"TEST_ARRAYSTRING"`
MapStringInt map[string]int `env:"TEST_MAPSTRINGINT"`
MapStringString map[string]string `env:"TEST_MAPSTRINGSTRING"`
}
want := AllTypes{
Integer: -5,
UnsInteger: 5,
Float: 5.5,
Boolean: true,
String: "test",
Duration: durationFunc("1h5m10s"),
Time: timeFunc("2012-04-23T18:25:43.511Z", time.RFC3339),
Location: time.UTC,
ArrayInt: []int{1, 2, 3},
ArrayString: []string{"a", "b", "c"},
MapStringInt: map[string]int{
"a": 1,
"b": 2,
"c": 3,
},
MapStringString: map[string]string{
"a": "x",
"b": "y",
"c": "z",
},
}
var res AllTypes
err := ReadEnvFrom(&res, lookup)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(want, res) {
t.Errorf("wrong data %v, want %v", res, want)
}
}

func TestReadEnvVars(t *testing.T) {
durationFunc := func(s string) time.Duration {
d, err := time.ParseDuration(s)
Expand Down Expand Up @@ -313,7 +395,7 @@ func TestReadEnvVars(t *testing.T) {
}
defer os.Clearenv()

if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr {
if err := readEnvVars(tt.cfg, false, os.LookupEnv); (err != nil) != tt.wantErr {
t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(tt.cfg, tt.want) {
Expand Down Expand Up @@ -391,7 +473,7 @@ func TestReadEnvErrors(t *testing.T) {
}
defer os.Clearenv()

err := readEnvVars(tt.cfg, false)
err := readEnvVars(tt.cfg, false, os.LookupEnv)

if err == nil {
t.Fatalf("expected error but got nil")
Expand Down Expand Up @@ -455,7 +537,7 @@ func TestReadEnvVarsURL(t *testing.T) {
}
defer os.Clearenv()

if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr {
if err := readEnvVars(tt.cfg, false, os.LookupEnv); (err != nil) != tt.wantErr {
t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(tt.cfg, tt.want) {
Expand Down Expand Up @@ -506,7 +588,7 @@ func TestReadEnvVarsTime(t *testing.T) {
}
defer os.Clearenv()

if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr {
if err := readEnvVars(tt.cfg, false, os.LookupEnv); (err != nil) != tt.wantErr {
t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(tt.cfg, tt.want) {
Expand All @@ -515,6 +597,7 @@ func TestReadEnvVarsTime(t *testing.T) {
})
}
}

func TestReadEnvVarsWithPrefix(t *testing.T) {
type Logging struct {
Debug bool `env:"DEBUG"`
Expand All @@ -532,7 +615,7 @@ func TestReadEnvVarsWithPrefix(t *testing.T) {
Extra DBConfig `env-prefix:"EXTRA_"`
}

var env = map[string]string{
env := map[string]string{
"DB_HOST": "db1.host",
"DB_PORT": "10000",
"DB_DEBUG": "true",
Expand All @@ -548,11 +631,11 @@ func TestReadEnvVarsWithPrefix(t *testing.T) {
}

var cfg Config
if err := readEnvVars(&cfg, false); err != nil {
if err := readEnvVars(&cfg, false, os.LookupEnv); err != nil {
t.Fatal("failed to read env vars", err)
}

var expected = Config{
expected := Config{
Default: DBConfig{
Host: "db1.host",
Port: 10000,
Expand Down Expand Up @@ -595,7 +678,6 @@ type testConfigUpdateNoFunction struct {
}

func TestReadUpdateFunctions(t *testing.T) {

tests := []struct {
name string
cfg interface{}
Expand Down Expand Up @@ -635,7 +717,7 @@ func TestReadUpdateFunctions(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := readEnvVars(tt.cfg, false); (err != nil) != tt.wantErr {
if err := readEnvVars(tt.cfg, false, os.LookupEnv); (err != nil) != tt.wantErr {
t.Errorf("wrong error behavior %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(tt.cfg, tt.want) {
Expand Down Expand Up @@ -1387,6 +1469,7 @@ func (v *StructSetter) SetValue(s string) error {
v.Value = s
return nil
}

func TestStructSetter(t *testing.T) {
want := StructSetter{
Value: "bar",
Expand Down