Skip to content

Commit 92c8c61

Browse files
authored
Merge pull request #5 from zignd/add-is-err-composition
Add IsErrComposition to help check if a custom error type is a composition of Err
2 parents 42b9692 + 41b11e8 commit 92c8c61

File tree

4 files changed

+218
-119
lines changed

4 files changed

+218
-119
lines changed

errors.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package errors
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
47

58
type Data map[string]any
69

@@ -101,3 +104,28 @@ func WithCause(err error, cause error) error {
101104
return err
102105
}
103106
}
107+
108+
// IsErrComposition returns true if the provided error is a composition of Err or *Err.
109+
func IsErrComposition(err error) bool {
110+
typeOfErr := reflect.TypeOf(err)
111+
112+
if typeOfErr.Kind() == reflect.Pointer {
113+
typeOfErr = typeOfErr.Elem()
114+
}
115+
116+
if typeOfErr.Kind() != reflect.Struct {
117+
return false
118+
}
119+
120+
for i := 0; i < typeOfErr.NumField(); i++ {
121+
if typeOfErr.Field(i).Type == reflect.TypeOf(Err{}) {
122+
return true
123+
}
124+
125+
if typeOfErr.Field(i).Type == reflect.TypeOf((*Err)(nil)) {
126+
return true
127+
}
128+
}
129+
130+
return false
131+
}

errors_test.go

Lines changed: 143 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -8,133 +8,184 @@ import (
88
"testing"
99
)
1010

11+
// CustomError is a custom error type composed with Err.
12+
type CustomError struct {
13+
*Err
14+
}
15+
16+
// NewCustomError returns a new CustomError and adds a stack trace.
17+
func NewCustomError(message string) error {
18+
customError := CustomError{Err: &Err{Message: message}}
19+
WithStack(customError.Err)
20+
return customError
21+
}
22+
23+
// CustomError2 is a custom error type composed with Err.
24+
type CustomError2 struct {
25+
*Err
26+
}
27+
28+
// NewCustom2Error returns a new CustomError2 and adds a cause to the error.
29+
func NewCustom2Error(message string, cause error) error {
30+
customError2 := CustomError2{Err: &Err{Message: message}}
31+
WithCause(customError2.Err, cause)
32+
return customError2
33+
}
34+
35+
// customError3 is a custom error type not composed with Err.
36+
type customError3 struct{}
37+
38+
func (e customError3) Error() string {
39+
return "this is a custom error type"
40+
}
41+
1142
func TestNew(t *testing.T) {
12-
msg := "error message"
13-
if got := New(msg).Error(); got != msg {
14-
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
15-
return
16-
}
43+
t.Run("when New is provided with a message, it should create a new error with the message", func(t *testing.T) {
44+
msg := "error message"
45+
if got := New(msg).Error(); got != msg {
46+
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
47+
return
48+
}
49+
})
1750
}
1851

19-
func TestErrorc(t *testing.T) {
20-
msg := "error message"
21-
data := Data{
22-
"id": 1,
23-
"description": "fool",
24-
}
25-
26-
err := Errord(data, msg)
27-
if got := err.Error(); got != msg {
28-
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
29-
return
30-
}
31-
32-
if e := err.(*Err); !reflect.DeepEqual(e.Data, data) {
33-
t.Errorf(`wrong data, got %+v, expected %+v`, e.Data, data)
34-
return
35-
}
52+
func TestErrord(t *testing.T) {
53+
t.Run("when Errord is provided with a message and data, it should create a new error with the message and data", func(t *testing.T) {
54+
msg := "error message"
55+
data := Data{
56+
"id": 1,
57+
"description": "fool",
58+
}
59+
60+
err := Errord(data, msg)
61+
if got := err.Error(); got != msg {
62+
t.Errorf(`wrong error message, got "%v", expected "%v"`, got, msg)
63+
return
64+
}
65+
66+
if e := err.(*Err); !reflect.DeepEqual(e.Data, data) {
67+
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data)
68+
return
69+
}
70+
})
3671
}
3772

3873
func TestWrap(t *testing.T) {
39-
msg1 := "error message 1"
40-
err1 := New(msg1)
41-
msg2 := "error message 2"
42-
err2 := Wrap(err1, msg2)
43-
msg3 := "error message 3"
44-
err3 := Wrap(err2, msg3)
45-
got := err3.Error()
46-
expected := fmt.Sprintf("%s: %s: %s", msg3, msg2, msg1)
47-
if got != expected {
48-
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
49-
return
50-
}
74+
t.Run("when Wrap is provided with an error and a message, it should create a new error with the message and the provided error as the cause", func(t *testing.T) {
75+
msg1 := "error message 1"
76+
err1 := New(msg1)
77+
msg2 := "error message 2"
78+
err2 := Wrap(err1, msg2)
79+
msg3 := "error message 3"
80+
err3 := Wrap(err2, msg3)
81+
got := err3.Error()
82+
expected := fmt.Sprintf("%s: %s: %s", msg3, msg2, msg1)
83+
if got != expected {
84+
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
85+
return
86+
}
87+
})
5188
}
5289

5390
func TestWrapc(t *testing.T) {
54-
msg1 := "error message 1"
55-
err1 := errors.New(msg1)
56-
57-
msg2 := "error message 2"
58-
data2 := Data{
59-
"id": 2,
60-
"description": "bar",
61-
}
62-
err2 := Wrapd(err1, data2, msg2)
63-
64-
msg3 := "error message 3"
65-
data3 := Data{
66-
"id": 3,
67-
"description": "spam",
68-
}
69-
err3 := Wrapd(err2, data3, msg3)
70-
71-
msg4 := "error message 4"
72-
data4 := Data{
73-
"id": 4,
74-
"description": "spam",
75-
}
76-
err4 := Wrapd(err3, data4, msg4)
77-
78-
got := err4.Error()
79-
expected := fmt.Sprintf("%s: %s: %s: %s", msg4, msg3, msg2, msg1)
80-
if got != expected {
81-
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
82-
return
83-
}
84-
}
91+
t.Run("when Wrapd is provided with an error and data, it should add the data to the error", func(t *testing.T) {
92+
msg1 := "error message 1"
93+
err1 := errors.New(msg1)
94+
95+
msg2 := "error message 2"
96+
data2 := Data{
97+
"id": 2,
98+
"description": "bar",
99+
}
100+
err2 := Wrapd(err1, data2, msg2)
85101

86-
// CustomError is a custom error type composed with Err.
87-
type CustomError struct {
88-
*Err
89-
}
102+
msg3 := "error message 3"
103+
data3 := Data{
104+
"id": 3,
105+
"description": "spam",
106+
}
107+
err3 := Wrapd(err2, data3, msg3)
90108

91-
// NewCustomError returns a new CustomError and adds a stack trace.
92-
func NewCustomError(message string) error {
93-
customError := CustomError{Err: &Err{Message: message}}
94-
WithStack(customError.Err)
95-
return customError
109+
msg4 := "error message 4"
110+
data4 := Data{
111+
"id": 4,
112+
"description": "spam",
113+
}
114+
err4 := Wrapd(err3, data4, msg4)
115+
116+
got := err4.Error()
117+
expected := fmt.Sprintf("%s: %s: %s: %s", msg4, msg3, msg2, msg1)
118+
if got != expected {
119+
t.Errorf(`wrong error message, got "%s", expected "%s"`, got, expected)
120+
return
121+
}
122+
123+
if e := err4.(*Err); !reflect.DeepEqual(e.Data, data4) {
124+
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data4)
125+
return
126+
}
127+
128+
if e := err3.(*Err); !reflect.DeepEqual(e.Data, data3) {
129+
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data3)
130+
return
131+
}
132+
133+
if e := err2.(*Err); !reflect.DeepEqual(e.Data, data2) {
134+
t.Errorf(`wrong data, got "%+v", expected "%+v"`, e.Data, data2)
135+
return
136+
}
137+
})
96138
}
97139

98140
func TestWithStack(t *testing.T) {
99141
t.Run("when WithStack is provided with an error of type Err, it should add a stack trace to the error", func(t *testing.T) {
100142
err := NewCustomError("this is a custom error type with stack")
101143

102144
if err.(CustomError).Stack == nil {
103-
t.Errorf(`expected stack to be not nil, got nil`)
145+
t.Fatal("expected stack to be not nil, got nil")
104146
return
105147
}
106148

107149
outputStr := fmt.Sprintf("%+v", err)
108150
if !strings.Contains(outputStr, "message:") {
109-
t.Errorf(`expected "message:" to be in the output string, got %v`, outputStr)
110-
return
151+
t.Errorf(`expected "message:" to be in the output string, got "%v"`, outputStr)
111152
}
112153
if !strings.Contains(outputStr, "stack:") {
113-
t.Errorf(`expected "stack:" to be in the output string, got %v`, outputStr)
114-
return
154+
t.Errorf(`expected "stack:" to be in the output string, got "%v"`, outputStr)
115155
}
116156
})
117157
}
118158

119-
// CustomError2 is a custom error type composed with Err.
120-
type CustomError2 struct {
121-
*Err
122-
}
123-
124-
// NewCustom2Error returns a new CustomError2 and adds a cause to the error.
125-
func NewCustom2Error(message string, cause error) error {
126-
customError2 := CustomError2{Err: &Err{Message: message}}
127-
WithCause(customError2.Err, cause)
128-
return customError2
129-
}
130-
131159
func TestWithCause(t *testing.T) {
132160
t.Run("when WithCause is provided with an error and a cause, it should add the cause to the error", func(t *testing.T) {
133161
causeErr := New("inner error")
134162
err := NewCustom2Error("outer error", causeErr)
135163

136164
if err.(CustomError2).Cause != causeErr {
137-
t.Errorf(`expected cause to be %v, got %v`, causeErr, err.(CustomError2).Cause)
165+
t.Errorf(`expected cause to be "%v", got "%v"`, causeErr, err.(CustomError2).Cause)
166+
}
167+
})
168+
}
169+
170+
func TestIsErrComposition(t *testing.T) {
171+
t.Run("when a custom error type is composed with *Err, it should return true", func(t *testing.T) {
172+
err := NewCustomError("this is a custom error type with stack")
173+
if !IsErrComposition(err) {
174+
t.Errorf("expected IsErrComposition to return true, got false")
175+
}
176+
})
177+
178+
t.Run("when an error type is Pointer but the element type is a struct not composed with *Err, it should return false", func(t *testing.T) {
179+
err := errors.New("this is a regular error")
180+
if IsErrComposition(err) {
181+
t.Errorf("expected IsErrComposition to return false, got true")
182+
}
183+
})
184+
185+
t.Run("when a custom error type is not composed with *Err, it should return false", func(t *testing.T) {
186+
err := customError3{}
187+
if IsErrComposition(err) {
188+
t.Errorf("expected IsErrComposition to return false, got true")
138189
}
139190
})
140191
}

go113_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type customErr struct {
1616
func (c customErr) Error() string { return c.msg }
1717

1818
func TestGo113Compatibility(t *testing.T) {
19-
t.Run("Wrap should be able to return an error compatible with the standard library Is", func(t *testing.T) {
19+
t.Run("when Wrap is used to wrap a standard error, it should return an error compatible with the standard library Is", func(t *testing.T) {
2020
// First we create an error using the standard library
2121
err := stderrors.New("error that gets wrapped")
2222

@@ -25,11 +25,12 @@ func TestGo113Compatibility(t *testing.T) {
2525

2626
// Finally we check if the standard library Is function can handle our wrapped error
2727
if !stderrors.Is(wrapped, err) {
28-
t.Errorf("Wrap does not support Go 1.13 error chains")
28+
t.Errorf("our Wrap does not support Go 1.13 error chains")
2929
}
3030
})
3131

32-
t.Run("Is should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
32+
// Is should be able to handle errors created and wrapped using the standard Go features
33+
t.Run("when Is is used to check if an error is a certain error, it should behave just like the equivalent Is function in the standard library", func(t *testing.T) {
3334
// First we create an error using the standard Go features
3435
err := customErr{msg: "test message"}
3536
wrapped := fmt.Errorf("wrap it: %w", err)
@@ -38,9 +39,14 @@ func TestGo113Compatibility(t *testing.T) {
3839
if !Is(wrapped, err) {
3940
t.Error("Is failed")
4041
}
42+
43+
// Finally just to make sure, we check if the standard library Is function can handle it
44+
if !stderrors.Is(wrapped, err) {
45+
t.Error("stderrors.Is failed")
46+
}
4147
})
4248

43-
t.Run("As should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
49+
t.Run("when As is used to check if an error is a certain error, it should behave just like the equivalent As function in the standard library", func(t *testing.T) {
4450
// First we create an error using the standard Go features
4551
err := customErr{msg: "test message"}
4652
wrapped := fmt.Errorf("wrap it: %w", err)
@@ -50,14 +56,24 @@ func TestGo113Compatibility(t *testing.T) {
5056
if !As(wrapped, target) {
5157
t.Error("As failed")
5258
}
59+
60+
// Finally just to make sure, we check if the standard library As function can handle it
61+
if !stderrors.As(wrapped, target) {
62+
t.Error("stderrors.As failed")
63+
}
5364
})
5465

55-
t.Run("Unwrap should be able to handle errors created and wrapped using the standard Go features", func(t *testing.T) {
66+
// Unwrap should be able to handle errors created and wrapped using the standard Go features
67+
t.Run("when Unwrap is used to unwrap an error, it should behave just like the equivalent Unwrap function in the standard library", func(t *testing.T) {
5668
err := customErr{msg: "test message"}
5769
wrapped := fmt.Errorf("wrap it: %w", err)
5870

5971
if unwrappedErr := Unwrap(wrapped); !reflect.DeepEqual(unwrappedErr, err) {
6072
t.Error("Unwrap failed")
6173
}
74+
75+
if unwrappedErr := stderrors.Unwrap(wrapped); !reflect.DeepEqual(unwrappedErr, err) {
76+
t.Error("stderrors.Unwrap failed")
77+
}
6278
})
6379
}

0 commit comments

Comments
 (0)