Skip to content

Commit 497f29c

Browse files
committed
feat: add errors/reason
1 parent a68b800 commit 497f29c

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

errors/reason/reason.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Package reason handles error reasons to separate internal from user errors.
2+
package reason
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
)
8+
9+
// InternalReason is the reason set when the system is experiencing
10+
// an error that the user cannot resolve.
11+
const InternalReason = "The system has an internal error"
12+
13+
// Error is a reason error that is detectable.
14+
//
15+
// This should not be used as a "normal" error,
16+
// instead extract the reasons from the chains
17+
// using Extract.
18+
type Error struct {
19+
// Msg is the reason message.
20+
Msg string
21+
}
22+
23+
// Errorf returns a formatted reason error.
24+
func Errorf(format string, a ...any) Error {
25+
return Error{Msg: fmt.Sprintf(format, a...)}
26+
}
27+
28+
// Error return the reason as if it were a message.
29+
// This is used to conform with the error type.
30+
func (e Error) Error() string {
31+
return e.Msg
32+
}
33+
34+
// Extract removes all reason errors from the error
35+
// chains, returning all other errors and the reason
36+
// messages.
37+
func Extract(err error) ([]string, error) {
38+
//nolint:errorlint // This is the only way to check for the interface.
39+
switch x := err.(type) {
40+
case interface{ Unwrap() []error }:
41+
var (
42+
reasons []string
43+
errs []error
44+
)
45+
for _, err = range x.Unwrap() {
46+
r, e := Extract(err)
47+
reasons = append(reasons, r...)
48+
if e != nil {
49+
errs = append(errs, e)
50+
}
51+
}
52+
53+
switch len(errs) {
54+
case 0:
55+
return reasons, nil
56+
case 1:
57+
return reasons, errs[0]
58+
default:
59+
return reasons, errors.Join(errs...)
60+
}
61+
case interface{ Unwrap() error }:
62+
return Extract(x.Unwrap())
63+
default:
64+
var r Error
65+
if errors.As(err, &r) {
66+
return []string{r.Msg}, nil
67+
}
68+
return nil, err
69+
}
70+
}

errors/reason/reason_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package reason_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/hamba/pkg/v2/errors/reason"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestExtract(t *testing.T) {
14+
var errs error
15+
errs = errors.Join(errs, errors.New("test1"))
16+
errs = errors.Join(errs, reason.Error{Msg: "First Error"})
17+
errs = errors.Join(errs, fmt.Errorf("some error: %w", reason.Errorf("Second %s", "Error")))
18+
errs = errors.Join(errs, errors.New("test2"))
19+
20+
reasons, errs := reason.Extract(errs)
21+
22+
require.NotEmpty(t, errs)
23+
assert.Equal(t, "test1\ntest2", errs.Error())
24+
assert.Equal(t, []string{"First Error", "Second Error"}, reasons)
25+
}

0 commit comments

Comments
 (0)