diff --git a/env.go b/env.go index 3e6391e..625aaa2 100644 --- a/env.go +++ b/env.go @@ -200,46 +200,55 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ... func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Options) error { refType := ref.Type() + var agrErr aggregateError + for i := 0; i < refType.NumField(); i++ { refField := ref.Field(i) - if !refField.CanSet() { - continue - } - if reflect.Ptr == refField.Kind() && !refField.IsNil() { - if refField.Elem().Kind() == reflect.Struct { - if err := ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refType.Field(i), opts)...); err != nil { - return err - } - continue - } - if err := ParseWithFuncs(refField.Interface(), funcMap, opts...); err != nil { - return err - } - continue - } - if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { - if err := ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refType.Field(i), opts)...); err != nil { - return err - } - continue - } refTypeField := refType.Field(i) - value, err := get(refTypeField, opts) - if err != nil { - return err - } - if value == "" { - if reflect.Struct == refField.Kind() { - if err := doParse(refField, funcMap, optsWithPrefix(refType.Field(i), opts)); err != nil { - return err - } + + if err := doParseField(refField, refTypeField, funcMap, opts); err != nil { + if val, ok := err.(aggregateError); ok { + agrErr.errors = append(agrErr.errors, val.errors...) + } else { + agrErr.errors = append(agrErr.errors, err) } - continue } - if err := set(refField, refTypeField, value, funcMap); err != nil { - return err + } + + if len(agrErr.errors) == 0 { + return nil + } + + return agrErr +} + +func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error { + if !refField.CanSet() { + return nil + } + if reflect.Ptr == refField.Kind() && !refField.IsNil() { + if refField.Elem().Kind() == reflect.Struct { + return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) } + + return ParseWithFuncs(refField.Interface(), funcMap, opts...) } + if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { + return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) + } + value, err := get(refTypeField, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, funcMap) + } + + if reflect.Struct == refField.Kind() { + return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts)) + } + return nil } @@ -267,7 +276,7 @@ func get(field reflect.StructField, opts []Options) (val string, err error) { case "notEmpty": notEmpty = true default: - return "", fmt.Errorf("env: tag option %q not supported", tag) + return "", fmt.Errorf("tag option %q not supported", tag) } } expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") @@ -283,18 +292,18 @@ func get(field reflect.StructField, opts []Options) (val string, err error) { } if required && !exists && len(ownKey) > 0 { - return "", fmt.Errorf(`env: required environment variable %q is not set`, key) + return "", fmt.Errorf(`required environment variable %q is not set`, key) } if notEmpty && val == "" { - return "", fmt.Errorf("env: environment variable %q should not be empty", key) + return "", fmt.Errorf("environment variable %q should not be empty", key) } if loadFile && val != "" { filename := val val, err = getFromFile(filename) if err != nil { - return "", fmt.Errorf(`env: could not load content of file "%s" from variable %s: %v`, filename, key, err) + return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err) } } @@ -467,11 +476,11 @@ type parseError struct { } func (e parseError) Error() string { - return fmt.Sprintf(`env: parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) + return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.sf.Name, e.sf.Type, e.err) } func newNoParserError(sf reflect.StructField) error { - return fmt.Errorf(`env: no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) + return fmt.Errorf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) } func optsWithPrefix(field reflect.StructField, opts []Options) []Options { @@ -482,3 +491,18 @@ func optsWithPrefix(field reflect.StructField, opts []Options) []Options { } return subOpts } + +type aggregateError struct { + errors []error +} + +func (e aggregateError) Error() string { + var sb strings.Builder + sb.WriteString("env:") + + for _, err := range e.errors { + sb.WriteString(fmt.Sprintf(" %v;", err.Error())) + } + + return strings.TrimRight(sb.String(), ";") +} diff --git a/env_test.go b/env_test.go index ca22b65..cf25c1e 100644 --- a/env_test.go +++ b/env_test.go @@ -444,6 +444,20 @@ func TestParsesEnvInnerFails(t *testing.T) { isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`) } +func TestParsesEnvInnerFailsMultipleErrors(t *testing.T) { + type config struct { + Foo struct { + Name string `env:"NAME,required"` + Number int `env:"NUMBER"` + Bar struct { + Age int `env:"AGE,required"` + } + } + } + setEnv(t, "NUMBER", "not-a-number") + isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "NAME" is not set; parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax; required environment variable "AGE" is not set`) +} + func TestParsesEnvInnerNil(t *testing.T) { setEnv(t, "innervar", "someinnervalue") cfg := ParentStruct{} @@ -492,37 +506,37 @@ func TestPassReference(t *testing.T) { func TestInvalidBool(t *testing.T) { setEnv(t, "BOOL", "should-be-a-bool") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Bool" of type "bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Bool" of type "bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax; parse error on field "BoolPtr" of type "*bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`) } func TestInvalidInt(t *testing.T) { setEnv(t, "INT", "should-be-an-int") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int" of type "int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int" of type "int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax; parse error on field "IntPtr" of type "*int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`) } func TestInvalidUint(t *testing.T) { setEnv(t, "UINT", "-44") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint" of type "uint": strconv.ParseUint: parsing "-44": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint" of type "uint": strconv.ParseUint: parsing "-44": invalid syntax; parse error on field "UintPtr" of type "*uint": strconv.ParseUint: parsing "-44": invalid syntax`) } func TestInvalidFloat32(t *testing.T) { setEnv(t, "FLOAT32", "AAA") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float32" of type "float32": strconv.ParseFloat: parsing "AAA": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float32" of type "float32": strconv.ParseFloat: parsing "AAA": invalid syntax; parse error on field "Float32Ptr" of type "*float32": strconv.ParseFloat: parsing "AAA": invalid syntax`) } func TestInvalidFloat64(t *testing.T) { setEnv(t, "FLOAT64", "AAA") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float64" of type "float64": strconv.ParseFloat: parsing "AAA": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Float64" of type "float64": strconv.ParseFloat: parsing "AAA": invalid syntax; parse error on field "Float64Ptr" of type "*float64": strconv.ParseFloat: parsing "AAA": invalid syntax`) } func TestInvalidUint64(t *testing.T) { setEnv(t, "UINT64", "AAA") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint64" of type "uint64": strconv.ParseUint: parsing "AAA": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Uint64" of type "uint64": strconv.ParseUint: parsing "AAA": invalid syntax; parse error on field "Uint64Ptr" of type "*uint64": strconv.ParseUint: parsing "AAA": invalid syntax`) } func TestInvalidInt64(t *testing.T) { setEnv(t, "INT64", "AAA") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int64" of type "int64": strconv.ParseInt: parsing "AAA": invalid syntax`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Int64" of type "int64": strconv.ParseInt: parsing "AAA": invalid syntax; parse error on field "Int64Ptr" of type "*int64": strconv.ParseInt: parsing "AAA": invalid syntax`) } func TestInvalidInt64Slice(t *testing.T) { @@ -567,12 +581,12 @@ func TestInvalidBoolsSlice(t *testing.T) { func TestInvalidDuration(t *testing.T) { setEnv(t, "DURATION", "should-be-a-valid-duration") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Duration" of type "time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Duration" of type "time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"; parse error on field "DurationPtr" of type "*time.Duration": unable to parse duration: time: invalid duration "should-be-a-valid-duration"`) } func TestInvalidDurations(t *testing.T) { setEnv(t, "DURATIONS", "1s,contains-an-invalid-duration,3s") - isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Durations" of type "[]time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`) + isErrorWithMessage(t, Parse(&Config{}), `env: parse error on field "Durations" of type "[]time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"; parse error on field "DurationPtrs" of type "[]*time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`) } func TestParseStructWithoutEnvTag(t *testing.T) { @@ -1330,7 +1344,7 @@ func TestRequiredIfNoDefOption(t *testing.T) { var cfg config t.Run("missing", func(t *testing.T) { - isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "NAME" is not set`) + isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "NAME" is not set; required environment variable "FRUIT" is not set`) setEnv(t, "NAME", "John") isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "FRUIT" is not set`) })