Skip to content

Commit

Permalink
Add support for custom error messages
Browse files Browse the repository at this point in the history
Add a function to each defined error to set a custom message.

Signed-off-by: Derek McGowan <derek@mcg.dev>
  • Loading branch information
dmcgowan committed Jul 2, 2024
1 parent 6c7f402 commit dc9b20e
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
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)
}
}

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

0 comments on commit dc9b20e

Please sign in to comment.