Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor error messages for missing variables. #117

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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.
Jump to
Jump to file
Failed to load files.
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")
}
}