CardSimEngine/cardsim/errors.go

240 lines
6.7 KiB
Go

package cardsim
import (
"errors"
"fmt"
"reflect"
"strings"
)
// Warning is an error that does not crash the game, but is displayed immediately.
type Warning struct {
E error
}
// AnyWarning is a special Warning, not intended to be returned from anything,
// which every Warning recognizes that they "are" -- errors.Is(w, AnyWarning)
// returns true when w is a Warning.
var AnyWarning = &Warning{
E: errors.New("unspecified warning"),
}
// Generic warnings that may be used in multiple spots in the library without
// any particular comment that the function may return these.
var (
WarningRecoverableBug = Warningf("cardsim library has a bug, but can keep going")
)
// Error implements the error interface. A warning's error message is the
// message of its underlying error, unmodified.
func (w *Warning) Error() string {
return w.E.Error()
}
// Unwrap implements the Go 1.13 error handling system by allow error identity
// searches to continue to the underlying error.
func (w *Warning) Unwrap() error {
return w.E
}
// Is recognizes every Warning as equivalent to AnyWarning.
func (*Warning) Is(target error) bool {
w, ok := target.(*Warning)
if !ok {
return false
}
return w == AnyWarning
}
// Warningf calls fmt.Errorf with the provided arguments, wraps the error
// created as a Warning, and returns it.
func Warningf(f string, args ...any) *Warning {
return &Warning{fmt.Errorf(f, args...)}
}
// IsSeriousError returns whether e is a non-warning error. If e is nil, this
// returns false.
func IsSeriousError(e error) bool {
if e == nil {
return false
}
return !errors.Is(e, AnyWarning)
}
// A Failure is an error that is definitely not a Warning. If a Warning is
// wrapped in a Failure, it stops being a Warning.
type Failure struct {
e error
}
// Error implements error. It returns the message of the underlying error,
// unchanged.
func (f Failure) Error() string {
return f.e.Error()
}
type singleUnwrappable interface {
Unwrap() error
}
type multiUnwrappable interface {
Unwrap() []error
}
// Unwrap returns a new Failure wrapping the error wrapped by the underlying
// error, if any. If the underlying error wraps nothing, it returns nil.
func (f Failure) Unwrap() error {
if su, ok := f.e.(singleUnwrappable); ok {
w := su.Unwrap()
if w != nil {
// Do not wrap Failure in Failure, to avoid an infinite loop.
// If Failure already wraps Failure, this will "collapse" the
// chain down to a single Failure.
if f, ok := w.(Failure); ok {
return f
}
return Failure{w}
}
return nil
}
if mu, ok := f.e.(multiUnwrappable); ok {
w := mu.Unwrap()
if len(w) == 0 {
return nil
}
return aggregateFailure(w)
}
// Underlying error cannot be unwrapped.
return nil
}
// Is implements Go 1.13 error handling for a Failure. It specifically rejects
// the notion that it could be AnyWarning, but otherwise forwards the check
// to its underlying
// However, it allows comparisons to other warnings to proceed as normal.
func (f Failure) Is(target error) bool {
if w, ok := target.(*Warning); ok {
if w == AnyWarning {
return false
}
// Strip the warning of its warning-ness and recurse.
return errors.Is(f, w.E)
}
return false // let Unwrap handle the rest
}
// As implements Go 1.13 error handling for a Failure. It directly unpacks its
// contained error if possible, bypassing the "re-wrap in failure" behavior that
// Unwrap otherwise uses.
func (f Failure) As(target any) bool {
// Most of this algorithm is copied from the Go standard library.
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Pointer || val.IsNil() {
return false
}
if reflect.TypeOf(f.e).AssignableTo(typ.Elem()) {
val.Elem().Set(reflect.ValueOf(f.e))
return true
}
if asable, ok := f.e.(interface{ As(any) bool }); ok {
return asable.As(target)
}
return false
}
// ErrorCollector accumulates errors as an operation progresses. The zero
// ErrorCollector is its correct starting value. When it emits a final error:
//
// - If it contains exactly zero errors, it returns nil.
// - If it contains exactly one error, it returns it.
// - Otherwise, it returns an error that combines all the errors it collected.
// The aggregated error is a Warning if and only if all collected errors
// were also warnings.
//
// An ErrorCollector's Errs should not be written by anything other than Add,
// or HasFailure may get out of sync.
type ErrorCollector struct {
Errs []error
hasFailure bool
}
// Add inserts e into the error collection. If e is nil, this does nothing.
func (ec *ErrorCollector) Add(e error) {
if e == nil {
return
}
if !errors.Is(e, AnyWarning) {
ec.hasFailure = true
}
ec.Errs = append(ec.Errs, e)
}
// IsEmpty reports whether ec has zero accumulated errors/warnings.
func (ec *ErrorCollector) IsEmpty() bool {
return len(ec.Errs) == 0
}
// HasFailure reports whether ec has at least one non-warning error.
func (ec *ErrorCollector) HasFailure() bool {
return ec.hasFailure
}
// Emit returns the final error from this ErrorCollector. Do not Add
// to this ErrorCollector after calling Emit, or the error it emitted might
// be modified.
//
// If the error collector has collected no errors, this returns nil. If it has
// collected exactly one error, it returns that error. Otherwise, it returns an
// aggregate error, which is itself a warning if and only if all aggregated
// errors are warnings. This may involve wrapping contained warnings, so
// errors.Is does not erroneously represent a failure as a warning because it
// contains a warning as a subcomponent.
func (ec *ErrorCollector) Emit() error {
if len(ec.Errs) == 0 {
return nil
}
if len(ec.Errs) == 1 {
return ec.Errs[0]
}
if ec.HasFailure() {
return aggregateFailure(ec.Errs)
}
return &aggregateError{ec.Errs} // all these are recognizable warnings
}
// An aggregateError is a collection of errors that is itself an error.
type aggregateError struct {
errs []error
}
// Error returns a multi-line indented error message.
func (a *aggregateError) Error() string {
var messages []string
for i, e := range a.errs {
m := fmt.Sprintf("\t[%d]:\t%s", i, strings.ReplaceAll(e.Error(), "\n", "\n\t\t"))
messages = append(messages, m)
}
return fmt.Sprintf("%d errors: <\n%s\n>", len(a.errs), strings.Join(messages, "\n"))
}
// Unwrap implements Go 1.13 error handling. It returns its contained errors.
func (a *aggregateError) Unwrap() []error {
return a.errs
}
// aggregateFailure wraps every error in a slice in a Failure, cancelling any warnings.
func aggregateFailure(errs []error) *aggregateError {
ret := make([]error, len(errs))
for i, e := range errs {
if f, ok := e.(Failure); ok {
ret[i] = f
} else {
ret[i] = Failure{e}
}
}
return &aggregateError{ret}
}