CardSimEngine/cardsim/errors.go
Kistaro Windrider f8b6b6b376
Start implementing Deck as a distinct type.
Deck does enough stuff it should be its own thing; its internal representation might change to make multiple insertions not quadratic, among other reasons. Currently it just does the obvious stuff, though. Allows inserting cards in random or specific slots and drawing cards. Shuffling a range of the deck comes later.
2023-04-01 11:50:39 -07:00

228 lines
6.4 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...)}
}
// 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.
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}
}