Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom error messages #10

Merged
merged 1 commit into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ func (errUnknown) Error() string { return "unknown" }

func (errUnknown) Unknown() {}

func (e errUnknown) WithMessage(msg string) error {
return customMessage{e, msg}
}

// unknown maps to Moby's "ErrUnknown"
type unknown interface {
Unknown()
Expand All @@ -86,6 +90,10 @@ func (errInvalidArgument) Error() string { return "invalid argument" }

func (errInvalidArgument) InvalidParameter() {}

func (e errInvalidArgument) WithMessage(msg string) error {
return customMessage{e, msg}
}

// invalidParameter maps to Moby's "ErrInvalidParameter"
type invalidParameter interface {
InvalidParameter()
Expand Down Expand Up @@ -113,6 +121,10 @@ func (errNotFound) Error() string { return "not found" }

func (errNotFound) NotFound() {}

func (e errNotFound) WithMessage(msg string) error {
return customMessage{e, msg}
}

// notFound maps to Moby's "ErrNotFound"
type notFound interface {
NotFound()
Expand All @@ -127,6 +139,10 @@ type errAlreadyExists struct{}

func (errAlreadyExists) Error() string { return "already exists" }

func (e errAlreadyExists) WithMessage(msg string) error {
return customMessage{e, msg}
}

// IsAlreadyExists returns true if the error is due to an already existing
// metadata item
func IsAlreadyExists(err error) bool {
Expand All @@ -137,6 +153,10 @@ type errPermissionDenied struct{}

func (errPermissionDenied) Error() string { return "permission denied" }

func (e errPermissionDenied) WithMessage(msg string) error {
return customMessage{e, msg}
}

// forbidden maps to Moby's "ErrForbidden"
type forbidden interface {
Forbidden()
Expand All @@ -152,6 +172,10 @@ type errResourceExhausted struct{}

func (errResourceExhausted) Error() string { return "resource exhausted" }

func (e errResourceExhausted) WithMessage(msg string) error {
return customMessage{e, msg}
}

// IsResourceExhausted returns true if the error is due to
// a lack of resources or too many attempts.
func IsResourceExhausted(err error) bool {
Expand All @@ -162,6 +186,10 @@ type errFailedPrecondition struct{}

func (e errFailedPrecondition) Error() string { return "failed precondition" }

func (e errFailedPrecondition) WithMessage(msg string) error {
return customMessage{e, msg}
}

// IsFailedPrecondition returns true if an operation could not proceed due to
// the lack of a particular condition
func IsFailedPrecondition(err error) bool {
Expand All @@ -174,6 +202,10 @@ func (errConflict) Error() string { return "conflict" }

func (errConflict) Conflict() {}

func (e errConflict) WithMessage(msg string) error {
return customMessage{e, msg}
}

// conflict maps to Moby's "ErrConflict"
type conflict interface {
Conflict()
Expand All @@ -191,6 +223,10 @@ func (errNotModified) Error() string { return "not modified" }

func (errNotModified) NotModified() {}

func (e errNotModified) WithMessage(msg string) error {
return customMessage{e, msg}
}

// notModified maps to Moby's "ErrNotModified"
type notModified interface {
NotModified()
Expand All @@ -206,6 +242,10 @@ type errAborted struct{}

func (errAborted) Error() string { return "aborted" }

func (e errAborted) WithMessage(msg string) error {
return customMessage{e, msg}
}

// IsAborted returns true if an operation was aborted.
func IsAborted(err error) bool {
return errors.Is(err, errAborted{})
Expand All @@ -215,6 +255,10 @@ type errOutOfRange struct{}

func (errOutOfRange) Error() string { return "out of range" }

func (e errOutOfRange) WithMessage(msg string) error {
return customMessage{e, msg}
}

// IsOutOfRange returns true if an operation could not proceed due
// to data being out of the expected range.
func IsOutOfRange(err error) bool {
Expand All @@ -227,6 +271,10 @@ func (errNotImplemented) Error() string { return "not implemented" }

func (errNotImplemented) NotImplemented() {}

func (e errNotImplemented) WithMessage(msg string) error {
return customMessage{e, msg}
}

// notImplemented maps to Moby's "ErrNotImplemented"
type notImplemented interface {
NotImplemented()
Expand All @@ -243,6 +291,10 @@ func (errInternal) Error() string { return "internal" }

func (errInternal) System() {}

func (e errInternal) WithMessage(msg string) error {
return customMessage{e, msg}
}

// system maps to Moby's "ErrSystem"
type system interface {
System()
Expand All @@ -259,6 +311,10 @@ func (errUnavailable) Error() string { return "unavailable" }

func (errUnavailable) Unavailable() {}

func (e errUnavailable) WithMessage(msg string) error {
return customMessage{e, msg}
}

// unavailable maps to Moby's "ErrUnavailable"
type unavailable interface {
Unavailable()
Expand All @@ -275,6 +331,10 @@ func (errDataLoss) Error() string { return "data loss" }

func (errDataLoss) DataLoss() {}

func (e errDataLoss) WithMessage(msg string) error {
return customMessage{e, msg}
}

// dataLoss maps to Moby's "ErrDataLoss"
type dataLoss interface {
DataLoss()
Expand All @@ -291,6 +351,10 @@ func (errUnauthorized) Error() string { return "unauthorized" }

func (errUnauthorized) Unauthorized() {}

func (e errUnauthorized) WithMessage(msg string) error {
return customMessage{e, msg}
}

// unauthorized maps to Moby's "ErrUnauthorized"
type unauthorized interface {
Unauthorized()
Expand All @@ -307,6 +371,8 @@ func isInterface[T any](err error) bool {
switch x := err.(type) {
case T:
return true
case customMessage:
err = x.err
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
Expand All @@ -324,3 +390,22 @@ func isInterface[T any](err error) bool {
}
}
}

// customMessage is used to provide a defined error with a custom message.
// The message is not wrapped but can be compared by the `Is(error) bool` interface.
type customMessage struct {
err error
msg string
}

func (c customMessage) Is(err error) bool {
return c.err == err
}

func (c customMessage) As(target any) bool {
return errors.As(c.err, target)
}

func (c customMessage) Error() string {
return c.msg
}
114 changes: 114 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package errdefs
import (
"context"
"errors"
"reflect"
"testing"
)

Expand All @@ -44,6 +45,119 @@ func TestInvalidArgument(t *testing.T) {
}
}

func TestErrorEquivalence(t *testing.T) {
var e1 error = ErrAborted
var e2 error = ErrUnknown
if e1 == e2 {
t.Fatal("should not equal the same error")
}
if errors.Is(e1, e2) {
t.Fatal("errors.Is should not return true")
}

var e3 error = errAborted{}
if e1 != e3 {
t.Fatal("new instance should be equivalent")
}
if !errors.Is(e1, e3) {
t.Fatal("errors.Is should be true")
}
if !errors.Is(e3, e1) {
t.Fatal("errors.Is should be true")
}
var aborted errAborted
if !errors.As(e1, &aborted) {
t.Fatal("errors.As should be true")
}

var e4 = ErrAborted.WithMessage("custom message")
if e1 == e4 {
t.Fatal("should not equal the same error")
}

if !errors.Is(e4, e1) {
t.Fatal("errors.Is should be true, e1 is in the tree of e4")
}

if errors.Is(e1, e4) {
t.Fatal("errors.Is should be false, e1 is not a custom message")
}

if !errors.As(e4, &aborted) {
t.Fatal("errors.As should be true")
}

var custom customMessage
if !errors.As(e4, &custom) {
t.Fatal("errors.As should be true")
}
if custom.msg != "custom message" {
t.Fatalf("unexpected custom message: %q", custom.msg)
}
if custom.err != e1 {
t.Fatalf("unexpected custom message error: %v", custom.err)
}
akhilerm marked this conversation as resolved.
Show resolved Hide resolved
}

func TestWithMessage(t *testing.T) {
testErrors := []error{ErrUnknown,
ErrInvalidArgument,
ErrNotFound,
ErrAlreadyExists,
ErrPermissionDenied,
ErrResourceExhausted,
ErrFailedPrecondition,
ErrConflict,
ErrNotModified,
ErrAborted,
ErrOutOfRange,
ErrNotImplemented,
ErrInternal,
ErrUnavailable,
ErrDataLoss,
ErrUnauthenticated,
}
for _, err := range testErrors {
e1 := err
t.Run(err.Error(), func(t *testing.T) {
wm, ok := e1.(interface{ WithMessage(string) error })
if !ok {
t.Fatal("WithMessage not supported")
}
e2 := wm.WithMessage("custom message")

if e1 == e2 {
t.Fatal("should not equal the same error")
}

if !errors.Is(e2, e1) {
t.Fatal("errors.Is should return true")
}

if errors.Is(e1, e2) {
t.Fatal("errors.Is should be false, e1 is not a custom message")
}

var raw = reflect.New(reflect.TypeOf(e1)).Interface()
if !errors.As(e2, raw) {
t.Fatal("errors.As should be true")
}

var custom customMessage
if !errors.As(e2, &custom) {
t.Fatal("errors.As should be true")
}
if custom.msg != "custom message" {
t.Fatalf("unexpected custom message: %q", custom.msg)
}
if custom.err != e1 {
t.Fatalf("unexpected custom message error: %v", custom.err)
}

})
}
}

type customInvalidArgument struct{}

func (*customInvalidArgument) Error() string {
Expand Down
2 changes: 2 additions & 0 deletions resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func firstError(err error) error {
return err
}
switch e := err.(type) {
case customMessage:
err = e.err
case unknown:
return ErrUnknown
case invalidParameter:
Expand Down
3 changes: 3 additions & 0 deletions resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func TestResolve(t *testing.T) {
{errors.Join(testUnavailable{}, ErrPermissionDenied), ErrUnavailable},
{errors.Join(errors.New("untyped join")), ErrUnknown},
{errors.Join(errors.New("untyped1"), errors.New("untyped2")), ErrUnknown},
{ErrNotFound.WithMessage("something else"), ErrNotFound},
{wrap(ErrNotFound.WithMessage("something else")), ErrNotFound},
{errors.Join(ErrNotFound.WithMessage("something else"), ErrPermissionDenied), ErrNotFound},
} {
name := fmt.Sprintf("%d-%s", i, errorString(tc.resolved))
tc := tc
Expand Down
Loading