Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@ func (s *settings) setFieldValue(field reflect.Value, e entry) error {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, err := strconv.ParseInt(e.value, 10, field.Type().Bits())
if err != nil {
return &FieldConversionError{FieldName: e.key, TargetType: field.Type().String(), Err: err}
return &FieldConversionError{FieldName: e.fieldName, TargetType: field.Type().String(), Err: err}
}
field.SetInt(n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n, err := strconv.ParseUint(e.value, 10, field.Type().Bits())
if err != nil {
return &FieldConversionError{FieldName: e.key, TargetType: field.Type().String(), Err: err}
return &FieldConversionError{FieldName: e.fieldName, TargetType: field.Type().String(), Err: err}
}
field.SetUint(n)
case reflect.Float32, reflect.Float64:
f, err := strconv.ParseFloat(e.value, field.Type().Bits())
if err != nil {
return &FieldConversionError{FieldName: e.key, TargetType: field.Type().String(), Err: err}
return &FieldConversionError{FieldName: e.fieldName, TargetType: field.Type().String(), Err: err}
}
field.SetFloat(f)
case reflect.Bool:
b, err := strconv.ParseBool(e.value)
if err != nil {
return &FieldConversionError{FieldName: e.key, TargetType: "bool", Err: err}
return &FieldConversionError{FieldName: e.fieldName, TargetType: "bool", Err: err}
}
field.SetBool(b)
case reflect.Slice:
return setSliceField(field, e)
default:
return &UnsupportedFieldTypeError{FieldType: field.Interface()}
return &UnsupportedFieldTypeError{FieldName: e.fieldName, FieldType: field.Type().String()}
}
return nil
}
Expand All @@ -50,19 +50,19 @@ func setSliceField(field reflect.Value, e entry) error {
case reflect.String:
field.Set(splitStringSlice(e.value, field.Type()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, err := splitIntSlice(e.key, e.value, field.Type())
v, err := splitIntSlice(e.fieldName, e.value, field.Type())
if err != nil {
return err
}
field.Set(v)
case reflect.Float32, reflect.Float64:
v, err := splitFloatSlice(e.key, e.value, field.Type())
v, err := splitFloatSlice(e.fieldName, e.value, field.Type())
if err != nil {
return err
}
field.Set(v)
default:
return &UnsupportedFieldTypeError{FieldType: field.Interface()}
return &UnsupportedFieldTypeError{FieldName: e.fieldName, FieldType: field.Type().String()}
}
return nil
}
Expand All @@ -76,28 +76,28 @@ func splitStringSlice(value string, sliceType reflect.Type) reflect.Value {
return result
}

func splitIntSlice(key, value string, sliceType reflect.Type) (reflect.Value, error) {
func splitIntSlice(fieldName, value string, sliceType reflect.Type) (reflect.Value, error) {
parts := strings.Split(value, ",")
result := reflect.MakeSlice(sliceType, len(parts), len(parts))
bits := sliceType.Elem().Bits()
for i, v := range parts {
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, bits)
if err != nil {
return reflect.Value{}, &FieldConversionError{FieldName: key, TargetType: sliceType.String(), Err: err}
return reflect.Value{}, &FieldConversionError{FieldName: fieldName, TargetType: sliceType.String(), Err: err}
}
result.Index(i).SetInt(n)
}
return result, nil
}

func splitFloatSlice(key, value string, sliceType reflect.Type) (reflect.Value, error) {
func splitFloatSlice(fieldName, value string, sliceType reflect.Type) (reflect.Value, error) {
parts := strings.Split(value, ",")
result := reflect.MakeSlice(sliceType, len(parts), len(parts))
bits := sliceType.Elem().Bits()
for i, v := range parts {
f, err := strconv.ParseFloat(strings.TrimSpace(v), bits)
if err != nil {
return reflect.Value{}, &FieldConversionError{FieldName: key, TargetType: sliceType.String(), Err: err}
return reflect.Value{}, &FieldConversionError{FieldName: fieldName, TargetType: sliceType.String(), Err: err}
}
result.Index(i).SetFloat(f)
}
Expand Down
61 changes: 20 additions & 41 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var (
ErrConversion = errors.New("conversion error")
ErrReplacement = errors.New("replacement error")
ErrTag = errors.New("tag error")
ErrSyntax = errors.New("invalid syntax")
)

// FileTypeValidationError occurs when the .env config file fails to open.
Expand All @@ -26,19 +27,19 @@ func (e *FileTypeValidationError) Error() string {
return fmt.Sprintf("file extension is not a valid environment file: %q", e.Filepath)
}

func (e *FileTypeValidationError) Is(target error) bool { return target == ErrFile }
func (e *FileTypeValidationError) Unwrap() error { return ErrFile }

// OpenFileError occurs when the .env config file fails to open.
type OpenFileError struct {
Err error
Filepath string
Err error
}

func (e *OpenFileError) Error() string {
return fmt.Sprintf("failed to open config file: %v", e.Err)
return fmt.Sprintf("opening config file %q: %v", e.Filepath, e.Err)
}

func (e *OpenFileError) Unwrap() error { return e.Err }
func (e *OpenFileError) Is(target error) bool { return target == ErrFile }
func (e *OpenFileError) Unwrap() []error { return []error{e.Err, ErrFile} }

// FieldConversionError occurs when a field on the config struct fails to be set.
type FieldConversionError struct {
Expand All @@ -48,22 +49,22 @@ type FieldConversionError struct {
}

func (e *FieldConversionError) Error() string {
return fmt.Sprintf("failed to convert field %v to %v: %v", e.FieldName, e.TargetType, e.Err)
return fmt.Sprintf("failed to convert field %q to %v: %v", e.FieldName, e.TargetType, e.Err)
}

func (e *FieldConversionError) Unwrap() error { return e.Err }
func (e *FieldConversionError) Is(target error) bool { return target == ErrConversion }
func (e *FieldConversionError) Unwrap() []error { return []error{e.Err, ErrConversion} }

// UnsupportedFieldTypeError occurs when the a field type on the config struct is not compatible.
type UnsupportedFieldTypeError struct {
FieldType any
FieldName string
FieldType string
}

func (e *UnsupportedFieldTypeError) Error() string {
return fmt.Sprintf("unsupported field type: %T", e.FieldType)
return fmt.Sprintf("unsupported field type %q: %s", e.FieldName, e.FieldType)
}

func (e *UnsupportedFieldTypeError) Is(target error) bool { return target == ErrUnsupported }
func (e *UnsupportedFieldTypeError) Unwrap() error { return ErrUnsupported }

// InvalidConfigTypeError occurs when config is not a pointer to a struct.
type InvalidConfigTypeError struct {
Expand All @@ -74,18 +75,18 @@ func (e *InvalidConfigTypeError) Error() string {
return fmt.Sprintf("output must be a pointer to a struct, got %T", e.ProvidedType)
}

func (e *InvalidConfigTypeError) Is(target error) bool { return target == ErrInvalidConfig }
func (e *InvalidConfigTypeError) Unwrap() error { return ErrInvalidConfig }

// RequiredFieldError occurs when a required field is not set in the configuration.
type RequiredFieldError struct {
FieldName string
}

func (e *RequiredFieldError) Error() string {
return fmt.Sprintf("required field is not set in configuration: %v", e.FieldName)
return fmt.Sprintf("required field is not set in configuration: %q", e.FieldName)
}

func (e *RequiredFieldError) Is(target error) bool { return target == ErrRequired }
func (e *RequiredFieldError) Unwrap() error { return ErrRequired }

// ReplacementError occurs when the configuration variable being used for replacement is not set.
type ReplacementError struct {
Expand All @@ -96,23 +97,19 @@ func (e *ReplacementError) Error() string {
return fmt.Sprintf("configuration variable for replacement is not set: %v", e.VariableName)
}

func (e *ReplacementError) Is(target error) bool { return target == ErrReplacement }
func (e *ReplacementError) Unwrap() error { return ErrReplacement }

// ParseError occurs when a line from the .env config file has been parsed incorrectly.
type ParseError struct {
Line string
Err error
}

// ErrSyntax indicates that a line is invalid syntax.
var ErrSyntax = errors.New("invalid syntax")

func (e *ParseError) Error() string {
return fmt.Sprintf("parse line: %v: %v", e.Line, e.Err)
}

func (e *ParseError) Unwrap() error { return e.Err }
func (e *ParseError) Is(target error) bool { return target == ErrParse }
func (e *ParseError) Unwrap() []error { return []error{e.Err, ErrParse} }

// FileReadError occurs when an error occurs when scanning the .env file.
type FileReadError struct {
Expand All @@ -124,8 +121,7 @@ func (e *FileReadError) Error() string {
return fmt.Sprintf("reading %v: %v", e.Filepath, e.Err)
}

func (e *FileReadError) Unwrap() error { return e.Err }
func (e *FileReadError) Is(target error) bool { return target == ErrFile }
func (e *FileReadError) Unwrap() []error { return []error{e.Err, ErrFile} }

// MalformedTagError occurs when a config struct tag is invalid.
type MalformedTagError struct {
Expand All @@ -140,23 +136,7 @@ func (e *MalformedTagError) Error() string {
return fmt.Sprintf("malformed tag %q", e.Tag)
}

func (e *MalformedTagError) Unwrap() error { return e.Err }
func (e *MalformedTagError) Is(target error) bool { return target == ErrTag }

// FieldError wraps an error with the name of the field that caused it.
type FieldError struct {
FieldName string
Err error
}

func (e *FieldError) Error() string {
if e.FieldName != "" {
return fmt.Sprintf("field %q: %v", e.FieldName, e.Err)
}
return e.Err.Error()
}

func (e *FieldError) Unwrap() error { return e.Err }
func (e *MalformedTagError) Unwrap() []error { return []error{e.Err, ErrTag} }

// MalformedDefaultError occurs when the default value in a struct tag cannot be
// parsed into the field's type. This is a developer error in the tag definition.
Expand All @@ -170,5 +150,4 @@ func (e *MalformedDefaultError) Error() string {
return fmt.Sprintf("default value %q is invalid for field %q: %v", e.Default, e.FieldName, e.Err)
}

func (e *MalformedDefaultError) Unwrap() error { return e.Err }
func (e *MalformedDefaultError) Is(target error) bool { return target == ErrTag }
func (e *MalformedDefaultError) Unwrap() []error { return []error{e.Err, ErrTag} }
92 changes: 61 additions & 31 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ func TestErrorsIs(t *testing.T) {
target error
}{
{"FileTypeValidationError is ErrFile", &configutil.FileTypeValidationError{Filepath: "bad.txt"}, configutil.ErrFile},
{"OpenFileError is ErrFile", &configutil.OpenFileError{Err: fmt.Errorf("open failed")}, configutil.ErrFile},
{"OpenFileError is ErrFile", &configutil.OpenFileError{Filepath: "test.env", Err: fmt.Errorf("open failed")}, configutil.ErrFile},
{"FieldConversionError is ErrConversion", &configutil.FieldConversionError{FieldName: "Port", TargetType: "int", Err: fmt.Errorf("bad")}, configutil.ErrConversion},
{"UnsupportedFieldTypeError is ErrUnsupported", &configutil.UnsupportedFieldTypeError{FieldType: "complex128"}, configutil.ErrUnsupported},
{"UnsupportedFieldTypeError is ErrUnsupported", &configutil.UnsupportedFieldTypeError{FieldName: "X", FieldType: "complex128"}, configutil.ErrUnsupported},
{"InvalidConfigTypeError is ErrInvalidConfig", &configutil.InvalidConfigTypeError{ProvidedType: "string"}, configutil.ErrInvalidConfig},
{"RequiredFieldError is ErrRequired", &configutil.RequiredFieldError{FieldName: "Name"}, configutil.ErrRequired},
{"ReplacementError is ErrReplacement", &configutil.ReplacementError{VariableName: "HOST"}, configutil.ErrReplacement},
Expand Down Expand Up @@ -60,43 +60,75 @@ func TestErrorsUnwrap(t *testing.T) {
inner := fmt.Errorf("inner error")

tests := []struct {
name string
err error
name string
err error
inner error
}{
{"OpenFileError", &configutil.OpenFileError{Err: inner}},
{"FieldConversionError", &configutil.FieldConversionError{FieldName: "X", TargetType: "int", Err: inner}},
{"ParseError", &configutil.ParseError{Line: "bad", Err: inner}},
{"FileReadError", &configutil.FileReadError{Filepath: "x.env", Err: inner}},
{"MalformedTagError", &configutil.MalformedTagError{Tag: "bad", Err: inner}},
{"FieldError", &configutil.FieldError{FieldName: "X", Err: inner}},
{"MalformedDefaultError", &configutil.MalformedDefaultError{FieldName: "X", Default: "bad", Err: inner}},
{"OpenFileError", &configutil.OpenFileError{Err: inner}, inner},
{"FieldConversionError", &configutil.FieldConversionError{FieldName: "X", TargetType: "int", Err: inner}, inner},
{"ParseError", &configutil.ParseError{Line: "bad", Err: inner}, inner},
{"FileReadError", &configutil.FileReadError{Filepath: "x.env", Err: inner}, inner},
{"MalformedTagError", &configutil.MalformedTagError{Tag: "bad", Err: inner}, inner},
{"MalformedDefaultError", &configutil.MalformedDefaultError{FieldName: "X", Default: "bad", Err: inner}, inner},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
unwrapped := errors.Unwrap(tc.err)
if unwrapped != inner {
t.Errorf("Unwrap() = %v, want %v", unwrapped, inner)
if !errors.Is(tc.err, tc.inner) {
t.Errorf("errors.Is(%T, inner) = false, want true", tc.err)
}
})
}
}

func TestErrorsAs_ThroughFieldError(t *testing.T) {
inner := &configutil.RequiredFieldError{FieldName: "Name"}
wrapped := &configutil.FieldError{FieldName: "Name", Err: inner}
func TestSentinelUnwrap(t *testing.T) {
tests := []struct {
name string
err error
sentinel error
}{
{"FileTypeValidationError", &configutil.FileTypeValidationError{Filepath: "bad.txt"}, configutil.ErrFile},
{"InvalidConfigTypeError", &configutil.InvalidConfigTypeError{ProvidedType: "string"}, configutil.ErrInvalidConfig},
{"RequiredFieldError", &configutil.RequiredFieldError{FieldName: "Name"}, configutil.ErrRequired},
{"ReplacementError", &configutil.ReplacementError{VariableName: "HOST"}, configutil.ErrReplacement},
{"UnsupportedFieldTypeError", &configutil.UnsupportedFieldTypeError{FieldName: "X", FieldType: "chan int"}, configutil.ErrUnsupported},
}

var target *configutil.RequiredFieldError
if !errors.As(wrapped, &target) {
t.Error("errors.As through FieldError failed to find RequiredFieldError")
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if !errors.Is(tc.err, tc.sentinel) {
t.Errorf("errors.Is(%T, sentinel) = false, want true", tc.err)
}
})
}
if target.FieldName != "Name" {
t.Errorf("FieldName = %q, want %q", target.FieldName, "Name")
}

func TestDualUnwrap(t *testing.T) {
inner := fmt.Errorf("inner error")

tests := []struct {
name string
err error
inner error
sentinel error
}{
{"OpenFileError", &configutil.OpenFileError{Filepath: "test.env", Err: inner}, inner, configutil.ErrFile},
{"FieldConversionError", &configutil.FieldConversionError{FieldName: "X", TargetType: "int", Err: inner}, inner, configutil.ErrConversion},
{"ParseError", &configutil.ParseError{Line: "bad", Err: inner}, inner, configutil.ErrParse},
{"FileReadError", &configutil.FileReadError{Filepath: "x.env", Err: inner}, inner, configutil.ErrFile},
{"MalformedTagError", &configutil.MalformedTagError{Tag: "bad", Err: inner}, inner, configutil.ErrTag},
{"MalformedDefaultError", &configutil.MalformedDefaultError{FieldName: "X", Default: "bad", Err: inner}, inner, configutil.ErrTag},
}

// FieldError wrapping RequiredFieldError should match ErrRequired via the chain.
if !errors.Is(wrapped, configutil.ErrRequired) {
t.Error("errors.Is(FieldError{RequiredFieldError}, ErrRequired) = false, want true")
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if !errors.Is(tc.err, tc.inner) {
t.Errorf("errors.Is(%T, inner) = false, want true", tc.err)
}
if !errors.Is(tc.err, tc.sentinel) {
t.Errorf("errors.Is(%T, sentinel) = false, want true", tc.err)
}
})
}
}

Expand All @@ -107,17 +139,15 @@ func TestErrorMessages(t *testing.T) {
want string
}{
{"FileTypeValidationError", &configutil.FileTypeValidationError{Filepath: "bad.txt"}, `file extension is not a valid environment file: "bad.txt"`},
{"OpenFileError", &configutil.OpenFileError{Err: fmt.Errorf("no such file")}, "failed to open config file: no such file"},
{"UnsupportedFieldTypeError", &configutil.UnsupportedFieldTypeError{FieldType: "test"}, "unsupported field type: string"},
{"OpenFileError", &configutil.OpenFileError{Filepath: "no such file", Err: fmt.Errorf("no such file")}, `opening config file "no such file": no such file`},
{"UnsupportedFieldTypeError", &configutil.UnsupportedFieldTypeError{FieldName: "X", FieldType: "chan int"}, `unsupported field type "X": chan int`},
{"InvalidConfigTypeError", &configutil.InvalidConfigTypeError{ProvidedType: "test"}, "output must be a pointer to a struct, got string"},
{"RequiredFieldError", &configutil.RequiredFieldError{FieldName: "Name"}, "required field is not set in configuration: Name"},
{"RequiredFieldError", &configutil.RequiredFieldError{FieldName: "Name"}, `required field is not set in configuration: "Name"`},
{"ReplacementError", &configutil.ReplacementError{VariableName: "HOST"}, "configuration variable for replacement is not set: HOST"},
{"MalformedTagError with err", &configutil.MalformedTagError{Tag: "bad", Err: fmt.Errorf("reason")}, `malformed tag "bad": reason`},
{"MalformedTagError without err", &configutil.MalformedTagError{Tag: "bad"}, `malformed tag "bad"`},
{"FieldError with name", &configutil.FieldError{FieldName: "X", Err: fmt.Errorf("fail")}, `field "X": fail`},
{"FieldError without name", &configutil.FieldError{Err: fmt.Errorf("fail")}, "fail"},
{"MalformedDefaultError", &configutil.MalformedDefaultError{FieldName: "Port", Default: "abc", Err: fmt.Errorf("bad")}, `default value "abc" is invalid for field "Port": bad`},
{"FieldConversionError", &configutil.FieldConversionError{FieldName: "Port", TargetType: "int", Err: fmt.Errorf("bad")}, "failed to convert field Port to int: bad"},
{"FieldConversionError", &configutil.FieldConversionError{FieldName: "Port", TargetType: "int", Err: fmt.Errorf("bad")}, `failed to convert field "Port" to int: bad`},
{"ParseError", &configutil.ParseError{Line: "BADLINE", Err: configutil.ErrSyntax}, "parse line: BADLINE: invalid syntax"},
{"FileReadError", &configutil.FileReadError{Filepath: "x.env", Err: fmt.Errorf("read err")}, "reading x.env: read err"},
}
Expand Down
Loading
Loading