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

Error aggregation #233

Merged
merged 2 commits into from Aug 22, 2022
Merged
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
102 changes: 63 additions & 39 deletions env.go
Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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(), ";")
}
34 changes: 24 additions & 10 deletions env_test.go
Expand Up @@ -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{}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`)
})
Expand Down