|  | 
|  | 1 | +package errors | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"errors" | 
|  | 5 | +	"fmt" | 
|  | 6 | +	"strings" | 
|  | 7 | +) | 
|  | 8 | + | 
|  | 9 | +var _ ReasonableError = &ErrorWithReason{} | 
|  | 10 | + | 
|  | 11 | +// ReasonableError enhances an error with a reason. | 
|  | 12 | +// The reason is meant to be a CamelCased, machine-readable, enum-like string. | 
|  | 13 | +// Use WithReason(err, reason) to wrap a normal error into an ReasonableError. | 
|  | 14 | +type ReasonableError interface { | 
|  | 15 | +	error | 
|  | 16 | +	Reason() string | 
|  | 17 | +} | 
|  | 18 | + | 
|  | 19 | +// ErrorWithReason wraps an error and adds a reason to it. | 
|  | 20 | +// The reason is meant to be a CamelCased, machine-readable, enum-like string. | 
|  | 21 | +// Use WithReason(err, reason) to wrap a normal error into an *ErrorWithReason. | 
|  | 22 | +type ErrorWithReason struct { | 
|  | 23 | +	error | 
|  | 24 | +	reason string | 
|  | 25 | +} | 
|  | 26 | + | 
|  | 27 | +// Reason returns the reason for this error. | 
|  | 28 | +func (e *ErrorWithReason) Reason() string { | 
|  | 29 | +	return e.reason | 
|  | 30 | +} | 
|  | 31 | + | 
|  | 32 | +// WithReason wraps an error together with a reason into ErrorWithReason. | 
|  | 33 | +// The reason is meant to be a CamelCased, machine-readable, enum-like string. | 
|  | 34 | +// If the given error is nil, nil is returned. | 
|  | 35 | +func WithReason(err error, reason string) ReasonableError { | 
|  | 36 | +	if err == nil { | 
|  | 37 | +		return nil | 
|  | 38 | +	} | 
|  | 39 | +	return &ErrorWithReason{ | 
|  | 40 | +		error:  err, | 
|  | 41 | +		reason: reason, | 
|  | 42 | +	} | 
|  | 43 | +} | 
|  | 44 | + | 
|  | 45 | +// Errorf works similarly to fmt.Errorf, with the exception that it requires an ErrorWithReason as second argument and returns nil if that one is nil. | 
|  | 46 | +// Otherwise, it calls fmt.Errorf to construct an error and wraps it in an ErrorWithReason, using the reason from the given error. | 
|  | 47 | +// This is useful for expanding the error message without losing the reason. | 
|  | 48 | +func Errorf(format string, err ReasonableError, a ...any) ReasonableError { | 
|  | 49 | +	if err == nil { | 
|  | 50 | +		return nil | 
|  | 51 | +	} | 
|  | 52 | +	return WithReason(fmt.Errorf(format, a...), err.Reason()) | 
|  | 53 | +} | 
|  | 54 | + | 
|  | 55 | +// Join joins multiple errors into a single one. | 
|  | 56 | +// Returns nil if all given errors are nil. | 
|  | 57 | +// This is equivalent to NewErrorList(errs...).Aggregate(). | 
|  | 58 | +func Join(errs ...error) ReasonableError { | 
|  | 59 | +	return NewReasonableErrorList(errs...).Aggregate() | 
|  | 60 | +} | 
|  | 61 | + | 
|  | 62 | +// ReasonableErrorList is a helper struct for situations in which multiple errors (with or without reasons) should be returned as a single one. | 
|  | 63 | +type ReasonableErrorList struct { | 
|  | 64 | +	Errs    []error | 
|  | 65 | +	Reasons []string | 
|  | 66 | +} | 
|  | 67 | + | 
|  | 68 | +// NewReasonableErrorList creates a new *ErrorListWithReasons containing the provided errors. | 
|  | 69 | +func NewReasonableErrorList(errs ...error) *ReasonableErrorList { | 
|  | 70 | +	res := &ReasonableErrorList{ | 
|  | 71 | +		Errs:    []error{}, | 
|  | 72 | +		Reasons: []string{}, | 
|  | 73 | +	} | 
|  | 74 | +	return res.Append(errs...) | 
|  | 75 | +} | 
|  | 76 | + | 
|  | 77 | +// Aggregate aggregates all errors in the list into a single ErrorWithReason. | 
|  | 78 | +// Returns nil if the list is either nil or empty. | 
|  | 79 | +// If the list contains a single error, that error is returned. | 
|  | 80 | +// Otherwise, a new error is constructed by appending all contained errors' messages. | 
|  | 81 | +// The reason in the returned error is the first reason that was added to the list, | 
|  | 82 | +// or the empty string if none of the contained errors was an ErrorWithReason. | 
|  | 83 | +func (el *ReasonableErrorList) Aggregate() ReasonableError { | 
|  | 84 | +	if el == nil || len(el.Errs) == 0 { | 
|  | 85 | +		return nil | 
|  | 86 | +	} | 
|  | 87 | +	reason := "" | 
|  | 88 | +	if len(el.Reasons) > 0 { | 
|  | 89 | +		reason = el.Reasons[0] | 
|  | 90 | +	} | 
|  | 91 | +	if len(el.Errs) == 1 { | 
|  | 92 | +		if ewr, ok := el.Errs[0].(ReasonableError); ok { | 
|  | 93 | +			return ewr | 
|  | 94 | +		} | 
|  | 95 | +		return WithReason(el.Errs[0], reason) | 
|  | 96 | +	} | 
|  | 97 | +	sb := strings.Builder{} | 
|  | 98 | +	sb.WriteString("multiple errors occurred:") | 
|  | 99 | +	for _, e := range el.Errs { | 
|  | 100 | +		sb.WriteString("\n") | 
|  | 101 | +		sb.WriteString(e.Error()) | 
|  | 102 | +	} | 
|  | 103 | +	return WithReason(errors.New(sb.String()), reason) | 
|  | 104 | +} | 
|  | 105 | + | 
|  | 106 | +// Append appends all given errors to the ErrorListWithReasons. | 
|  | 107 | +// This modifies the receiver object. | 
|  | 108 | +// If a given error is of type ErrorWithReason, its reason is added to the list of reasons. | 
|  | 109 | +// nil pointers in the arguments are ignored. | 
|  | 110 | +// Returns the receiver for chaining. | 
|  | 111 | +func (el *ReasonableErrorList) Append(errs ...error) *ReasonableErrorList { | 
|  | 112 | +	for _, e := range errs { | 
|  | 113 | +		if e != nil { | 
|  | 114 | +			el.Errs = append(el.Errs, e) | 
|  | 115 | +			if ewr, ok := e.(ReasonableError); ok { | 
|  | 116 | +				el.Reasons = append(el.Reasons, ewr.Reason()) | 
|  | 117 | +			} | 
|  | 118 | +		} | 
|  | 119 | +	} | 
|  | 120 | +	return el | 
|  | 121 | +} | 
|  | 122 | + | 
|  | 123 | +// Reason returns the first reason from the list of reasons contained in this error list. | 
|  | 124 | +// If the list is nil or no reasons are contained, the empty string is returned. | 
|  | 125 | +// This is equivalent to el.Aggregate().Reason(), except that it also works for an empty error list. | 
|  | 126 | +func (el *ReasonableErrorList) Reason() string { | 
|  | 127 | +	if el == nil || len(el.Reasons) == 0 { | 
|  | 128 | +		return "" | 
|  | 129 | +	} | 
|  | 130 | +	return el.Reasons[0] | 
|  | 131 | +} | 
0 commit comments