From bd0e0456e7445d70d8dc12196ebe4fb90c4de4bd Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Wed, 7 Dec 2022 17:39:11 +0000 Subject: [PATCH 01/12] support error types --- env.go | 66 ++++---------------- env_test.go | 172 ++++++++++++++++++++++++++++++++++++++++------------ error.go | 138 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 285 insertions(+), 91 deletions(-) create mode 100644 error.go diff --git a/env.go b/env.go index 625aaa2..cdce7c7 100644 --- a/env.go +++ b/env.go @@ -2,7 +2,6 @@ package env import ( "encoding" - "errors" "fmt" "net/url" "os" @@ -14,10 +13,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 +74,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(fmt.Sprintf("unable to parse URL: %v", 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(fmt.Sprintf("unable to parse duration: %v", err)) } return s, err }, @@ -183,11 +178,11 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ... ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { - return ErrNotAStructPtr + return newAggregateError(newNotStructPtrError()) } ref := ptrRef.Elem() if ref.Kind() != reflect.Struct { - return ErrNotAStructPtr + return newAggregateError(newNotStructPtrError()) } parsers := defaultTypeParsers() for k, v := range funcMap { @@ -200,22 +195,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 } @@ -276,7 +271,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") @@ -292,18 +287,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) } } @@ -463,26 +458,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) @@ -491,18 +466,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 cf25c1e..a561a5e 100644 --- a/env_test.go +++ b/env_test.go @@ -441,7 +441,9 @@ func TestParsesEnvInnerFails(t *testing.T) { } } setEnv(t, "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) { } } 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`) + 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) { 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; 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) { 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; 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) { setEnv(t, "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) { setEnv(t, "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) { setEnv(t, "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) { setEnv(t, "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) { setEnv(t, "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) { 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"; 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) { 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"; 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"` } setEnv(t, "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) { } setEnv(t, "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) { } setEnv(t, "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{}}) setEnv(t, "PASSWORD", "superSecret") isNoErr(t, Parse(&cfg)) @@ -843,12 +895,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) { @@ -873,6 +929,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) { @@ -888,6 +945,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{}}) }) } @@ -971,6 +1029,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) { @@ -1017,6 +1076,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) { @@ -1024,7 +1084,9 @@ func TestUnsupportedStructType(t *testing.T) { Foo http.Client `env:"FOO"` } setEnv(t, "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) { @@ -1043,7 +1105,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) { @@ -1051,7 +1115,9 @@ func TestTextUnmarshalerError(t *testing.T) { Unmarshaler unmarshaler `env:"UNMARSHALER"` } setEnv(t, "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) { @@ -1059,7 +1125,9 @@ func TestTextUnmarshalersError(t *testing.T) { Unmarshalers []unmarshaler `env:"UNMARSHALERS"` } setEnv(t, "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) { @@ -1077,7 +1145,9 @@ func TestParseInvalidURL(t *testing.T) { } setEnv(t, "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() { @@ -1244,7 +1314,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) { @@ -1259,7 +1332,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) { @@ -1344,9 +1420,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{}}) setEnv(t, "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) { @@ -1376,7 +1456,9 @@ func TestRequiredIfNoDefNested(t *testing.T) { setEnv(t, "SERVER_HOST", "https://google.com") setEnv(t, "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) { @@ -1491,6 +1573,20 @@ func isFalse(tb testing.TB, b bool) { } } +func isErrorWithType(tb testing.TB, err error, expErrs []error) { + 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..bb92ffd --- /dev/null +++ b/error.go @@ -0,0 +1,138 @@ +package env + +import ( + "fmt" + "reflect" + "strings" +) + +// List of available errors +// AggregateError - aggregates and contains errors below: +// ParseError +// NotStructPtrErrorType +// NoParserError +// NoSupportedTagOptionError +// EnvVarIsNotSetError +// 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(), ";") +} + +type ParseError struct { + msg string +} + +func newParseError(sf reflect.StructField, err error) error { + return ParseError{fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, sf.Name, sf.Type, err)} +} + +func (e ParseError) Error() string { + return e.msg +} + +// ErrNotAStructPtr is returned if you pass something that is not a pointer to a Struct to Parse. +type NotStructPtrError struct { + msg string +} + +func newNotStructPtrError() error { + return NotStructPtrError{"expected a pointer to a Struct"} +} + +func (e NotStructPtrError) Error() string { + return e.msg +} + +type NoParserError struct { + msg string +} + +func newNoParserError(sf reflect.StructField) error { + return NoParserError{fmt.Sprintf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type)} +} + +func (e NoParserError) Error() string { + return e.msg +} + +type NoSupportedTagOptionError struct { + msg string +} + +func newNoSupportedTagOptionError(tag string) error { + return NoSupportedTagOptionError{fmt.Sprintf("tag option %q not supported", tag)} +} + +func (e NoSupportedTagOptionError) Error() string { + return e.msg +} + +type EnvVarIsNotSetError struct { + msg string +} + +func newEnvVarIsNotSet(key string) error { + return EnvVarIsNotSetError{fmt.Sprintf(`required environment variable %q is not set`, key)} +} + +func (e EnvVarIsNotSetError) Error() string { + return e.msg +} + +type EmptyEnvVarError struct { + msg string +} + +func newEmptyEnvVarError(key string) error { + return EmptyEnvVarError{fmt.Sprintf("environment variable %q should not be empty", key)} +} + +func (e EmptyEnvVarError) Error() string { + return e.msg +} + +type LoadFileContentError struct { + msg string +} + +func newLoadFileContentError(filename, key string, err error) error { + return LoadFileContentError{fmt.Sprintf(`could not load content of file "%s" from variable %s: %v`, filename, key, err)} +} + +func (e LoadFileContentError) Error() string { + return e.msg +} + +type ParseValueError struct { + msg string +} + +func newParseValueError(message string) error { + return ParseValueError{message} +} + +func (e ParseValueError) Error() string { + return e.msg +} From 41bc5b8ab05e0020ba881ef9c26dc96529e03106 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Wed, 7 Dec 2022 17:44:07 +0000 Subject: [PATCH 02/12] update erorr description --- error.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/error.go b/error.go index bb92ffd..ec8db00 100644 --- a/error.go +++ b/error.go @@ -7,14 +7,14 @@ import ( ) // List of available errors -// AggregateError - aggregates and contains errors below: -// ParseError -// NotStructPtrErrorType -// NoParserError -// NoSupportedTagOptionError -// EnvVarIsNotSetError -// LoadFileContentError -// ParseValueError +// AggregateError - aggregates and contains errors below: +// ParseError +// NotStructPtrErrorType +// NoParserError +// NoSupportedTagOptionError +// EnvVarIsNotSetError +// LoadFileContentError +// ParseValueError type AggregateError struct { Errors []error From e4239d516c396048e7ad1a3baa6d9a805c8f0ede Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Wed, 7 Dec 2022 18:51:07 +0000 Subject: [PATCH 03/12] add tb.Helper --- env_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/env_test.go b/env_test.go index a561a5e..c11e8e3 100644 --- a/env_test.go +++ b/env_test.go @@ -1574,6 +1574,8 @@ 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) { @@ -1594,6 +1596,23 @@ func isErrorWithMessage(tb testing.TB, err error, msg string) { tb.Fatalf("expected error, got nil") } + if e, ok := err.(*AggregateError); ok { + for _, er := range e.Errors { + switch v := er.(type) { + case ParseError: + // handle it + case NotStructPtrError: + // handle it + case NoParserError: + // handle it + case NoSupportedTagOptionError: + // handle it + default: + fmt.Printf("Unknown type %v", v) + } + } + } + if msg != err.Error() { tb.Fatalf("expected error message %q, got %q", msg, err.Error()) } From 60a0f3b12f4c34cf661c7aacf7673f3228fbcc64 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 18:42:49 +0000 Subject: [PATCH 04/12] add documentation --- env.go | 4 +-- env_test.go | 17 --------- error.go | 102 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/env.go b/env.go index cdce7c7..d6a9b02 100644 --- a/env.go +++ b/env.go @@ -178,11 +178,11 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ... ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { - return newAggregateError(newNotStructPtrError()) + return newAggregateError(NotStructPtrError{}) } ref := ptrRef.Elem() if ref.Kind() != reflect.Struct { - return newAggregateError(newNotStructPtrError()) + return newAggregateError(NotStructPtrError{}) } parsers := defaultTypeParsers() for k, v := range funcMap { diff --git a/env_test.go b/env_test.go index c11e8e3..5cb1ffc 100644 --- a/env_test.go +++ b/env_test.go @@ -1596,23 +1596,6 @@ func isErrorWithMessage(tb testing.TB, err error, msg string) { tb.Fatalf("expected error, got nil") } - if e, ok := err.(*AggregateError); ok { - for _, er := range e.Errors { - switch v := er.(type) { - case ParseError: - // handle it - case NotStructPtrError: - // handle it - case NoParserError: - // handle it - case NoSupportedTagOptionError: - // handle it - default: - fmt.Printf("Unknown type %v", v) - } - } - } - if msg != err.Error() { tb.Fatalf("expected error message %q, got %q", msg, err.Error()) } diff --git a/error.go b/error.go index ec8db00..ef4842a 100644 --- a/error.go +++ b/error.go @@ -6,16 +6,40 @@ import ( "strings" ) -// List of available errors -// AggregateError - aggregates and contains errors below: +// An aggregated error wrapper to combine gathered errors. This allows either to display all errors or convert them individually. +// +// +// An example of converting errors by type: +// +// if e, ok := err.(*AggregateError); ok { +// for _, er := range e.Errors { +// switch v := er.(type) { +// case ParseError: +// // handle it +// case NotStructPtrError: +// // handle it +// case NoParserError: +// // handle it +// case NoSupportedTagOptionError: +// // handle it +// default: +// fmt.Printf("Unknown type %v", v) +// } +// } +// } +// +// +// List of the available errors // ParseError -// NotStructPtrErrorType +// NotStructPtrError // NoParserError // NoSupportedTagOptionError // EnvVarIsNotSetError +// EmptyEnvVarError // LoadFileContentError // ParseValueError - +// +// type AggregateError struct { Errors []error } @@ -40,93 +64,109 @@ func (e AggregateError) Error() string { return strings.TrimRight(sb.String(), ";") } +// The error occurs when it's impossible to convert the value for given type type ParseError struct { - msg string + Name string + Type reflect.Type + Err error } func newParseError(sf reflect.StructField, err error) error { - return ParseError{fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, sf.Name, sf.Type, err)} + return ParseError{sf.Name, sf.Type, err} } func (e ParseError) Error() string { - return e.msg + return fmt.Sprintf(`parse error on field "%s" of type "%s": %v`, e.Name, e.Type, e.Err) } -// ErrNotAStructPtr is returned if you pass something that is not a pointer to a Struct to Parse. +// The error occurs when pass something that is not a pointer to a Struct to Parse type NotStructPtrError struct { - msg string -} - -func newNotStructPtrError() error { - return NotStructPtrError{"expected a pointer to a Struct"} } func (e NotStructPtrError) Error() string { - return e.msg + return fmt.Sprintf("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 { - msg string + Name string + Type reflect.Type } func newNoParserError(sf reflect.StructField) error { - return NoParserError{fmt.Sprintf(`no parser found for field "%s" of type "%s"`, sf.Name, sf.Type)} + return NoParserError{sf.Name, sf.Type} } func (e NoParserError) Error() string { - return e.msg + 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 { - msg string + Tag string } func newNoSupportedTagOptionError(tag string) error { - return NoSupportedTagOptionError{fmt.Sprintf("tag option %q not supported", tag)} + return NoSupportedTagOptionError{tag} } func (e NoSupportedTagOptionError) Error() string { - return e.msg + 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 { - msg string + Key string } func newEnvVarIsNotSet(key string) error { - return EnvVarIsNotSetError{fmt.Sprintf(`required environment variable %q is not set`, key)} + return EnvVarIsNotSetError{key} } func (e EnvVarIsNotSetError) Error() string { - return e.msg + 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 { - msg string + Key string } func newEmptyEnvVarError(key string) error { - return EmptyEnvVarError{fmt.Sprintf("environment variable %q should not be empty", key)} + return EmptyEnvVarError{key} } func (e EmptyEnvVarError) Error() string { - return e.msg + 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 { - msg string + Filename string + Key string + Err error } func newLoadFileContentError(filename, key string, err error) error { - return LoadFileContentError{fmt.Sprintf(`could not load content of file "%s" from variable %s: %v`, filename, key, err)} + return LoadFileContentError{filename, key, err} } func (e LoadFileContentError) Error() string { - return e.msg + 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 + Msg string } func newParseValueError(message string) error { @@ -134,5 +174,5 @@ func newParseValueError(message string) error { } func (e ParseValueError) Error() string { - return e.msg + return e.Msg } From c41979f91eda1e55335c2f639ab17db935a61e35 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 19:16:27 +0000 Subject: [PATCH 05/12] merge --- .github/workflows/build.yml | 40 +++++++++++++------------------------ .github/workflows/lint.yml | 13 ++++-------- README.md | 10 ++++++---- go.sum | 0 4 files changed, 24 insertions(+), 39 deletions(-) create mode 100644 go.sum diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 852c443..bfb0aad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,44 +9,32 @@ on: pull_request: jobs: + govulncheck: + uses: caarlos0/meta/.github/workflows/govulncheck.yml@main + semgrep: + uses: caarlos0/meta/.github/workflows/semgrep.yml@main + ruleguard: + uses: caarlos0/meta/.github/workflows/ruleguard.yml@main build: strategy: matrix: - go-version: [~1.17] os: [ ubuntu-latest, macos-latest, windows-latest ] runs-on: ${{ matrix.os }} steps: - - - name: Checkout - uses: actions/checkout@v3 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v3 + - uses: actions/setup-go@v3 with: - go-version: ${{ matrix.go-version }} - - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: CI - run: make setup ci - - - name: Upload coverage - uses: codecov/codecov-action@v3 + go-version: 1.19 + cache: true + - run: make setup ci + - uses: codecov/codecov-action@v3 if: matrix.os == 'ubuntu-latest' with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v3 + - uses: goreleaser/goreleaser-action@v4 if: success() && startsWith(github.ref, 'refs/tags/') && matrix.os == 'ubuntu-latest' with: version: latest @@ -58,4 +46,4 @@ jobs: TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} - TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 28e8802..1db9dcb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,21 +1,16 @@ name: golangci-lint on: push: - tags: - - v* - branches: - - main pull_request: jobs: golangci: name: lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: - go-version: ~1.16 - - uses: actions/checkout@v3 + go-version: ~1.19 + cache: true - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - skip-go-installation: true + uses: golangci/golangci-lint-action@v3 \ No newline at end of file diff --git a/README.md b/README.md index e56399b..89759ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # env -[![Build Status](https://img.shields.io/github/workflow/status/caarlos0/env/build?style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) +[![Build Status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?branch=main&style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build) [![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env) [![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v6) @@ -53,7 +53,9 @@ $ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go {Home:/your/home Port:3000 IsProduction:true Hosts:[host1 host2 host3] Duration:1s} ``` -⚠️⚠️⚠️ **Attention:** _unexported fields_ will be **ignored**. +> **Warning** +> +> _Unexported fields_ are **ignored** by `env`. ## Supported types and defaults @@ -148,7 +150,7 @@ type config struct { ## Not Empty fields -While `required` demands the environment variable to be check, it doesn't check its value. +While `required` demands the environment variable to be set, it doesn't check its value. If you want to make sure the environment is set and not empty, you need to use the `notEmpty` tag option instead (`env:"SOME_ENV,notEmpty"`). Example: @@ -449,4 +451,4 @@ func main() { ## Stargazers over time -[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) \ No newline at end of file diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 From e3738e3cf558113b7ae95e47ec79f25ca7dd8817 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 21:28:23 +0000 Subject: [PATCH 06/12] yaml new line --- .github/workflows/build.yml | 2 +- .github/workflows/lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfb0aad..c917beb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,4 +46,4 @@ jobs: TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} - TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} \ No newline at end of file + TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1db9dcb..9b89d57 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,4 +13,4 @@ jobs: go-version: ~1.19 cache: true - name: golangci-lint - uses: golangci/golangci-lint-action@v3 \ No newline at end of file + uses: golangci/golangci-lint-action@v3 From 493b2ef271530e534b07d9a6b422441a2e2673e2 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 21:35:51 +0000 Subject: [PATCH 07/12] readme new line --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 89759ff..84ecf7d 100644 --- a/README.md +++ b/README.md @@ -451,4 +451,4 @@ func main() { ## Stargazers over time -[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) \ No newline at end of file +[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env) From 4c034b3e4be20c45ca6e8915680c32aab1df6d89 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 21:44:55 +0000 Subject: [PATCH 08/12] update error --- env.go | 5 ++--- error.go | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/env.go b/env.go index d6a9b02..b20e51b 100644 --- a/env.go +++ b/env.go @@ -2,7 +2,6 @@ package env import ( "encoding" - "fmt" "net/url" "os" "reflect" @@ -74,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, newParseValueError(fmt.Sprintf("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, newParseValueError(fmt.Sprintf("unable to parse duration: %v", err)) + return nil, newParseValueError("unable to parse duration", err) } return s, err }, diff --git a/error.go b/error.go index ef4842a..d1a242c 100644 --- a/error.go +++ b/error.go @@ -84,7 +84,7 @@ type NotStructPtrError struct { } func (e NotStructPtrError) Error() string { - return fmt.Sprintf("expected a pointer to a Struct") + return "expected a pointer to a Struct" } // This error occurs when there is no parser provided for given type @@ -167,12 +167,13 @@ func (e LoadFileContentError) Error() string { // 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) error { - return ParseValueError{message} +func newParseValueError(message string, err error) error { + return ParseValueError{message, err} } func (e ParseValueError) Error() string { - return e.Msg + return fmt.Sprintf("%s: %v", e.Msg, e.Err) } From e9c1ee9095fc5a25a016c54ded243234319a4f00 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 22:13:34 +0000 Subject: [PATCH 09/12] linting --- error.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/error.go b/error.go index d1a242c..04cd6a8 100644 --- a/error.go +++ b/error.go @@ -80,8 +80,7 @@ func (e ParseError) Error() string { } // The error occurs when pass something that is not a pointer to a Struct to Parse -type NotStructPtrError struct { -} +type NotStructPtrError struct{} func (e NotStructPtrError) Error() string { return "expected a pointer to a Struct" From be3dd44ef2caa69faaa144a74af30d05b11b600a Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 22:19:55 +0000 Subject: [PATCH 10/12] goimport fix --- error.go | 1 - 1 file changed, 1 deletion(-) diff --git a/error.go b/error.go index 04cd6a8..1a01233 100644 --- a/error.go +++ b/error.go @@ -28,7 +28,6 @@ import ( // } // } // -// // List of the available errors // ParseError // NotStructPtrError From a8106f372f506dbc3c2e2245d6a073a743df4d85 Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Mon, 9 Jan 2023 22:35:15 +0000 Subject: [PATCH 11/12] linting fix --- error.go | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/error.go b/error.go index 1a01233..a187530 100644 --- a/error.go +++ b/error.go @@ -6,28 +6,7 @@ import ( "strings" ) -// An aggregated error wrapper to combine gathered errors. This allows either to display all errors or convert them individually. -// -// -// An example of converting errors by type: -// -// if e, ok := err.(*AggregateError); ok { -// for _, er := range e.Errors { -// switch v := er.(type) { -// case ParseError: -// // handle it -// case NotStructPtrError: -// // handle it -// case NoParserError: -// // handle it -// case NoSupportedTagOptionError: -// // handle it -// default: -// fmt.Printf("Unknown type %v", v) -// } -// } -// } -// +// 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 @@ -37,8 +16,6 @@ import ( // EmptyEnvVarError // LoadFileContentError // ParseValueError -// -// type AggregateError struct { Errors []error } From d9976c3d260dac24463cc9f372aeb4d3fbb8114f Mon Sep 17 00:00:00 2001 From: Alexander Kutuev Date: Tue, 10 Jan 2023 09:08:54 +0000 Subject: [PATCH 12/12] try to revert readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 84ecf7d..fd167e4 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ $ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go ``` > **Warning** -> +> > _Unexported fields_ are **ignored** by `env`. ## Supported types and defaults