Skip to content

Commit

Permalink
Add a resolve error function to return first error
Browse files Browse the repository at this point in the history
When an error object is returned and must be resolved to a single return
error, the first error matching one defined by this package should be
returned.

Signed-off-by: Derek McGowan <derek@mcg.dev>
  • Loading branch information
dmcgowan committed Jun 21, 2024
1 parent 038bb7b commit 9f87502
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 9f87502

Please sign in to comment.