Skip to content

Commit

Permalink
Merge pull request #1 from mailgun/thrawn/last
Browse files Browse the repository at this point in the history
Added errors.Last()
  • Loading branch information
thrawn01 authored Feb 2, 2023
2 parents 8088208 + ce4f2ed commit f75ac50
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 0 deletions.
52 changes: 52 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package errors

import (
"errors"
"reflect"
)

// Import all the standard errors functions as a convenience.
Expand Down Expand Up @@ -29,3 +30,54 @@ func New(text string) error {
func Unwrap(err error) error {
return errors.Unwrap(err)
}

// Last finds the last error in err's chain that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method `As(any) bool` such that
// As(target) returns true.
//
// An error type might provide an As() method so it can be treated as if it were a
// different error type.
//
// Last panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
//
// NOTE: Last() is much slower than As(). Therefore As() should always be used
// unless you absolutely need Last() to retrieve the last error in the error chain
// that matches the target.
func Last(err error, target any) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflect.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflect.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
var found error
for err != nil {
if reflect.TypeOf(err).AssignableTo(targetType) {
found = err
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
found = err
}
err = Unwrap(err)
}
if found != nil {
val.Elem().Set(reflect.ValueOf(found))
return true
}
return false
}

var errorType = reflect.TypeOf((*error)(nil)).Elem()
31 changes: 31 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package errors_test

import (
"fmt"
"testing"

"github.com/mailgun/errors"
"github.com/mailgun/errors/callstack"
"github.com/stretchr/testify/assert"
)

type ErrTest struct {
Msg string
}
Expand Down Expand Up @@ -30,3 +39,25 @@ func (e *ErrHasFields) Is(target error) bool {
func (e *ErrHasFields) Fields() map[string]interface{} {
return e.F
}

func TestLast(t *testing.T) {
err := errors.New("bottom")
err = errors.Wrap(err, "last")
err = errors.Wrap(err, "second")
err = errors.Wrap(err, "first")
err = fmt.Errorf("wrapped: %w", err)

// errors.As() returns the "first" error in the chain with a stack trace
var first callstack.HasStackTrace
assert.True(t, errors.As(err, &first))
assert.Equal(t, "first: second: last: bottom", first.(error).Error())

// errors.Last() returns the last error in the chain with a stack trace
var last callstack.HasStackTrace
assert.True(t, errors.Last(err, &last))
assert.Equal(t, "last: bottom", last.(error).Error())

// If no stack trace is found, then should not set target and should return false
assert.False(t, errors.Last(errors.New("no stack"), &last))
assert.Equal(t, "last: bottom", last.(error).Error())
}

0 comments on commit f75ac50

Please sign in to comment.