Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions changes/20251013123506.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[errors]` Added a way to provide context (`DescribeCircumstance`) to an error but without changing is type if it is a common error
117 changes: 115 additions & 2 deletions utils/commonerrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,89 @@ func IsCommonError(target error) bool {
return Any(target, ErrNotImplemented, ErrNoExtension, ErrNoLogger, ErrNoLoggerSource, ErrNoLogSource, ErrUndefined, ErrInvalidDestination, ErrTimeout, ErrLocked, ErrStaleLock, ErrExists, ErrNotFound, ErrUnsupported, ErrUnavailable, ErrWrongUser, ErrUnauthorised, ErrUnknown, ErrInvalid, ErrConflict, ErrMarshalling, ErrCancelled, ErrEmpty, ErrUnexpected, ErrTooLarge, ErrForbidden, ErrCondition, ErrEOF, ErrMalicious, ErrWarning, ErrOutOfRange, ErrFailed)
}

// RetrieveCommonError tries to return the common error of an error.
func RetrieveCommonError(target error) (isCommonError bool, commonError error) {
switch {
case IsEmpty(target):
return true, nil
case Any(target, ErrNotImplemented):
return true, ErrNotImplemented
case Any(target, ErrNoExtension):
return true, ErrNoExtension
case Any(target, ErrNoLogger):
return true, ErrNoLogger
case Any(target, ErrNoLoggerSource):
return true, ErrNoLoggerSource
case Any(target, ErrNoLogSource):
return true, ErrNoLogSource
case Any(target, ErrUndefined):
return true, ErrUndefined
case Any(target, ErrInvalidDestination):
return true, ErrInvalidDestination
case Any(target, ErrTimeout):
return true, ErrTimeout
case Any(target, ErrLocked):
return true, ErrLocked
case Any(target, ErrStaleLock):
return true, ErrStaleLock
case Any(target, ErrExists):
return true, ErrExists
case Any(target, ErrNotFound):
return true, ErrNotFound
case Any(target, ErrUnsupported):
return true, ErrUnsupported
case Any(target, ErrUnavailable):
return true, ErrUnavailable
case Any(target, ErrWrongUser):
return true, ErrWrongUser
case Any(target, ErrUnauthorised):
return true, ErrUnauthorised
case Any(target, ErrUnknown):
return true, ErrUnknown
case Any(target, ErrInvalid):
return true, ErrInvalid
case Any(target, ErrConflict):
return true, ErrConflict
case Any(target, ErrMarshalling):
return true, ErrMarshalling
case Any(target, ErrCancelled):
return true, ErrCancelled
case Any(target, ErrEmpty):
return true, ErrEmpty
case Any(target, ErrUnexpected):
return true, ErrUnexpected
case Any(target, ErrTooLarge):
return true, ErrTooLarge
case Any(target, ErrForbidden):
return true, ErrForbidden
case Any(target, ErrCondition):
return true, ErrCondition
case Any(target, ErrEOF):
return true, ErrEOF
case Any(target, ErrMalicious):
return true, ErrMalicious
case Any(target, ErrOutOfRange):
return true, ErrOutOfRange
case Any(target, ErrFailed):
return true, ErrFailed
case Any(target, ErrWarning):
return true, ErrWarning
}

underlyingErr, parseError := GetUnderlyingErrorType(target)
if parseError != nil {
commonError = ErrUnknown
return
}
if IsCommonError(underlyingErr) {
commonError = underlyingErr
isCommonError = true
} else {
commonError = ErrUnknown
}
return
}

// Any determines whether the target error is of the same type as any of the errors `err`
func Any(target error, err ...error) bool {
for i := range err {
Expand Down Expand Up @@ -371,6 +454,36 @@ func Errorf(targetErr error, format string, args ...any) error {
}
}

// DescribeCircumstance adds some context to a particular error. If the error is of a known type (as in, a common error), it will be kept. Otherwise, it will be set as Unexpected.
func DescribeCircumstance(originalError error, circumstance string) error {
origErr := ConvertContextError(originalError)
isCommonError, commonError := RetrieveCommonError(origErr)
if isCommonError {
return WrapError(commonError, origErr, circumstance)
}
return WrapError(ErrUnexpected, origErr, circumstance)
}

// DescribeCircumstanceAndKeepType does almost the same as DescribeCircumstance but if the err is not a common error, it won't wrap it but just add the context string to it.
func DescribeCircumstanceAndKeepType(err error, circumstance string) error {
origErr := ConvertContextError(err)
isCommonError, commonError := RetrieveCommonError(origErr)
if isCommonError {
return WrapError(commonError, origErr, circumstance)
}
return New(origErr, circumstance)
}

// DescribeCircumstanceAndKeepTypef is like DescribeCircumstanceAndKeepType but uses a message format to describe the error context.
func DescribeCircumstanceAndKeepTypef(err error, circumstanceFormat string, args ...any) error {
return DescribeCircumstanceAndKeepType(err, fmt.Sprintf(circumstanceFormat, args...))
}

// DescribeCircumstancef is the same as DescribeCircumstance but uses a format for capturing the error context.
func DescribeCircumstancef(err error, circumstanceFormat string, args ...any) error {
return DescribeCircumstance(err, fmt.Sprintf(circumstanceFormat, args...))
}

// WrapError wraps an error into a particular targetError. However, if the original error has to do with a contextual error (i.e. ErrCancelled or ErrTimeout) or should be considered as a failure rather than an error, it will be passed through without having its type changed.
// Same is true with warnings.
// This method should be used to safely wrap errors without losing information about context control information.
Expand Down Expand Up @@ -407,7 +520,7 @@ func WrapIfNotCommonError(targetError, originalError error, msg string) error {
return WrapError(targetError, originalError, msg)
}
if IsCommonError(originalError) {
return New(originalError, msg)
return DescribeCircumstance(originalError, msg)
}
return WrapError(targetError, originalError, msg)
}
Expand All @@ -426,7 +539,7 @@ func WrapIfNotCommonErrorf(targetError, originalError error, msgFormat string, a
return WrapErrorf(targetError, originalError, msgFormat, args...)
}
if IsCommonError(originalError) {
return Newf(originalError, msgFormat, args...)
return DescribeCircumstancef(originalError, msgFormat, args...)
}
return WrapErrorf(targetError, originalError, msgFormat, args...)
}
Expand Down
62 changes: 62 additions & 0 deletions utils/commonerrors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,44 @@ func TestIsCommonError(t *testing.T) {
}
for i := range commonErrors {
assert.True(t, IsCommonError(commonErrors[i]))
is, rawError := RetrieveCommonError(commonErrors[i])
assert.True(t, is)
assert.ErrorIs(t, rawError, commonErrors[i])
assert.True(t, Any(rawError, commonErrors[i]))
}

assert.False(t, IsCommonError(errors.New(faker.Sentence())))
}

func TestRetrieveCommonError(t *testing.T) {
is, rawError := RetrieveCommonError(nil)
assert.True(t, is)
assert.True(t, Any(rawError, nil))

is, rawError = RetrieveCommonError(errors.New(" "))
assert.True(t, is)
assert.True(t, Any(rawError, nil))

is, rawError = RetrieveCommonError(ErrUndefined)
assert.True(t, is)
assert.ErrorIs(t, rawError, ErrUndefined)
assert.True(t, Any(rawError, ErrUndefined))

is, rawError = RetrieveCommonError(fmt.Errorf("%w: %v", ErrInvalid, faker.Sentence()))
assert.True(t, is)
assert.ErrorIs(t, rawError, ErrInvalid)
assert.True(t, Any(rawError, ErrInvalid))

is, rawError = RetrieveCommonError(fmt.Errorf("%v: %v", ErrCondition.Error(), faker.Sentence()))
assert.True(t, is)
assert.ErrorIs(t, rawError, ErrCondition)
assert.True(t, Any(rawError, ErrCondition))

is, rawError = RetrieveCommonError(errors.New(faker.Sentence()))
assert.False(t, is)
assert.ErrorIs(t, rawError, ErrUnknown)
}

func TestIsWarning(t *testing.T) {
assert.True(t, IsWarning(ErrWarning))
assert.True(t, IsWarning(NewWarningMessage(faker.Sentence())))
Expand Down Expand Up @@ -256,6 +289,35 @@ func TestWrapError(t *testing.T) {
assert.True(t, Any(WrapIfNotCommonErrorf(ErrUndefined, errors.New(faker.Sentence()), faker.Sentence()), ErrUndefined))
}

func TestDescribeCircumstance(t *testing.T) {
tmpErr := errors.New(faker.Name())
assert.False(t, IsCommonError(tmpErr))
assert.True(t, Any(DescribeCircumstance(context.Canceled, faker.Sentence()), ErrCancelled))
assert.True(t, Any(DescribeCircumstance(ErrConflict, faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstance(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstance(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstance(tmpErr, faker.Sentence()), ErrUnexpected))
assert.Equal(t, "conflict: some context: conflict: initial error", DescribeCircumstance(New(ErrConflict, "initial error"), "some context").Error())
assert.True(t, Any(DescribeCircumstancef(context.Canceled, "%v", faker.Sentence()), ErrCancelled))
assert.True(t, Any(DescribeCircumstancef(ErrConflict, "%v", faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstancef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstancef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstancef(tmpErr, "%v", faker.Sentence()), ErrUnexpected))
assert.True(t, Any(DescribeCircumstanceAndKeepType(context.Canceled, faker.Sentence()), ErrCancelled))
assert.True(t, Any(DescribeCircumstanceAndKeepType(ErrConflict, faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstanceAndKeepType(New(ErrConflict, faker.Sentence()), faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstanceAndKeepType(errors.New(ErrConflict.Error()), faker.Sentence()), ErrConflict))
assert.False(t, Any(DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), ErrUnexpected))
assert.ErrorIs(t, DescribeCircumstanceAndKeepType(tmpErr, faker.Sentence()), tmpErr)
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(context.Canceled, "%v", faker.Sentence()), ErrCancelled))
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(ErrConflict, "%v", faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(New(ErrConflict, faker.Sentence()), "%v", faker.Sentence()), ErrConflict))
assert.True(t, Any(DescribeCircumstanceAndKeepTypef(errors.New(ErrConflict.Error()), "%v", faker.Sentence()), ErrConflict))
assert.False(t, Any(DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), ErrUnexpected))
assert.ErrorIs(t, DescribeCircumstanceAndKeepTypef(tmpErr, "%v", faker.Sentence()), tmpErr)

}

func TestString(t *testing.T) {
assert.Equal(t, "unknown", New(nil, "").Error())
assert.Equal(t, "unknown", Newf(nil, "").Error())
Expand Down
Loading