240 lines
6.7 KiB
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}
|
|
}
|