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
1 change: 1 addition & 0 deletions changes/20230926155050.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[commonerrors]` Add a way to serialise and deserialise errors
84 changes: 84 additions & 0 deletions utils/commonerrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"strings"
)

// List of common errors used to qualify and categorise go errors
// Note: if adding error types to this list, ensure mapping functions (below) are also updated.
var (
ErrNotImplemented = errors.New("not implemented")
ErrNoExtension = errors.New("missing extension")
Expand Down Expand Up @@ -85,6 +87,76 @@ func CorrespondTo(target error, description ...string) bool {
return false
}

// deserialiseCommonError returns the common error corresponding to its string value
func deserialiseCommonError(errStr string) (bool, error) {
errStr = strings.TrimSpace(errStr)
switch {
case errStr == "":
return true, nil
case errStr == ErrInvalid.Error():
return true, ErrInvalid
case errStr == ErrNotFound.Error():
return true, ErrNotFound
case CorrespondTo(ErrNotImplemented, errStr):
return true, ErrNotImplemented
case CorrespondTo(ErrNoExtension, errStr):
return true, ErrNoExtension
case CorrespondTo(ErrNoLogger, errStr):
return true, ErrNoLogger
case CorrespondTo(ErrNoLoggerSource, errStr):
return true, ErrNoLoggerSource
case CorrespondTo(ErrNoLogSource, errStr):
return true, ErrNoLogSource
case CorrespondTo(ErrUndefined, errStr):
return true, ErrUndefined
case CorrespondTo(ErrInvalidDestination, errStr):
return true, ErrInvalidDestination
case CorrespondTo(ErrTimeout, errStr):
return true, ErrTimeout
case CorrespondTo(ErrLocked, errStr):
return true, ErrLocked
case CorrespondTo(ErrStaleLock, errStr):
return true, ErrStaleLock
case CorrespondTo(ErrExists, errStr):
return true, ErrExists
case CorrespondTo(ErrNotFound, errStr):
return true, ErrExists
case CorrespondTo(ErrUnsupported, errStr):
return true, ErrUnsupported
case CorrespondTo(ErrUnavailable, errStr):
return true, ErrUnavailable
case CorrespondTo(ErrWrongUser, errStr):
return true, ErrWrongUser
case CorrespondTo(ErrUnauthorised, errStr):
return true, ErrUnauthorised
case CorrespondTo(ErrUnknown, errStr):
return true, ErrUnknown
case CorrespondTo(ErrInvalid, errStr):
return true, ErrInvalid
case CorrespondTo(ErrConflict, errStr):
return true, ErrConflict
case CorrespondTo(ErrMarshalling, errStr):
return true, ErrMarshalling
case CorrespondTo(ErrCancelled, errStr):
return true, ErrCancelled
case CorrespondTo(ErrEmpty, errStr):
return true, ErrEmpty
case CorrespondTo(ErrUnexpected, errStr):
return true, ErrUnexpected
case CorrespondTo(ErrTooLarge, errStr):
return true, ErrTooLarge
case CorrespondTo(ErrForbidden, errStr):
return true, ErrForbidden
case CorrespondTo(ErrCondition, errStr):
return true, ErrCondition
case CorrespondTo(ErrEOF, errStr):
return true, ErrEOF
case CorrespondTo(ErrMalicious, errStr):
return true, ErrMalicious
}
return false, ErrUnknown
}

// ConvertContextError converts a context error into common errors.
func ConvertContextError(err error) error {
if err == nil {
Expand All @@ -106,3 +178,15 @@ func Ignore(target error, ignore ...error) error {
}
return target
}

// IsEmpty states whether an error is empty or not.
// An error is considered empty if it is `nil` or has no description.
func IsEmpty(err error) bool {
if err == nil {
return true
}
if strings.TrimSpace(err.Error()) == "" {
return true
}
return false
}
246 changes: 246 additions & 0 deletions utils/commonerrors/serialisation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package commonerrors

import (
"encoding"
"errors"
"fmt"
"strings"
)

const (
TypeReasonErrorSeparator = ':'
MultipleErrorSeparator = '\n'
)

type iMarshallingError interface {
encoding.TextMarshaler
encoding.TextUnmarshaler
fmt.Stringer
error
ConvertToError() error
SetWrappedError(err error)
}

type marshallingError struct {
Reason string
ErrorType error
}

func (e *marshallingError) MarshalText() (text []byte, err error) {
str := e.String()
return []byte(str), nil
}

func (e *marshallingError) String() string {
return serialiseMarshallingError(e)
}

func (e *marshallingError) Error() string {
return e.String()
}

func (e *marshallingError) UnmarshalText(text []byte) error {
er := processErrorStrLine(string(text))
if er == nil {
return ErrMarshalling
}
e.ErrorType = er.ErrorType
e.Reason = er.Reason
return nil
}

func (e *marshallingError) ConvertToError() error {
if e == nil {
return nil
}
if e.ErrorType == nil {
if e.Reason == "" {
return nil
}
return errors.New(e.Reason)
}
if e.Reason == "" {
return e.ErrorType
}
return fmt.Errorf("%w%v %v", e.ErrorType, string(TypeReasonErrorSeparator), e.Reason)
}

func (e *marshallingError) SetWrappedError(err error) {
e.ErrorType = err
}

type multiplemarshallingError struct {
subErrs []iMarshallingError
}

func (m *multiplemarshallingError) MarshalText() (text []byte, err error) {
for i := range m.subErrs {
subtext, suberr := m.subErrs[i].MarshalText()
if suberr != nil {
err = fmt.Errorf("%w%v an error item could not be marshalled%v %v", ErrMarshalling, string(TypeReasonErrorSeparator), string(TypeReasonErrorSeparator), suberr.Error())
return
}
text = append(text, subtext...)
text = append(text, MultipleErrorSeparator)
}
return
}

func (m *multiplemarshallingError) String() string {
text, err := m.MarshalText()
if err == nil {
return string(text)
}
return ""
}

func (m *multiplemarshallingError) Error() string {
return m.String()
}

func (m *multiplemarshallingError) UnmarshalText(text []byte) error {
sub := processErrorStr(string(text))
if IsEmpty(sub) {
return ErrMarshalling
}
if mul, ok := sub.(*multiplemarshallingError); ok {
m.subErrs = mul.subErrs
} else {
m.subErrs = append(m.subErrs, sub)
}
return nil
}

func (m *multiplemarshallingError) ConvertToError() error {
var errs []error
for i := range m.subErrs {
errs = append(errs, m.subErrs[i].ConvertToError())
}
return errors.Join(errs...)
}

func (m *multiplemarshallingError) SetWrappedError(err error) {
if err == nil {
return
}
if x, ok := err.(interface{ Unwrap() []error }); ok {
unwrapped := x.Unwrap()
if len(unwrapped) > len(m.subErrs) {
for i := 0; i < len(unwrapped)-len(m.subErrs); i++ {
m.subErrs = append(m.subErrs, &marshallingError{})
}
}
for i := range unwrapped {
subErr := m.subErrs[i]
if subErr != nil {
subErr.SetWrappedError(unwrapped[i])
}
}
}
}
func processErrorStr(s string) iMarshallingError {
if strings.Contains(s, string(MultipleErrorSeparator)) {
elems := strings.Split(s, string(MultipleErrorSeparator))
m := &multiplemarshallingError{}
for i := range elems {
mErr := processErrorStrLine(elems[i])

if mErr != nil {
m.subErrs = append(m.subErrs, mErr)
}
}
return m
} else {
return processErrorStrLine(s)
}
}

func processError(err error) (mErr iMarshallingError) {
if err == nil {
return
}
mErr = processErrorStr(err.Error())
if IsEmpty(mErr) {
mErr = &marshallingError{
ErrorType: fmt.Errorf("%w%v error `%T` with no description returned", ErrUnknown, string(TypeReasonErrorSeparator), err),
}
return
}
switch x := err.(type) {
case interface{ Unwrap() error }:
mErr.SetWrappedError(x.Unwrap())
case interface{ Unwrap() []error }:
unwrap := x.Unwrap()
var nonNilUnwrappedErrors []error
for i := range unwrap {
if !IsEmpty(unwrap[i]) {
nonNilUnwrappedErrors = append(nonNilUnwrappedErrors, unwrap[i])
}
}
mErr.SetWrappedError(errors.Join(nonNilUnwrappedErrors...))
}
return
}

func processErrorStrLine(err string) (mErr *marshallingError) {
err = strings.TrimSpace(err)
if err == "" {
return nil
}
mErr = &marshallingError{}
elems := strings.Split(err, string(TypeReasonErrorSeparator))
found, commonErr := deserialiseCommonError(elems[0])
if !found || commonErr == nil {
mErr.SetWrappedError(errors.New(strings.TrimSpace(elems[0])))
} else {
mErr.SetWrappedError(commonErr)
}
if len(elems) > 0 {
var reasonElems []string
for i := 1; i < len(elems); i++ {
reasonElems = append(reasonElems, strings.TrimSpace(elems[i]))
}
mErr.Reason = strings.Join(reasonElems, fmt.Sprintf("%v ", string(TypeReasonErrorSeparator)))
}
return
}

func serialiseMarshallingError(err *marshallingError) string {
if err == nil {
return ""
}
mErr := err.ConvertToError()
if mErr == nil {
return ""
}
return mErr.Error()
}

// SerialiseError marshals an error following a certain convention: `error type: reason`.
func SerialiseError(err error) ([]byte, error) {
mErr := processError(err)
if mErr == nil {
return nil, nil
}
return mErr.MarshalText()
}

// DeserialiseError unmarshals text into an error. It tries to determine the error type.
func DeserialiseError(text []byte) (deserialisedError, err error) {
if len(text) == 0 {
return
}
var mErr iMarshallingError

if strings.Contains(string(text), string(MultipleErrorSeparator)) {
mErr = &multiplemarshallingError{}
} else {
mErr = &marshallingError{}
}
err = mErr.UnmarshalText(text)
if err != nil {
return
}
deserialisedError = mErr.ConvertToError()
return
}
Loading