Skip to content

Commit

Permalink
Document interfaces and add oops.As, oops.DeepAs and make oops.DeepIs…
Browse files Browse the repository at this point in the history
… accept error instead of Error
  • Loading branch information
cpl committed Mar 6, 2024
1 parent ff7eed5 commit 3095e6a
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 11 deletions.
91 changes: 81 additions & 10 deletions pkg/oops/functions.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Expand All @@ -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
}
39 changes: 38 additions & 1 deletion pkg/oops/types.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 3095e6a

Please sign in to comment.