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
70 changes: 63 additions & 7 deletions cleanenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,39 +398,89 @@ func readStructMetadata(cfgRoot interface{}) ([]structMeta, error) {
return metas, nil
}

type readEnvVarsError struct {
missingEnvs []string
parsingErrs []error
}

func (e readEnvVarsError) IsEmpty() bool {
return len(e.missingEnvs) == 0 && len(e.parsingErrs) == 0
}

func (e readEnvVarsError) Error() string {
var tmp string
for i, env := range e.missingEnvs {
tmp += fmt.Sprintf("\t%q", env)
// add new line if this is not the last element
if i != len(e.missingEnvs)-1 {
tmp += "\n"
}
}
missings := fmt.Sprintf("missing required environment variables: \n%s", tmp)

tmp = ""
for i, err := range e.parsingErrs {
tmp += fmt.Sprintf("\t%v", err)
// add new line if this is not the last element
if i != len(e.parsingErrs)-1 {
tmp += "\n"
}
}
parsingErrs := fmt.Sprintf("parsing errors for environment variables: \n%s", tmp)

var res string

switch {
case len(e.missingEnvs) != 0 && len(e.parsingErrs) != 0:
res = fmt.Sprintf("%s\n%s", missings, parsingErrs)
case len(e.missingEnvs) != 0:
res = missings
case len(e.parsingErrs) != 0:
res = parsingErrs
}

return res
}

// readEnvVars reads environment variables to the provided configuration structure
func readEnvVars(cfg interface{}, update bool) error {
metaInfo, err := readStructMetadata(cfg)
if err != nil {
return err
}

// store initial configuration, so we can return default values if errors occur
initialCfg := reflect.ValueOf(cfg).Elem().Interface()

if updater, ok := cfg.(Updater); ok {
if err := updater.Update(); err != nil {
return err
}
}

var errs readEnvVarsError

for _, meta := range metaInfo {
// update only updatable fields
if update && !meta.updatable {
continue
}

var rawValue *string
var (
rawValue *string
env string
)

for _, env := range meta.envList {
for _, env = range meta.envList {
if value, ok := os.LookupEnv(env); ok {
rawValue = &value
break
}
}

if rawValue == nil && meta.required && meta.isFieldValueZero() {
return fmt.Errorf(
"field %q is required but the value is not provided",
meta.fieldName,
)
errs.missingEnvs = append(errs.missingEnvs, env)
continue
}

if rawValue == nil && meta.isFieldValueZero() {
Expand All @@ -447,10 +497,16 @@ func readEnvVars(cfg interface{}, update bool) error {
}

if err := parseValue(meta.fieldValue, *rawValue, meta.separator, meta.layout); err != nil {
return fmt.Errorf("parsing field %v env %v: %v", meta.fieldName, envName, err)
errs.parsingErrs = append(errs.parsingErrs, fmt.Errorf("field %v env %q: %v", meta.fieldName, envName, err))
}
}

if !errs.IsEmpty() {
// restore initial configuration
reflect.ValueOf(cfg).Elem().Set(reflect.ValueOf(initialCfg))
return errs
}

return nil
}

Expand Down
44 changes: 42 additions & 2 deletions cleanenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1191,8 +1191,8 @@ func TestTimeLocation(t *testing.T) {
func TestSkipUnexportedField(t *testing.T) {
conf := struct {
Database struct {
Host string `yaml:"host" env:"DB_HOST" env-description:"Database host"`
Port string `yaml:"port" env:"DB_PORT" env-description:"Database port"`
Host string `yaml:"host" env:"DB_HOST" env-description:"Database host"`
Port string `yaml:"port" env:"DB_PORT" env-description:"Database port"`
} `yaml:"database"`
server struct {
Host string `yaml:"host" env:"SRV_HOST,HOST" env-description:"Server host" env-default:"localhost"`
Expand All @@ -1210,3 +1210,43 @@ func TestSkipUnexportedField(t *testing.T) {
t.Fatal("expect value on exported fields")
}
}

func TestReturnMissingVariables(t *testing.T) {
conf := struct {
Port string `env:"PORT" env-required:"true"`
JWTsalt string `env:"JWT_SALT" env-required:"true"`
ReadTimout time.Duration `env:"READ_TIMEOUT" env-required:"true"`
WriteTimeout time.Duration `env:"WRITE_TIMEOUT" env-default:"15s"`
}{}
expectedErrMsg := `missing required environment variables:
"PORT"
"JWT_SALT"
"READ_TIMEOUT"
parsing errors for environment variables:
field WriteTimeout env "WRITE_TIMEOUT": time: invalid duration "incorrect"`

os.Setenv("WRITE_TIMEOUT", "incorrect")
defer os.Clearenv()

err := ReadEnv(&conf)

if err == nil {
t.Fatal("expect error")
}

switch errt := err.(type) {
case readEnvVarsError:
t.Log(errt.Error())
if len(errt.missingEnvs) != 3 {
t.Fatalf("wrong number of missing envs: got %v want %v", len(errt.missingEnvs), 4)
}
if len(errt.parsingErrs) != 1 {
t.Fatalf("wrong number of parsing errors: got %v want %v", len(errt.parsingErrs), 1)
}
if errt.Error() != expectedErrMsg {
t.Fatalf("wrong error message: got %v want %v", errt.Error(), expectedErrMsg)
}
default:
t.Fatal("expect type of error to be readEnvVarsError")
}
}