diff --git a/env.go b/env.go index f0b09df..b20e51b 100644 --- a/env.go +++ b/env.go @@ -2,8 +2,6 @@ package env import ( "encoding" - "errors" - "fmt" "net/url" "os" "reflect" @@ -14,10 +12,6 @@ import ( // nolint: gochecknoglobals var ( - // ErrNotAStructPtr is returned if you pass something that is not a pointer to a - // Struct to Parse. - ErrNotAStructPtr = errors.New("env: expected a pointer to a Struct") - defaultBuiltInParsers = map[reflect.Kind]ParserFunc{ reflect.Bool: func(v string) (interface{}, error) { return strconv.ParseBool(v) @@ -79,14 +73,14 @@ func defaultTypeParsers() map[reflect.Type]ParserFunc { reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { u, err := url.Parse(v) if err != nil { - return nil, fmt.Errorf("unable to parse URL: %v", err) + return nil, newParseValueError("unable to parse URL", err) } return *u, nil }, reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { s, err := time.ParseDuration(v) if err != nil { - return nil, fmt.Errorf("unable to parse duration: %v", err) + return nil, newParseValueError("unable to parse duration", err) } return s, err }, @@ -183,11 +177,11 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ... ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { - return ErrNotAStructPtr + return newAggregateError(NotStructPtrError{}) } ref := ptrRef.Elem() if ref.Kind() != reflect.Struct { - return ErrNotAStructPtr + return newAggregateError(NotStructPtrError{}) } parsers := defaultTypeParsers() for k, v := range funcMap { @@ -200,22 +194,22 @@ 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 + var agrErr AggregateError for i := 0; i < refType.NumField(); i++ { refField := ref.Field(i) refTypeField := refType.Field(i) if err := doParseField(refField, refTypeField, funcMap, opts); err != nil { - if val, ok := err.(aggregateError); ok { - agrErr.errors = append(agrErr.errors, val.errors...) + if val, ok := err.(AggregateError); ok { + agrErr.Errors = append(agrErr.Errors, val.Errors...) } else { - agrErr.errors = append(agrErr.errors, err) + agrErr.Errors = append(agrErr.Errors, err) } } } - if len(agrErr.errors) == 0 { + if len(agrErr.Errors) == 0 { return nil } @@ -226,8 +220,12 @@ func doParseField(refField reflect.Value, refTypeField reflect.StructField, func if !refField.CanSet() { return nil } - if reflect.Ptr == refField.Kind() && refField.Elem().Kind() == reflect.Struct { - return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...) + 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)...) @@ -272,7 +270,7 @@ func get(field reflect.StructField, opts []Options) (val string, err error) { case "notEmpty": notEmpty = true default: - return "", fmt.Errorf("tag option %q not supported", tag) + return "", newNoSupportedTagOptionError(tag) } } expand := strings.EqualFold(field.Tag.Get("envExpand"), "true") @@ -288,18 +286,18 @@ func get(field reflect.StructField, opts []Options) (val string, err error) { } if required && !exists && len(ownKey) > 0 { - return "", fmt.Errorf(`required environment variable %q is not set`, key) + return "", newEnvVarIsNotSet(key) } if notEmpty && val == "" { - return "", fmt.Errorf("environment variable %q should not be empty", key) + return "", newEmptyEnvVarError(key) } if loadFile && val != "" { filename := val val, err = getFromFile(filename) if err != nil { - return "", fmt.Errorf(`could not load content of file "%s" from variable %s: %v`, filename, key, err) + return "", newLoadFileContentError(filename, key, err) } } @@ -459,26 +457,6 @@ func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.Struct return nil } -func newParseError(sf reflect.StructField, err error) error { - return parseError{ - sf: sf, - err: err, - } -} - -type parseError struct { - sf reflect.StructField - err error -} - -func (e parseError) Error() string { - 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(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type) -} - func optsWithPrefix(field reflect.StructField, opts []Options) []Options { subOpts := make([]Options, len(opts)) copy(subOpts, opts) @@ -487,18 +465,3 @@ 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 ff1fb07..5cfade3 100644 --- a/env_test.go +++ b/env_test.go @@ -441,7 +441,9 @@ func TestParsesEnvInnerFails(t *testing.T) { } } t.Setenv("NUMBER", "not-a-number") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "Number" of type "int": strconv.ParseInt: parsing "not-a-number": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestParsesEnvInnerFailsMultipleErrors(t *testing.T) { @@ -455,7 +457,9 @@ func TestParsesEnvInnerFailsMultipleErrors(t *testing.T) { } } t.Setenv("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`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}, ParseError{}, EnvVarIsNotSetError{}}) } func TestParsesEnvInnerNil(t *testing.T) { @@ -469,7 +473,9 @@ func TestParsesEnvInnerInvalid(t *testing.T) { cfg := ParentStruct{ InnerStruct: &InnerStruct{}, } - isErrorWithMessage(t, Parse(&cfg), `env: parse error on field "Number" of type "uint": strconv.ParseUint: parsing "-547": invalid syntax`) + err := Parse(&cfg) + isErrorWithMessage(t, err, `env: parse error on field "Number" of type "uint": strconv.ParseUint: parsing "-547": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestParsesEnvNested(t *testing.T) { @@ -496,47 +502,65 @@ func TestEmptyVars(t *testing.T) { func TestPassAnInvalidPtr(t *testing.T) { var thisShouldBreak int - isErrorWithMessage(t, Parse(&thisShouldBreak), "env: expected a pointer to a Struct") + err := Parse(&thisShouldBreak) + isErrorWithMessage(t, err, "env: expected a pointer to a Struct") + isErrorWithType(t, err, []error{NotStructPtrError{}}) } func TestPassReference(t *testing.T) { cfg := Config{} - isErrorWithMessage(t, Parse(cfg), "env: expected a pointer to a Struct") + err := Parse(cfg) + isErrorWithMessage(t, err, "env: expected a pointer to a Struct") + isErrorWithType(t, err, []error{NotStructPtrError{}}) } func TestInvalidBool(t *testing.T) { t.Setenv("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; parse error on field "BoolPtr" of type "*bool": strconv.ParseBool: parsing "should-be-a-bool": invalid syntax`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidInt(t *testing.T) { t.Setenv("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; parse error on field "IntPtr" of type "*int": strconv.ParseInt: parsing "should-be-an-int": invalid syntax`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidUint(t *testing.T) { t.Setenv("UINT", "-44") - 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`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidFloat32(t *testing.T) { t.Setenv("FLOAT32", "AAA") - 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`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidFloat64(t *testing.T) { t.Setenv("FLOAT64", "AAA") - 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`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidUint64(t *testing.T) { t.Setenv("UINT64", "AAA") - 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`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidInt64(t *testing.T) { t.Setenv("INT64", "AAA") - 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`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidInt64Slice(t *testing.T) { @@ -544,7 +568,9 @@ func TestInvalidInt64Slice(t *testing.T) { type config struct { BadFloats []int64 `env:"BADINTS"` } - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]int64": strconv.ParseInt: parsing "A": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "BadFloats" of type "[]int64": strconv.ParseInt: parsing "A": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestInvalidUInt64Slice(t *testing.T) { @@ -552,7 +578,9 @@ func TestInvalidUInt64Slice(t *testing.T) { type config struct { BadFloats []uint64 `env:"BADINTS"` } - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]uint64": strconv.ParseUint: parsing "A": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "BadFloats" of type "[]uint64": strconv.ParseUint: parsing "A": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestInvalidFloat32Slice(t *testing.T) { @@ -560,7 +588,9 @@ func TestInvalidFloat32Slice(t *testing.T) { type config struct { BadFloats []float32 `env:"BADFLOATS"` } - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float32": strconv.ParseFloat: parsing "A": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "BadFloats" of type "[]float32": strconv.ParseFloat: parsing "A": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestInvalidFloat64Slice(t *testing.T) { @@ -568,7 +598,9 @@ func TestInvalidFloat64Slice(t *testing.T) { type config struct { BadFloats []float64 `env:"BADFLOATS"` } - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadFloats" of type "[]float64": strconv.ParseFloat: parsing "A": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "BadFloats" of type "[]float64": strconv.ParseFloat: parsing "A": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestInvalidBoolsSlice(t *testing.T) { @@ -576,17 +608,23 @@ func TestInvalidBoolsSlice(t *testing.T) { type config struct { BadBools []bool `env:"BADBOOLS"` } - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "BadBools" of type "[]bool": strconv.ParseBool: parsing "faaaalse": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "BadBools" of type "[]bool": strconv.ParseBool: parsing "faaaalse": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestInvalidDuration(t *testing.T) { t.Setenv("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"`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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"`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestInvalidDurations(t *testing.T) { t.Setenv("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"; parse error on field "DurationPtrs" of type "[]*time.Duration": unable to parse duration: time: invalid duration "contains-an-invalid-duration"`) + err := Parse(&Config{}) + isErrorWithMessage(t, err, `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"`) + isErrorWithType(t, err, []error{ParseError{}, ParseError{}}) } func TestParseStructWithoutEnvTag(t *testing.T) { @@ -600,7 +638,9 @@ func TestParseStructWithInvalidFieldKind(t *testing.T) { WontWorkByte byte `env:"BLAH"` } t.Setenv("BLAH", "a") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWorkByte" of type "uint8": strconv.ParseUint: parsing "a": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "WontWorkByte" of type "uint8": strconv.ParseUint: parsing "a": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestUnsupportedSliceType(t *testing.T) { @@ -609,7 +649,9 @@ func TestUnsupportedSliceType(t *testing.T) { } t.Setenv("WONTWORK", "1,2,3") - isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "WontWork" of type "[]map[int]int"`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: no parser found for field "WontWork" of type "[]map[int]int"`) + isErrorWithType(t, err, []error{NoParserError{}}) } func TestBadSeparator(t *testing.T) { @@ -618,7 +660,9 @@ func TestBadSeparator(t *testing.T) { } t.Setenv("WONTWORK", "1,2,3,4") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "WontWork" of type "[]int": strconv.ParseInt: parsing "1,2,3,4": invalid syntax`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "WontWork" of type "[]int": strconv.ParseInt: parsing "1,2,3,4": invalid syntax`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestNoErrorRequiredSet(t *testing.T) { @@ -678,7 +722,9 @@ func TestErrorRequiredNotSet(t *testing.T) { type config struct { IsRequired string `env:"IS_REQUIRED,required"` } - isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "IS_REQUIRED" is not set`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: required environment variable "IS_REQUIRED" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}}) } func TestNoErrorNotEmptySet(t *testing.T) { @@ -702,7 +748,9 @@ func TestErrorNotEmptySet(t *testing.T) { type config struct { IsRequired string `env:"IS_REQUIRED,notEmpty"` } - isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: environment variable "IS_REQUIRED" should not be empty`) + isErrorWithType(t, err, []error{EmptyEnvVarError{}}) } func TestErrorRequiredAndNotEmptySet(t *testing.T) { @@ -710,7 +758,9 @@ func TestErrorRequiredAndNotEmptySet(t *testing.T) { type config struct { IsRequired string `env:"IS_REQUIRED,notEmpty,required"` } - isErrorWithMessage(t, Parse(&config{}), `env: environment variable "IS_REQUIRED" should not be empty`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: environment variable "IS_REQUIRED" should not be empty`) + isErrorWithType(t, err, []error{EmptyEnvVarError{}}) } func TestErrorRequiredNotSetWithDefault(t *testing.T) { @@ -756,7 +806,9 @@ func TestParseUnsetRequireOptions(t *testing.T) { } cfg := config{} - isErrorWithMessage(t, Parse(&cfg), `env: required environment variable "PASSWORD" is not set`) + err := Parse(&cfg) + isErrorWithMessage(t, err, `env: required environment variable "PASSWORD" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}}) t.Setenv("PASSWORD", "superSecret") isNoErr(t, Parse(&cfg)) @@ -839,12 +891,16 @@ func TestIssue226(t *testing.T) { func TestParseWithFuncsNoPtr(t *testing.T) { type foo struct{} - isErrorWithMessage(t, ParseWithFuncs(foo{}, nil), "env: expected a pointer to a Struct") + err := ParseWithFuncs(foo{}, nil) + isErrorWithMessage(t, err, "env: expected a pointer to a Struct") + isErrorWithType(t, err, []error{NotStructPtrError{}}) } func TestParseWithFuncsInvalidType(t *testing.T) { var c int - isErrorWithMessage(t, ParseWithFuncs(&c, nil), "env: expected a pointer to a Struct") + err := ParseWithFuncs(&c, nil) + isErrorWithMessage(t, err, "env: expected a pointer to a Struct") + isErrorWithType(t, err, []error{NotStructPtrError{}}) } func TestCustomParserError(t *testing.T) { @@ -869,6 +925,7 @@ func TestCustomParserError(t *testing.T) { isEqual(t, cfg.Var.name, "") isErrorWithMessage(t, err, `env: parse error on field "Var" of type "env.foo": something broke`) + isErrorWithType(t, err, []error{ParseError{}}) }) t.Run("slice", func(t *testing.T) { @@ -884,6 +941,7 @@ func TestCustomParserError(t *testing.T) { isEqual(t, cfg.Var, nil) isErrorWithMessage(t, err, `env: parse error on field "Var" of type "[]env.foo": something broke`) + isErrorWithType(t, err, []error{ParseError{}}) }) } @@ -967,6 +1025,7 @@ func TestTypeCustomParserBasicInvalid(t *testing.T) { isEqual(t, cfg.Const, ConstT(0)) isErrorWithMessage(t, err, `env: parse error on field "Const" of type "env.ConstT": random error`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestCustomParserNotCalledForNonAlias(t *testing.T) { @@ -1013,6 +1072,7 @@ func TestCustomParserBasicUnsupported(t *testing.T) { isEqual(t, cfg.Const, ConstT{0}) isErrorWithMessage(t, err, `env: no parser found for field "Const" of type "env.ConstT"`) + isErrorWithType(t, err, []error{NoParserError{}}) } func TestUnsupportedStructType(t *testing.T) { @@ -1020,7 +1080,9 @@ func TestUnsupportedStructType(t *testing.T) { Foo http.Client `env:"FOO"` } t.Setenv("FOO", "foo") - isErrorWithMessage(t, Parse(&config{}), `env: no parser found for field "Foo" of type "http.Client"`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: no parser found for field "Foo" of type "http.Client"`) + isErrorWithType(t, err, []error{NoParserError{}}) } func TestEmptyOption(t *testing.T) { @@ -1039,7 +1101,9 @@ func TestErrorOptionNotRecognized(t *testing.T) { type config struct { Var string `env:"VAR,not_supported!"` } - isErrorWithMessage(t, Parse(&config{}), `env: tag option "not_supported!" not supported`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: tag option "not_supported!" not supported`) + isErrorWithType(t, err, []error{NoSupportedTagOptionError{}}) } func TestTextUnmarshalerError(t *testing.T) { @@ -1047,7 +1111,9 @@ func TestTextUnmarshalerError(t *testing.T) { Unmarshaler unmarshaler `env:"UNMARSHALER"` } t.Setenv("UNMARSHALER", "invalid") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshaler" of type "env.unmarshaler": time: invalid duration "invalid"`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "Unmarshaler" of type "env.unmarshaler": time: invalid duration "invalid"`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestTextUnmarshalersError(t *testing.T) { @@ -1055,7 +1121,9 @@ func TestTextUnmarshalersError(t *testing.T) { Unmarshalers []unmarshaler `env:"UNMARSHALERS"` } t.Setenv("UNMARSHALERS", "1s,invalid") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "Unmarshalers" of type "[]env.unmarshaler": time: invalid duration "invalid"`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "Unmarshalers" of type "[]env.unmarshaler": time: invalid duration "invalid"`) + isErrorWithType(t, err, []error{ParseError{}}) } func TestParseURL(t *testing.T) { @@ -1073,7 +1141,9 @@ func TestParseInvalidURL(t *testing.T) { } t.Setenv("EXAMPLE_URL_2", "nope://s s/") - isErrorWithMessage(t, Parse(&config{}), `env: parse error on field "ExampleURL" of type "url.URL": unable to parse URL: parse "nope://s s/": invalid character " " in host name`) + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: parse error on field "ExampleURL" of type "url.URL": unable to parse URL: parse "nope://s s/": invalid character " " in host name`) + isErrorWithType(t, err, []error{ParseError{}}) } func ExampleParse() { @@ -1240,7 +1310,10 @@ func TestFileNoParamRequired(t *testing.T) { type config struct { SecretKey string `env:"SECRET_KEY,file,required"` } - isErrorWithMessage(t, Parse(&config{}), `env: required environment variable "SECRET_KEY" is not set`) + + err := Parse(&config{}) + isErrorWithMessage(t, err, `env: required environment variable "SECRET_KEY" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}}) } func TestFileBadFile(t *testing.T) { @@ -1255,7 +1328,10 @@ func TestFileBadFile(t *testing.T) { if runtime.GOOS == "windows" { oserr = "The system cannot find the file specified." } - isErrorWithMessage(t, Parse(&config{}), fmt.Sprintf(`env: could not load content of file "%s" from variable SECRET_KEY: open %s: %s`, filename, filename, oserr)) + + err := Parse(&config{}) + isErrorWithMessage(t, err, fmt.Sprintf(`env: could not load content of file "%s" from variable SECRET_KEY: open %s: %s`, filename, filename, oserr)) + isErrorWithType(t, err, []error{LoadFileContentError{}}) } func TestFileWithDefault(t *testing.T) { @@ -1325,9 +1401,13 @@ 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; required environment variable "FRUIT" is not set`) + err := Parse(&cfg, Options{RequiredIfNoDef: true}) + isErrorWithMessage(t, err, `env: required environment variable "NAME" is not set; required environment variable "FRUIT" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}, EnvVarIsNotSetError{}}) t.Setenv("NAME", "John") - isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "FRUIT" is not set`) + err = Parse(&cfg, Options{RequiredIfNoDef: true}) + isErrorWithMessage(t, err, `env: required environment variable "FRUIT" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}}) }) t.Run("all set", func(t *testing.T) { @@ -1357,7 +1437,9 @@ func TestRequiredIfNoDefNested(t *testing.T) { t.Setenv("SERVER_HOST", "https://google.com") t.Setenv("SERVER_TOKEN", "0xdeadfood") - isErrorWithMessage(t, Parse(&cfg, Options{RequiredIfNoDef: true}), `env: required environment variable "SERVER_PORT" is not set`) + err := Parse(&cfg, Options{RequiredIfNoDef: true}) + isErrorWithMessage(t, err, `env: required environment variable "SERVER_PORT" is not set`) + isErrorWithType(t, err, []error{EnvVarIsNotSetError{}}) }) t.Run("all set", func(t *testing.T) { @@ -1456,52 +1538,6 @@ func TestComplePrefix(t *testing.T) { isEqual(t, "blahhh", cfg.Blah) } -func TestNonStructPtrValues(t *testing.T) { - type Foo struct { - FltPtr *float64 `env:"FLT_PRT"` - } - - type ComplexConfig struct { - StrPtr *string `env:"STR_PTR"` - Foo Foo `env:"FOO_"` - } - - cfg1 := ComplexConfig{} - - isNoErr(t, Parse(&cfg1)) - isEqual(t, nil, cfg1.StrPtr) - isEqual(t, nil, cfg1.Foo.FltPtr) - - strPtr := "str_ptr" - fltPtr := 3.16 - cfg2 := ComplexConfig{ - StrPtr: &strPtr, - Foo: Foo{ - FltPtr: &fltPtr, - }, - } - - t.Setenv("STR_PTR", "env_str_ptr") - t.Setenv("FLT_PRT", "5.16") - - isNoErr(t, Parse(&cfg2)) - isEqual(t, "env_str_ptr", *cfg2.StrPtr) - isEqual(t, 5.16, *cfg2.Foo.FltPtr) - - var strPtrNill *string - var fltPtrNill *float64 - cfg3 := ComplexConfig{ - StrPtr: strPtrNill, - Foo: Foo{ - FltPtr: fltPtrNill, - }, - } - - isNoErr(t, Parse(&cfg3)) - isEqual(t, "env_str_ptr", *cfg3.StrPtr) - isEqual(t, 5.16, *cfg3.Foo.FltPtr) -} - func isTrue(tb testing.TB, b bool) { tb.Helper() @@ -1518,6 +1554,22 @@ func isFalse(tb testing.TB, b bool) { } } +func isErrorWithType(tb testing.TB, err error, expErrs []error) { + tb.Helper() + + innerErrs := err.(AggregateError).Errors + + if len(innerErrs) != len(expErrs) { + tb.Fatalf("expected the same amount of the inner errors, got %v, expected %v", len(innerErrs), len(expErrs)) + } + + for i, expErr := range expErrs { + if reflect.TypeOf(expErr) != reflect.TypeOf(innerErrs[i]) { + tb.Fatalf("type of inner the error is not equal, got %v, expected %v", reflect.TypeOf(innerErrs[i]), reflect.TypeOf(expErr)) + } + } +} + func isErrorWithMessage(tb testing.TB, err error, msg string) { tb.Helper() diff --git a/error.go b/error.go new file mode 100644 index 0000000..a187530 --- /dev/null +++ b/error.go @@ -0,0 +1,154 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// An aggregated error wrapper to combine gathered errors. This allows either to display all errors or convert them individually +// List of the available errors +// ParseError +// NotStructPtrError +// NoParserError +// NoSupportedTagOptionError +// EnvVarIsNotSetError +// EmptyEnvVarError +// LoadFileContentError +// ParseValueError +type AggregateError struct { + Errors []error +} + +func newAggregateError(initErr error) error { + return AggregateError{ + []error{ + initErr, + }, + } +} + +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(), ";") +} + +// The error occurs when it's impossible to convert the value for given type +type ParseError struct { + Name string + Type reflect.Type + Err error +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{sf.Name, sf.Type, err} +} + +func (e ParseError) Error() string { + return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.Name, e.Type, e.Err) +} + +// The error occurs when pass something that is not a pointer to a Struct to Parse +type NotStructPtrError struct{} + +func (e NotStructPtrError) Error() string { + return "expected a pointer to a Struct" +} + +// This error occurs when there is no parser provided for given type +// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults +// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs +type NoParserError struct { + Name string + Type reflect.Type +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{sf.Name, sf.Type} +} + +func (e NoParserError) Error() string { + return fmt.Sprintf(`no parser found for field "%s" of type "%s"`, e.Name, e.Type) +} + +// This error occurs when the given tag is not supported +// In-built supported tags: "", "file", "required", "unset", "notEmpty", "envDefault", "envExpand", "envSeparator" +// How to create a custom tag: https://github.com/caarlos0/env#changing-default-tag-name +type NoSupportedTagOptionError struct { + Tag string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{tag} +} + +func (e NoSupportedTagOptionError) Error() string { + return fmt.Sprintf("tag option %q not supported", e.Tag) +} + +// This error occurs when the required variable is not set +// Read about required fields: https://github.com/caarlos0/env#required-fields +type EnvVarIsNotSetError struct { + Key string +} + +func newEnvVarIsNotSet(key string) error { + return EnvVarIsNotSetError{key} +} + +func (e EnvVarIsNotSetError) Error() string { + return fmt.Sprintf(`required environment variable %q is not set`, e.Key) +} + +// This error occurs when the variable which must be not empty is existing but has an empty value +// Read about not empty fields: https://github.com/caarlos0/env#not-empty-fields +type EmptyEnvVarError struct { + Key string +} + +func newEmptyEnvVarError(key string) error { + return EmptyEnvVarError{key} +} + +func (e EmptyEnvVarError) Error() string { + return fmt.Sprintf("environment variable %q should not be empty", e.Key) +} + +// This error occurs when it's impossible to load the value from the file +// Read about From file feature: https://github.com/caarlos0/env#from-file +type LoadFileContentError struct { + Filename string + Key string + Err error +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{filename, key, err} +} + +func (e LoadFileContentError) Error() string { + return fmt.Sprintf(`could not load content of file "%s" from variable %s: %v`, e.Filename, e.Key, e.Err) +} + +// This error occurs when it's impossible to convert value using given parser +// Supported types and defaults: https://github.com/caarlos0/env#supported-types-and-defaults +// How to create a custom parser: https://github.com/caarlos0/env#custom-parser-funcs +type ParseValueError struct { + Msg string + Err error +} + +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} +} + +func (e ParseValueError) Error() string { + return fmt.Sprintf("%s: %v", e.Msg, e.Err) +}