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} }