From 497f29c75cb99b1872498a9baa3276dd358615bd Mon Sep 17 00:00:00 2001 From: Christian Reinecke Date: Wed, 2 Oct 2024 12:50:25 +0200 Subject: [PATCH 1/2] feat: add errors/reason --- errors/reason/reason.go | 70 ++++++++++++++++++++++++++++++++++++ errors/reason/reason_test.go | 25 +++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 errors/reason/reason.go create mode 100644 errors/reason/reason_test.go diff --git a/errors/reason/reason.go b/errors/reason/reason.go new file mode 100644 index 0000000..85960b8 --- /dev/null +++ b/errors/reason/reason.go @@ -0,0 +1,70 @@ +// Package reason handles error reasons to separate internal from user errors. +package reason + +import ( + "errors" + "fmt" +) + +// InternalReason is the reason set when the system is experiencing +// an error that the user cannot resolve. +const InternalReason = "The system has an internal error" + +// Error is a reason error that is detectable. +// +// This should not be used as a "normal" error, +// instead extract the reasons from the chains +// using Extract. +type Error struct { + // Msg is the reason message. + Msg string +} + +// Errorf returns a formatted reason error. +func Errorf(format string, a ...any) Error { + return Error{Msg: fmt.Sprintf(format, a...)} +} + +// Error return the reason as if it were a message. +// This is used to conform with the error type. +func (e Error) Error() string { + return e.Msg +} + +// Extract removes all reason errors from the error +// chains, returning all other errors and the reason +// messages. +func Extract(err error) ([]string, error) { + //nolint:errorlint // This is the only way to check for the interface. + switch x := err.(type) { + case interface{ Unwrap() []error }: + var ( + reasons []string + errs []error + ) + for _, err = range x.Unwrap() { + r, e := Extract(err) + reasons = append(reasons, r...) + if e != nil { + errs = append(errs, e) + } + } + + switch len(errs) { + case 0: + return reasons, nil + case 1: + return reasons, errs[0] + default: + return reasons, errors.Join(errs...) + } + case interface{ Unwrap() error }: + return Extract(x.Unwrap()) + default: + var r Error + if errors.As(err, &r) { + return []string{r.Msg}, nil + } + return nil, err + } +} diff --git a/errors/reason/reason_test.go b/errors/reason/reason_test.go new file mode 100644 index 0000000..58074ee --- /dev/null +++ b/errors/reason/reason_test.go @@ -0,0 +1,25 @@ +package reason_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/hamba/pkg/v2/errors/reason" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtract(t *testing.T) { + var errs error + errs = errors.Join(errs, errors.New("test1")) + errs = errors.Join(errs, reason.Error{Msg: "First Error"}) + errs = errors.Join(errs, fmt.Errorf("some error: %w", reason.Errorf("Second %s", "Error"))) + errs = errors.Join(errs, errors.New("test2")) + + reasons, errs := reason.Extract(errs) + + require.NotEmpty(t, errs) + assert.Equal(t, "test1\ntest2", errs.Error()) + assert.Equal(t, []string{"First Error", "Second Error"}, reasons) +} From bda9df0dfbb7b3b478737abd0b738dcfbde0075b Mon Sep 17 00:00:00 2001 From: Christian Reinecke Date: Wed, 2 Oct 2024 12:52:49 +0200 Subject: [PATCH 2/2] fix: package comment --- errors/reason/reason.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors/reason/reason.go b/errors/reason/reason.go index 85960b8..a8e8348 100644 --- a/errors/reason/reason.go +++ b/errors/reason/reason.go @@ -1,4 +1,4 @@ -// Package reason handles error reasons to separate internal from user errors. +// Package reason contains types and functions commonly used to handle reason detection and extraction. package reason import (