Skip to content

Commit

Permalink
Merge pull request #9 from dmcgowan/resolve-error
Browse files Browse the repository at this point in the history
Add a resolve error function to return first error
  • Loading branch information
dmcgowan authored Jun 21, 2024
2 parents 6fb6cf0 + 9f87502 commit 6c7f402
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 0 deletions.
116 changes: 116 additions & 0 deletions resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Copyright The containerd 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 errdefs

import "context"

// Resolve returns the first error found in the error chain which matches an
// error defined in this package or context error. A raw, unwrapped error is
// returned or ErrUnknown if no matching error is found.
//
// This is useful for determining a response code based on the outermost wrapped
// error rather than the original cause. For example, a not found error deep
// in the code may be wrapped as an invalid argument. When determining status
// code from Is* functions, the depth or ordering of the error is not
// considered.
//
// The search order is depth first, a wrapped error returned from any part of
// the chain from `Unwrap() error` will be returned before any joined errors
// as returned by `Unwrap() []error`.
func Resolve(err error) error {
if err == nil {
return nil
}
err = firstError(err)
if err == nil {
err = ErrUnknown
}
return err
}

func firstError(err error) error {
for {
switch err {
case ErrUnknown,
ErrInvalidArgument,
ErrNotFound,
ErrAlreadyExists,
ErrPermissionDenied,
ErrResourceExhausted,
ErrFailedPrecondition,
ErrConflict,
ErrNotModified,
ErrAborted,
ErrOutOfRange,
ErrNotImplemented,
ErrInternal,
ErrUnavailable,
ErrDataLoss,
ErrUnauthenticated,
context.DeadlineExceeded,
context.Canceled:
return err
}
switch e := err.(type) {
case unknown:
return ErrUnknown
case invalidParameter:
return ErrInvalidArgument
case notFound:
return ErrNotFound
// Skip ErrAlreadyExists, no interface defined
case forbidden:
return ErrPermissionDenied
// Skip ErrResourceExhasuted, no interface defined
// Skip ErrFailedPrecondition, no interface defined
case conflict:
return ErrConflict
case notModified:
return ErrNotModified
// Skip ErrAborted, no interface defined
// Skip ErrOutOfRange, no interface defined
case notImplemented:
return ErrNotImplemented
case system:
return ErrInternal
case unavailable:
return ErrUnavailable
case dataLoss:
return ErrDataLoss
case unauthorized:
return ErrUnauthenticated
case deadlineExceeded:
return context.DeadlineExceeded
case cancelled:
return context.Canceled
case interface{ Unwrap() error }:
err = e.Unwrap()
if err == nil {
return nil
}
case interface{ Unwrap() []error }:
for _, ue := range e.Unwrap() {
if fe := firstError(ue); fe != nil {
return fe
}
}
return nil
default:
return nil
}
}
}
91 changes: 91 additions & 0 deletions resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright The containerd 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 errdefs

import (
"context"
"errors"
"fmt"
"testing"
)

func TestResolve(t *testing.T) {
for i, tc := range []struct {
err error
resolved error
}{
{nil, nil},
{wrap(ErrUnknown), ErrUnknown},
{wrap(ErrNotFound), ErrNotFound},
{wrap(ErrInvalidArgument), ErrInvalidArgument},
{wrap(ErrNotFound), ErrNotFound},
{wrap(ErrAlreadyExists), ErrAlreadyExists},
{wrap(ErrPermissionDenied), ErrPermissionDenied},
{wrap(ErrResourceExhausted), ErrResourceExhausted},
{wrap(ErrFailedPrecondition), ErrFailedPrecondition},
{wrap(ErrConflict), ErrConflict},
{wrap(ErrNotModified), ErrNotModified},
{wrap(ErrAborted), ErrAborted},
{wrap(ErrOutOfRange), ErrOutOfRange},
{wrap(ErrNotImplemented), ErrNotImplemented},
{wrap(ErrInternal), ErrInternal},
{wrap(ErrUnavailable), ErrUnavailable},
{wrap(ErrDataLoss), ErrDataLoss},
{wrap(ErrUnauthenticated), ErrUnauthenticated},
{wrap(context.DeadlineExceeded), context.DeadlineExceeded},
{wrap(context.Canceled), context.Canceled},
{errors.Join(errors.New("untyped"), wrap(ErrInvalidArgument)), ErrInvalidArgument},
{errors.Join(ErrConflict, ErrNotFound), ErrConflict},
{errors.New("untyped"), ErrUnknown},
{errors.Join(wrap(ErrUnauthenticated), ErrNotModified), ErrUnauthenticated},
{ErrDataLoss, ErrDataLoss},
{errors.Join(ErrOutOfRange), ErrOutOfRange},
{errors.Join(ErrNotImplemented, ErrInternal), ErrNotImplemented},
{context.Canceled, context.Canceled},
{testUnavailable{}, ErrUnavailable},
{wrap(testUnavailable{}), ErrUnavailable},
{errors.Join(testUnavailable{}, ErrPermissionDenied), ErrUnavailable},
{errors.Join(errors.New("untyped join")), ErrUnknown},
{errors.Join(errors.New("untyped1"), errors.New("untyped2")), ErrUnknown},
} {
name := fmt.Sprintf("%d-%s", i, errorString(tc.resolved))
tc := tc
t.Run(name, func(t *testing.T) {
resolved := Resolve(tc.err)
if resolved != tc.resolved {
t.Errorf("Expected %s, got %s", tc.resolved, resolved)
}
})
}
}

func wrap(err error) error {
err = fmt.Errorf("wrapped error: %w", err)
return fmt.Errorf("%w and also %w", err, ErrUnknown)
}

func errorString(err error) string {
if err == nil {
return "nil"
}
return err.Error()
}

type testUnavailable struct{}

func (testUnavailable) Error() string { return "" }
func (testUnavailable) Unavailable() {}

0 comments on commit 6c7f402

Please sign in to comment.