errors: add support for Is and As methods

An error can implement an Is method to
allow errors to define themselves as being
equal to other errors using the Is function.

An As method can be implemented by errors
for them to pose as another error type using
the As function.

Change-Id: Ib7fcf45acd18d1bca936dbb8c1d075e1a0b62510
Reviewed-on: https://go-review.googlesource.com/c/147877
Run-TryBot: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/errors/wrap.go b/errors/wrap.go
index bee0929..00d1cb2 100644
--- a/errors/wrap.go
+++ b/errors/wrap.go
@@ -43,7 +43,10 @@
 	return u.Unwrap()
 }
 
-// Is returns true if any error in err's chain is equal to target.
+// Is returns true if any error in err's chain matches target.
+//
+// An error is considered to match a target if it is equal to that target or if
+// it implements an Is method such that Is(target) returns true.
 func Is(err, target error) bool {
 	if target == nil {
 		return err == target
@@ -52,6 +55,12 @@
 		if err == target {
 			return true
 		}
+		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
+			return true
+		}
+		// TODO: consider supporing target.Is(err). This would allow
+		// user-definable predicates, but also may allow for coping with sloppy
+		// APIs, thereby making it easier to get away with them.
 		if err = Unwrap(err); err == nil {
 			return false
 		}
@@ -59,9 +68,12 @@
 }
 
 // As finds the first error in err's chain that matches a type to which target
-// points, and if so, sets the target to its value and reports success.
+// points, and if so, sets the target to its value and reports success. An error
+// matches a type if it is of the same type, or if it has an As method such that
+// As(target) returns true. As will panic if target is nil or not a pointer.
 //
-// As will panic if target is nil.
+// The As method should set the target to its value and report success if err
+// matches the type to which target points and report success.
 func As(err error, target interface{}) bool {
 	if target == nil {
 		panic("errors: target cannot be nil")
@@ -76,6 +88,9 @@
 			reflect.ValueOf(target).Elem().Set(reflect.ValueOf(err))
 			return true
 		}
+		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
+			return true
+		}
 		if err = Unwrap(err); err == nil {
 			return false
 		}
diff --git a/errors/wrap_test.go b/errors/wrap_test.go
index 9481401..4a5c663 100644
--- a/errors/wrap_test.go
+++ b/errors/wrap_test.go
@@ -21,6 +21,10 @@
 
 	err3 := errors.New("3")
 
+	poser := &poser{"either 1 or 3", func(err error) bool {
+		return err == err1 || err == err3
+	}}
+
 	testCases := []struct {
 		err    error
 		target error
@@ -37,6 +41,12 @@
 		{err1, err3, false},
 		{erra, err3, false},
 		{errb, err3, false},
+		{poser, err1, true},
+		{poser, err3, true},
+		{poser, erra, false},
+		{poser, errb, false},
+		{poser, erro, false},
+		{poser, errco, false},
 	}
 	for _, tc := range testCases {
 		t.Run("", func(t *testing.T) {
@@ -47,11 +57,32 @@
 	}
 }
 
+type poser struct {
+	msg string
+	f   func(error) bool
+}
+
+func (p *poser) Error() string     { return p.msg }
+func (p *poser) Is(err error) bool { return p.f(err) }
+func (p *poser) As(err interface{}) bool {
+	switch x := err.(type) {
+	case **poser:
+		*x = p
+	case *errorT:
+		*x = errorT{}
+	case **os.PathError:
+		*x = &os.PathError{}
+	default:
+		return false
+	}
+	return true
+}
+
 func TestAs(t *testing.T) {
 	var errT errorT
 	var errP *os.PathError
+	var p *poser
 	_, errF := os.Open("non-existing")
-	erro := errors.Opaque(errT)
 
 	testCases := []struct {
 		err    error
@@ -66,17 +97,33 @@
 		&errP,
 		true,
 	}, {
-		erro,
+		errors.Opaque(errT),
 		&errT,
 		false,
 	}, {
-		errT,
+		errorT{},
 		&errP,
 		false,
 	}, {
 		wrapped{nil},
 		&errT,
 		false,
+	}, {
+		&poser{"error", nil},
+		&errT,
+		true,
+	}, {
+		&poser{"path", nil},
+		&errP,
+		true,
+	}, {
+		&poser{"oh no", nil},
+		&p,
+		true,
+	}, {
+		&poser{"oo", nil},
+		&errF,
+		false,
 	}}
 	for _, tc := range testCases {
 		name := fmt.Sprintf("As(Errorf(..., %v), %v)", tc.err, tc.target)