diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 000000000..8f5a7dc1c --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,120 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package errors is a github.com/pkg/errors compatible API for native errors. +// It includes only the subset of the github.com/pkg/errors API that is used by +// the Crossplane project. +package errors + +import ( + "errors" + "fmt" +) + +// New returns an error that formats as the given text. Each call to New returns +// a distinct error value even if the text is identical. +func New(text string) error { return errors.New(text) } + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained +// by repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// +// An error type might provide an Is method so it can be treated as equivalent +// to an existing error. For example, if MyError defines +// +// func (m MyError) Is(target error) bool { return target == fs.ErrExist } +// +// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for +// an example in the standard library. +func Is(err, target error) bool { return errors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, 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(interface{}) bool +// such that As(target) returns true. In the latter case, the As method is +// responsible for setting target. +// +// An error type might provide an As method so it can be treated as if it were a +// different error type. +// +// As panics if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. +func As(err error, target interface{}) bool { return errors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's type +// contains an Unwrap method returning error. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { return errors.Unwrap(err) } + +// Errorf formats according to a format specifier and returns the string as a +// value that satisfies error. +// +// If the format specifier includes a %w verb with an error operand, the +// returned error will implement an Unwrap method returning the operand. It is +// invalid to include more than one %w verb or to supply it with an operand that +// does not implement the error interface. The %w verb is otherwise a synonym +// for %v. +func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } + +// Wrap the supplied error with the supplied context message. Returns nil if the +// supplied error is nil. If the supplied error is not nil the returned error +// will have an Unwrap method. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", message, err) +} + +// Wrapf wraps the supplied error with the supplied context message. Returns nil +// if the supplied error is nil. If the supplied error is not nil the returned +// error will have an Unwrap method. +func Wrapf(err error, message string, args ...interface{}) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", fmt.Sprintf(message, args...), err) +} + +// Cause calls Unwrap on each error it finds. It returns the first error it +// finds that does not have an Unwrap method - i.e. the first error that was not +// the result of a Wrap call, a Wrapf call, or an Errorf call with %w wrapping. +func Cause(err error) error { + type wrapped interface { + Unwrap() error + } + + for err != nil { + // We're ignoring errorlint telling us to use errors.As because + // we actually do want to check the outermost error. + //nolint:errorlint + w, ok := err.(wrapped) + if !ok { + return err + } + err = w.Unwrap() + } + + return err +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 000000000..9058f2b12 --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,125 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package errors + +import ( + "testing" + + "github.com/crossplane/crossplane-runtime/pkg/test" + "github.com/google/go-cmp/cmp" +) + +func TestWrap(t *testing.T) { + type args struct { + err error + message string + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context", + }, + want: Errorf("very useful context: %w", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrap(tc.args.err, tc.args.message) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrap(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestWrapf(t *testing.T) { + type args struct { + err error + message string + args []interface{} + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context about %s", + args: []interface{}{"ducks"}, + }, + want: Errorf("very useful context about %s: %w", "ducks", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrapf(tc.args.err, tc.args.message, tc.args.args...) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrapf(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestCause(t *testing.T) { + cases := map[string]struct { + err error + want error + }{ + "NilError": { + err: nil, + want: nil, + }, + "BareError": { + err: New("boom"), + want: New("boom"), + }, + "WrappedError": { + err: Wrap(Wrap(New("boom"), "interstitial context"), "very important context"), + want: New("boom"), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Cause(tc.err) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Cause(...): -want, +got:\n%s", diff) + } + }) + } +}