diff --git a/pkg/oops/functions.go b/pkg/oops/functions.go index 452605d..ef412a4 100644 --- a/pkg/oops/functions.go +++ b/pkg/oops/functions.go @@ -1,5 +1,12 @@ package oops +import ( + "errors" +) + +// Explainf is a helper function to check the given error if it's an Error and then call Error.Explainf with the given +// format and arguments, if and only if it's also not nil. If the given error is not an Error, it will be wrapped with +// ErrUncaught and the format and arguments will be passed to it. func Explainf(err error, format string, args ...any) Error { if err == nil { return nil @@ -19,25 +26,41 @@ func Explainf(err error, format string, args ...any) Error { return v } +// Nest is a shortcut to ErrorDefined.Yeet followed by a call to Error.Append, if and only if the source is not nil and +// the given errors are not empty. func Nest(source ErrorDefined, nested ...Error) Error { if source == nil || len(nested) == 0 { return nil } - finish, addf := source.Collect() - for _, err := range nested { - addf(err, "") + return source.Yeet().Append(nested...) +} + +// DeepIs will check if the given err is an Error and if the Error.Source matches the target ErrorDefined. If the given +// error is not an Error, it will attempt to traverse the unwrap chain until an Error is found or nil is reached. Once +// an Error is found, the check is repeated strictly on Error.Nested errors and never up to the parent of any errors. +// If any of the nested errors' source matches the target, true is returned. The check is repeated recursively until +// either the check is successful, or the nested errors exhaust. +// This function respects nil as valid targets (compared to DeepAs which does not). +func DeepIs(err error, target ErrorDefined) bool { + if err == nil { + return target == nil } - return finish() -} + v, ok := err.(Error) + if !ok { + return DeepIs(errors.Unwrap(err), target) + } + + if v == nil { + return target == nil + } -func DeepIs(err Error, target ErrorDefined) bool { - if err.Is(target) { + if v.Source() == target { return true } - for _, nested := range err.Nested() { + for _, nested := range v.Nested() { if DeepIs(nested, target) { return true } @@ -46,11 +69,59 @@ func DeepIs(err Error, target ErrorDefined) bool { return false } -func As(err error) (Error, bool) { +// As will check if the given err is an Error and if the Error.Source matches the target ErrorDefined, at which point +// err gets returned as an Error. If the given err is not an Error, or if the Error.Source does not match, the check +// is repeated with the parent of err (if any) until either the check is successful, or the parent is nil. +func As(err error, target ErrorDefined) (Error, bool) { if err == nil { return nil, false } v, ok := err.(Error) - return v, ok + if !ok { + return As(errors.Unwrap(err), target) + } + + if v == nil { + return nil, false + } + + if v.Source() == target { + return v, true + } + + return As(v.Unwrap(), target) +} + +// DeepAs will check if the given err is an Error and if the Error.Source matches the target ErrorDefined, at which +// point err gets returned as an Error. If the given err is not an Error, it will attempt to traverse the unwrap chain +// until an Error is found or nil is reached. Once an Error is found, the check is repeated strictly on Error.Nested +// errors and never up to the parent of any errors. If any of the nested errors' source matches the target, the +// nested error is returned. The check is repeated recursively until either the check is successful, or the nested +// errors exhaust. +func DeepAs(err error, target ErrorDefined) (Error, bool) { + if err == nil { + return nil, false + } + + v, ok := err.(Error) + if !ok { + return DeepAs(errors.Unwrap(err), target) + } + + if v == nil { + return nil, false + } + + if v.Source() == target { + return v, true + } + + for _, nested := range v.Nested() { + if result, ok := DeepAs(nested, target); ok { + return result, true + } + } + + return nil, false } diff --git a/pkg/oops/types.go b/pkg/oops/types.go index db7a599..1df2a5e 100644 --- a/pkg/oops/types.go +++ b/pkg/oops/types.go @@ -1,30 +1,67 @@ package oops type Error interface { + // Error returns the string representation of the error. The format of the string is implementation-specific. Error() string + + // Unwrap should return the parent error, if any. This method may have implementation-specific behaviour. Unwrap() error + // Append should add the given errors as "nested" errors returnable by calling Error.Nested. Append(errs ...Error) Error + + // Nested should return the list of errors that were added using Error.Append or by other means. Nested() []Error + // Is returns true if both errors are nil, or if the other error is a ErrorDefined and the Error.Source matches it, + // or if the other error is an Error, then both their Error.Source must match. Otherwise, the behaviour can be + // implementation-specific, but it's recommended to at least check any parent error if any. Is(other error) bool + + // As should check the target type to determine if it's a *Error, and if so, set the target to itself. This method + // must be implemented such that errors.As can be used to "cast" any error to an Error. As(any) bool + // Explainf should update the error's explanation with the given format and arguments. The format and arguments + // should be used to create a human-readable explanation of the error. This method may have implementation-specific + // behaviour. Explainf(format string, args ...any) + + // Set should add or update the value for the given key in the context of the Error. If set, values should be + // retrievable using Error.Get or Error.GetAll. This method may have implementation-specific behaviour. Set(key string, value any) Error + + // Get should return the value for the given key in the context of the Error. If the key is not found, the second + // return value should be false. This method may have implementation-specific behaviour. Get(key string) (value any, ok bool) + + // GetAll should return all the values in the context of the Error. This method may have implementation-specific + // behaviour. GetAll() map[string]any + // PathSetf should set the path and args for the error. The path meaning is implementation-specific. + PathSetf(path string, args ...any) Error + + // Path should return the formatted path of the error. The path meaning is implementation-specific. Path() string + + // PathArgs should return the arguments set by Error.PathSetf. The path meaning is implementation-specific. PathArgs() []any - PathSetf(path string, args ...any) Error + // Explanation should return the complete current explanation of the error. Explanation() string + + // Trace optionally returns a list of strings, where each string represents a step in the error's trace. The trace + // format is implementation-specific. Trace() []string + + // Source must return the ErrorDefined that created/spawned/returned this error. Source() ErrorDefined } type ErrorDefined interface { + // Error will panic, as ErrorDefined is not intended to be used as a replacement for `error`, but it can + // be passed as an `error` to specific functions. Error() string Yeet() Error