Skip to content

Commit ec6a9e7

Browse files
authored
❇️ [commonerrors] Error marshalling (#318)
<!-- Copyright (C) 2020-2022 Arm Limited or its affiliates and Contributors. All rights reserved. SPDX-License-Identifier: Apache-2.0 --> ### Description Add ways to marshal errors and not lose information about the "type". It is based on error definition general convention `error type: reason` ### Test Coverage <!-- Please put an `x` in the correct box e.g. `[x]` to indicate the testing coverage of this change. --> - [x] This change is covered by existing or additional automated tests. - [ ] Manual testing has been performed (and evidence provided) as automated testing was not feasible. - [ ] Additional tests are not required for this change (e.g. documentation update).
1 parent af5e5e1 commit ec6a9e7

File tree

4 files changed

+569
-0
lines changed

4 files changed

+569
-0
lines changed

changes/20230926155050.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[commonerrors]` Add a way to serialise and deserialise errors

utils/commonerrors/errors.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"strings"
1313
)
1414

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

90+
// deserialiseCommonError returns the common error corresponding to its string value
91+
func deserialiseCommonError(errStr string) (bool, error) {
92+
errStr = strings.TrimSpace(errStr)
93+
switch {
94+
case errStr == "":
95+
return true, nil
96+
case errStr == ErrInvalid.Error():
97+
return true, ErrInvalid
98+
case errStr == ErrNotFound.Error():
99+
return true, ErrNotFound
100+
case CorrespondTo(ErrNotImplemented, errStr):
101+
return true, ErrNotImplemented
102+
case CorrespondTo(ErrNoExtension, errStr):
103+
return true, ErrNoExtension
104+
case CorrespondTo(ErrNoLogger, errStr):
105+
return true, ErrNoLogger
106+
case CorrespondTo(ErrNoLoggerSource, errStr):
107+
return true, ErrNoLoggerSource
108+
case CorrespondTo(ErrNoLogSource, errStr):
109+
return true, ErrNoLogSource
110+
case CorrespondTo(ErrUndefined, errStr):
111+
return true, ErrUndefined
112+
case CorrespondTo(ErrInvalidDestination, errStr):
113+
return true, ErrInvalidDestination
114+
case CorrespondTo(ErrTimeout, errStr):
115+
return true, ErrTimeout
116+
case CorrespondTo(ErrLocked, errStr):
117+
return true, ErrLocked
118+
case CorrespondTo(ErrStaleLock, errStr):
119+
return true, ErrStaleLock
120+
case CorrespondTo(ErrExists, errStr):
121+
return true, ErrExists
122+
case CorrespondTo(ErrNotFound, errStr):
123+
return true, ErrExists
124+
case CorrespondTo(ErrUnsupported, errStr):
125+
return true, ErrUnsupported
126+
case CorrespondTo(ErrUnavailable, errStr):
127+
return true, ErrUnavailable
128+
case CorrespondTo(ErrWrongUser, errStr):
129+
return true, ErrWrongUser
130+
case CorrespondTo(ErrUnauthorised, errStr):
131+
return true, ErrUnauthorised
132+
case CorrespondTo(ErrUnknown, errStr):
133+
return true, ErrUnknown
134+
case CorrespondTo(ErrInvalid, errStr):
135+
return true, ErrInvalid
136+
case CorrespondTo(ErrConflict, errStr):
137+
return true, ErrConflict
138+
case CorrespondTo(ErrMarshalling, errStr):
139+
return true, ErrMarshalling
140+
case CorrespondTo(ErrCancelled, errStr):
141+
return true, ErrCancelled
142+
case CorrespondTo(ErrEmpty, errStr):
143+
return true, ErrEmpty
144+
case CorrespondTo(ErrUnexpected, errStr):
145+
return true, ErrUnexpected
146+
case CorrespondTo(ErrTooLarge, errStr):
147+
return true, ErrTooLarge
148+
case CorrespondTo(ErrForbidden, errStr):
149+
return true, ErrForbidden
150+
case CorrespondTo(ErrCondition, errStr):
151+
return true, ErrCondition
152+
case CorrespondTo(ErrEOF, errStr):
153+
return true, ErrEOF
154+
case CorrespondTo(ErrMalicious, errStr):
155+
return true, ErrMalicious
156+
}
157+
return false, ErrUnknown
158+
}
159+
88160
// ConvertContextError converts a context error into common errors.
89161
func ConvertContextError(err error) error {
90162
if err == nil {
@@ -106,3 +178,15 @@ func Ignore(target error, ignore ...error) error {
106178
}
107179
return target
108180
}
181+
182+
// IsEmpty states whether an error is empty or not.
183+
// An error is considered empty if it is `nil` or has no description.
184+
func IsEmpty(err error) bool {
185+
if err == nil {
186+
return true
187+
}
188+
if strings.TrimSpace(err.Error()) == "" {
189+
return true
190+
}
191+
return false
192+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package commonerrors
2+
3+
import (
4+
"encoding"
5+
"errors"
6+
"fmt"
7+
"strings"
8+
)
9+
10+
const (
11+
TypeReasonErrorSeparator = ':'
12+
MultipleErrorSeparator = '\n'
13+
)
14+
15+
type iMarshallingError interface {
16+
encoding.TextMarshaler
17+
encoding.TextUnmarshaler
18+
fmt.Stringer
19+
error
20+
ConvertToError() error
21+
SetWrappedError(err error)
22+
}
23+
24+
type marshallingError struct {
25+
Reason string
26+
ErrorType error
27+
}
28+
29+
func (e *marshallingError) MarshalText() (text []byte, err error) {
30+
str := e.String()
31+
return []byte(str), nil
32+
}
33+
34+
func (e *marshallingError) String() string {
35+
return serialiseMarshallingError(e)
36+
}
37+
38+
func (e *marshallingError) Error() string {
39+
return e.String()
40+
}
41+
42+
func (e *marshallingError) UnmarshalText(text []byte) error {
43+
er := processErrorStrLine(string(text))
44+
if er == nil {
45+
return ErrMarshalling
46+
}
47+
e.ErrorType = er.ErrorType
48+
e.Reason = er.Reason
49+
return nil
50+
}
51+
52+
func (e *marshallingError) ConvertToError() error {
53+
if e == nil {
54+
return nil
55+
}
56+
if e.ErrorType == nil {
57+
if e.Reason == "" {
58+
return nil
59+
}
60+
return errors.New(e.Reason)
61+
}
62+
if e.Reason == "" {
63+
return e.ErrorType
64+
}
65+
return fmt.Errorf("%w%v %v", e.ErrorType, string(TypeReasonErrorSeparator), e.Reason)
66+
}
67+
68+
func (e *marshallingError) SetWrappedError(err error) {
69+
e.ErrorType = err
70+
}
71+
72+
type multiplemarshallingError struct {
73+
subErrs []iMarshallingError
74+
}
75+
76+
func (m *multiplemarshallingError) MarshalText() (text []byte, err error) {
77+
for i := range m.subErrs {
78+
subtext, suberr := m.subErrs[i].MarshalText()
79+
if suberr != nil {
80+
err = fmt.Errorf("%w%v an error item could not be marshalled%v %v", ErrMarshalling, string(TypeReasonErrorSeparator), string(TypeReasonErrorSeparator), suberr.Error())
81+
return
82+
}
83+
text = append(text, subtext...)
84+
text = append(text, MultipleErrorSeparator)
85+
}
86+
return
87+
}
88+
89+
func (m *multiplemarshallingError) String() string {
90+
text, err := m.MarshalText()
91+
if err == nil {
92+
return string(text)
93+
}
94+
return ""
95+
}
96+
97+
func (m *multiplemarshallingError) Error() string {
98+
return m.String()
99+
}
100+
101+
func (m *multiplemarshallingError) UnmarshalText(text []byte) error {
102+
sub := processErrorStr(string(text))
103+
if IsEmpty(sub) {
104+
return ErrMarshalling
105+
}
106+
if mul, ok := sub.(*multiplemarshallingError); ok {
107+
m.subErrs = mul.subErrs
108+
} else {
109+
m.subErrs = append(m.subErrs, sub)
110+
}
111+
return nil
112+
}
113+
114+
func (m *multiplemarshallingError) ConvertToError() error {
115+
var errs []error
116+
for i := range m.subErrs {
117+
errs = append(errs, m.subErrs[i].ConvertToError())
118+
}
119+
return errors.Join(errs...)
120+
}
121+
122+
func (m *multiplemarshallingError) SetWrappedError(err error) {
123+
if err == nil {
124+
return
125+
}
126+
if x, ok := err.(interface{ Unwrap() []error }); ok {
127+
unwrapped := x.Unwrap()
128+
if len(unwrapped) > len(m.subErrs) {
129+
for i := 0; i < len(unwrapped)-len(m.subErrs); i++ {
130+
m.subErrs = append(m.subErrs, &marshallingError{})
131+
}
132+
}
133+
for i := range unwrapped {
134+
subErr := m.subErrs[i]
135+
if subErr != nil {
136+
subErr.SetWrappedError(unwrapped[i])
137+
}
138+
}
139+
}
140+
}
141+
func processErrorStr(s string) iMarshallingError {
142+
if strings.Contains(s, string(MultipleErrorSeparator)) {
143+
elems := strings.Split(s, string(MultipleErrorSeparator))
144+
m := &multiplemarshallingError{}
145+
for i := range elems {
146+
mErr := processErrorStrLine(elems[i])
147+
148+
if mErr != nil {
149+
m.subErrs = append(m.subErrs, mErr)
150+
}
151+
}
152+
return m
153+
} else {
154+
return processErrorStrLine(s)
155+
}
156+
}
157+
158+
func processError(err error) (mErr iMarshallingError) {
159+
if err == nil {
160+
return
161+
}
162+
mErr = processErrorStr(err.Error())
163+
if IsEmpty(mErr) {
164+
mErr = &marshallingError{
165+
ErrorType: fmt.Errorf("%w%v error `%T` with no description returned", ErrUnknown, string(TypeReasonErrorSeparator), err),
166+
}
167+
return
168+
}
169+
switch x := err.(type) {
170+
case interface{ Unwrap() error }:
171+
mErr.SetWrappedError(x.Unwrap())
172+
case interface{ Unwrap() []error }:
173+
unwrap := x.Unwrap()
174+
var nonNilUnwrappedErrors []error
175+
for i := range unwrap {
176+
if !IsEmpty(unwrap[i]) {
177+
nonNilUnwrappedErrors = append(nonNilUnwrappedErrors, unwrap[i])
178+
}
179+
}
180+
mErr.SetWrappedError(errors.Join(nonNilUnwrappedErrors...))
181+
}
182+
return
183+
}
184+
185+
func processErrorStrLine(err string) (mErr *marshallingError) {
186+
err = strings.TrimSpace(err)
187+
if err == "" {
188+
return nil
189+
}
190+
mErr = &marshallingError{}
191+
elems := strings.Split(err, string(TypeReasonErrorSeparator))
192+
found, commonErr := deserialiseCommonError(elems[0])
193+
if !found || commonErr == nil {
194+
mErr.SetWrappedError(errors.New(strings.TrimSpace(elems[0])))
195+
} else {
196+
mErr.SetWrappedError(commonErr)
197+
}
198+
if len(elems) > 0 {
199+
var reasonElems []string
200+
for i := 1; i < len(elems); i++ {
201+
reasonElems = append(reasonElems, strings.TrimSpace(elems[i]))
202+
}
203+
mErr.Reason = strings.Join(reasonElems, fmt.Sprintf("%v ", string(TypeReasonErrorSeparator)))
204+
}
205+
return
206+
}
207+
208+
func serialiseMarshallingError(err *marshallingError) string {
209+
if err == nil {
210+
return ""
211+
}
212+
mErr := err.ConvertToError()
213+
if mErr == nil {
214+
return ""
215+
}
216+
return mErr.Error()
217+
}
218+
219+
// SerialiseError marshals an error following a certain convention: `error type: reason`.
220+
func SerialiseError(err error) ([]byte, error) {
221+
mErr := processError(err)
222+
if mErr == nil {
223+
return nil, nil
224+
}
225+
return mErr.MarshalText()
226+
}
227+
228+
// DeserialiseError unmarshals text into an error. It tries to determine the error type.
229+
func DeserialiseError(text []byte) (deserialisedError, err error) {
230+
if len(text) == 0 {
231+
return
232+
}
233+
var mErr iMarshallingError
234+
235+
if strings.Contains(string(text), string(MultipleErrorSeparator)) {
236+
mErr = &multiplemarshallingError{}
237+
} else {
238+
mErr = &marshallingError{}
239+
}
240+
err = mErr.UnmarshalText(text)
241+
if err != nil {
242+
return
243+
}
244+
deserialisedError = mErr.ConvertToError()
245+
return
246+
}

0 commit comments

Comments
 (0)