From c80e0fa8d344adb48dcb57ed3742f06c777e80ac Mon Sep 17 00:00:00 2001 From: antiphp Date: Wed, 2 Oct 2024 12:55:42 +0200 Subject: [PATCH] feat: add errors/reason (#135) --- 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..a8e8348 --- /dev/null +++ b/errors/reason/reason.go @@ -0,0 +1,70 @@ +// Package reason contains types and functions commonly used to handle reason detection and extraction. +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) +}