Skip to content

Commit a6c2e1d

Browse files
authored
Merge joomcode/unstable_master
2 parents 4e359d1 + 19776b6 commit a6c2e1d

File tree

6 files changed

+250
-42
lines changed

6 files changed

+250
-42
lines changed

error.go

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,12 @@ func (e *Error) HasTrait(key Trait) bool {
103103
return false
104104
}
105105

106-
// IsOfType is a proper type check for an error.
106+
// IsOfType is a proper type check for an errorx-based errors.
107107
// It takes the transparency and error types hierarchy into account,
108108
// so that type check against any supertype of the original cause passes.
109+
// Go 1.13 and above: it also tolerates non-errorx errors in chain if those errors support errors unwrap.
109110
func (e *Error) IsOfType(t *Type) bool {
110-
cause := e
111-
for cause != nil {
112-
if !cause.transparent {
113-
return cause.errorType.IsOfType(t)
114-
}
115-
116-
cause = Cast(cause.Cause())
117-
}
118-
119-
return false
111+
return e.isOfType(t)
120112
}
121113

122114
// Type returns the exact type of this error.
@@ -156,6 +148,25 @@ func (e *Error) Cause() error {
156148
return e.cause
157149
}
158150

151+
// Is returns true if and only if target is errorx error that passes errorx type check against current error.
152+
// This behaviour is exactly the same as that of IsOfType().
153+
// See also: errors.Is()
154+
func (e *Error) Is(target error) bool {
155+
typedTarget := Cast(target)
156+
return typedTarget != nil && IsOfType(e, typedTarget.Type())
157+
}
158+
159+
// From errors package: if e.Unwrap() returns a non-nil error w, then we say that e wraps w.
160+
// Unwrap returns cause of current error in case it is wrapped transparently, nil otherwise.
161+
// See also: errors.Unwrap()
162+
func (e *Error) Unwrap() error {
163+
if e.cause != nil && e.transparent {
164+
return e.cause
165+
} else {
166+
return nil
167+
}
168+
}
169+
159170
// Format implements the Formatter interface.
160171
// Supported verbs:
161172
//
@@ -169,12 +180,12 @@ func (e *Error) Format(s fmt.State, verb rune) {
169180
message := e.fullMessage()
170181
switch verb {
171182
case 'v':
172-
io.WriteString(s, message)
183+
_, _ = io.WriteString(s, message)
173184
if s.Flag('+') {
174185
e.stackTrace.Format(s, verb)
175186
}
176187
case 's':
177-
io.WriteString(s, message)
188+
_, _ = io.WriteString(s, message)
178189
}
179190
}
180191

@@ -245,29 +256,3 @@ func (e *Error) messageText() string {
245256
}
246257
return message
247258
}
248-
249-
func joinStringsIfNonEmpty(delimiter string, parts ...string) string {
250-
switch len(parts) {
251-
case 0:
252-
return ""
253-
case 1:
254-
return parts[0]
255-
case 2:
256-
if len(parts[0]) == 0 {
257-
return parts[1]
258-
} else if len(parts[1]) == 0 {
259-
return parts[0]
260-
} else {
261-
return parts[0] + delimiter + parts[1]
262-
}
263-
default:
264-
filteredParts := make([]string, 0, len(parts))
265-
for _, part := range parts {
266-
if len(part) > 0 {
267-
filteredParts = append(filteredParts, part)
268-
}
269-
}
270-
271-
return strings.Join(filteredParts, delimiter)
272-
}
273-
}

error_112.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// +build !go1.13
2+
3+
package errorx
4+
5+
func isOfType(err error, t *Type) bool {
6+
e := Cast(err)
7+
return e != nil && e.IsOfType(t)
8+
}
9+
10+
func (e *Error) isOfType(t *Type) bool {
11+
cause := e
12+
for cause != nil {
13+
if !cause.transparent {
14+
return cause.errorType.IsOfType(t)
15+
}
16+
17+
cause = Cast(cause.Cause())
18+
}
19+
20+
return false
21+
}
22+
23+

error_113.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// +build go1.13
2+
3+
package errorx
4+
5+
import "errors"
6+
7+
func isOfType(err error, t *Type) bool {
8+
e := burrowForTyped(err)
9+
return e != nil && e.IsOfType(t)
10+
}
11+
12+
func (e *Error) isOfType(t *Type) bool {
13+
cause := e
14+
for cause != nil {
15+
if !cause.transparent {
16+
return cause.errorType.IsOfType(t)
17+
}
18+
19+
cause = burrowForTyped(cause.Cause())
20+
}
21+
22+
return false
23+
}
24+
25+
// burrowForTyped returns either the first *Error in unwrap chain or nil
26+
func burrowForTyped(err error) *Error {
27+
raw := err
28+
for raw != nil {
29+
typed := Cast(raw)
30+
if typed != nil {
31+
return typed
32+
}
33+
34+
raw = errors.Unwrap(raw)
35+
}
36+
37+
return nil
38+
}

error_113_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// +build go1.13
2+
3+
package errorx
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
"io"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestErrorUnwrap(t *testing.T) {
15+
t.Run("Trivial", func(t *testing.T) {
16+
err := testType.NewWithNoMessage()
17+
unwrapped := errors.Unwrap(err)
18+
require.Nil(t, unwrapped)
19+
})
20+
21+
t.Run("Wrap", func(t *testing.T) {
22+
err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "")
23+
unwrapped := errors.Unwrap(err)
24+
require.Nil(t, unwrapped)
25+
})
26+
27+
t.Run("WrapForeign", func(t *testing.T) {
28+
err := testTypeBar1.Wrap(io.EOF, "")
29+
unwrapped := errors.Unwrap(err)
30+
require.Nil(t, unwrapped)
31+
})
32+
33+
t.Run("Decorate", func(t *testing.T) {
34+
err := Decorate(testType.NewWithNoMessage(), "")
35+
unwrapped := errors.Unwrap(err)
36+
require.NotNil(t, unwrapped)
37+
require.True(t, IsOfType(unwrapped, testType))
38+
require.True(t, Cast(unwrapped).Type() == testType)
39+
})
40+
41+
t.Run("DecorateForeign", func(t *testing.T) {
42+
err := Decorate(io.EOF, "")
43+
unwrapped := errors.Unwrap(err)
44+
require.NotNil(t, unwrapped)
45+
require.True(t, errors.Is(unwrapped, io.EOF))
46+
require.True(t, unwrapped == io.EOF)
47+
})
48+
49+
t.Run("Nested", func(t *testing.T) {
50+
err := Decorate(Decorate(testType.NewWithNoMessage(), ""), "")
51+
unwrapped := errors.Unwrap(err)
52+
require.NotNil(t, unwrapped)
53+
unwrapped = errors.Unwrap(unwrapped)
54+
require.NotNil(t, unwrapped)
55+
require.True(t, IsOfType(unwrapped, testType))
56+
})
57+
58+
t.Run("NestedWrapped", func(t *testing.T) {
59+
err := Decorate(testTypeBar1.Wrap(testType.NewWithNoMessage(), ""), "")
60+
unwrapped := errors.Unwrap(err)
61+
require.NotNil(t, unwrapped)
62+
require.True(t, IsOfType(unwrapped, testTypeBar1))
63+
unwrapped = errors.Unwrap(unwrapped)
64+
require.Nil(t, unwrapped)
65+
})
66+
67+
t.Run("NestedForeign", func(t *testing.T) {
68+
err := Decorate(Decorate(io.EOF, ""), "")
69+
unwrapped := errors.Unwrap(err)
70+
require.NotNil(t, unwrapped)
71+
unwrapped = errors.Unwrap(unwrapped)
72+
require.NotNil(t, unwrapped)
73+
require.True(t, errors.Is(unwrapped, io.EOF))
74+
})
75+
}
76+
77+
func TestErrorIs(t *testing.T) {
78+
t.Run("Trivial", func(t *testing.T) {
79+
err := testType.NewWithNoMessage()
80+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
81+
require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage()))
82+
})
83+
84+
t.Run("Wrap", func(t *testing.T) {
85+
err := testTypeBar1.Wrap(testType.NewWithNoMessage(), "")
86+
require.False(t, errors.Is(err, testType.NewWithNoMessage()))
87+
require.True(t, errors.Is(err, testTypeBar1.NewWithNoMessage()))
88+
})
89+
90+
t.Run("Supertype", func(t *testing.T) {
91+
err := testSubtype0.Wrap(testTypeBar1.NewWithNoMessage(), "")
92+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
93+
require.True(t, errors.Is(err, testSubtype0.NewWithNoMessage()))
94+
require.False(t, errors.Is(err, testTypeBar1.NewWithNoMessage()))
95+
})
96+
97+
t.Run("Decorate", func(t *testing.T) {
98+
err := Decorate(testType.NewWithNoMessage(), "")
99+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
100+
})
101+
102+
t.Run("DecorateForeign", func(t *testing.T) {
103+
err := Decorate(io.EOF, "")
104+
require.True(t, errors.Is(err, io.EOF))
105+
})
106+
}
107+
108+
func TestErrorsAndErrorx(t *testing.T) {
109+
t.Run("DecoratedForeign", func(t *testing.T) {
110+
err := fmt.Errorf("error test: %w", testType.NewWithNoMessage())
111+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
112+
require.True(t, IsOfType(err, testType))
113+
})
114+
115+
t.Run("LayeredDecorate", func(t *testing.T) {
116+
err := Decorate(fmt.Errorf("error test: %w", testType.NewWithNoMessage()), "test")
117+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
118+
require.True(t, IsOfType(err, testType))
119+
})
120+
121+
t.Run("LayeredDecorateAgain", func(t *testing.T) {
122+
err := fmt.Errorf("error test: %w", Decorate(io.EOF, "test"))
123+
require.True(t, errors.Is(err, io.EOF))
124+
})
125+
126+
t.Run("Wrap", func(t *testing.T) {
127+
err := fmt.Errorf("error test: %w", testType.Wrap(io.EOF, "test"))
128+
require.False(t, errors.Is(err, io.EOF))
129+
require.True(t, errors.Is(err, testType.NewWithNoMessage()))
130+
})
131+
}

helper.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package errorx
2+
3+
import "strings"
4+
5+
func joinStringsIfNonEmpty(delimiter string, parts ...string) string {
6+
switch len(parts) {
7+
case 0:
8+
return ""
9+
case 1:
10+
return parts[0]
11+
case 2:
12+
if len(parts[0]) == 0 {
13+
return parts[1]
14+
} else if len(parts[1]) == 0 {
15+
return parts[0]
16+
} else {
17+
return parts[0] + delimiter + parts[1]
18+
}
19+
default:
20+
filteredParts := make([]string, 0, len(parts))
21+
for _, part := range parts {
22+
if len(part) > 0 {
23+
filteredParts = append(filteredParts, part)
24+
}
25+
}
26+
27+
return strings.Join(filteredParts, delimiter)
28+
}
29+
}
30+

type.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ func (t *Type) NewWithNoMessage() *Error {
5656
// The original error will not pass its dynamic properties, and those are accessible only via direct walk over Cause() chain.
5757
// Without args, leaves the original message intact, so a message may be generated or provided externally.
5858
// With args, a formatting is performed, and it is therefore expected a format string to be constant.
59+
// NB: Wrap is NOT the reverse of errors.Unwrap() or Error.Unwrap() method; name may be changed in future releases to avoid confusion.
5960
func (t *Type) Wrap(err error, message string, args ...interface{}) *Error {
6061
return NewErrorBuilder(t).
6162
WithConditionallyFormattedMessage(message, args...).
@@ -96,10 +97,10 @@ func (t *Type) HasTrait(key Trait) bool {
9697

9798
// IsOfType is a type check for errors.
9899
// Returns true either if both are of exactly the same type, or if the same is true for one of current type's ancestors.
99-
// For an error that does not have an errorx type, returns false.
100+
// Go 1.12 and below: for an error that does not have an errorx type, returns false.
101+
// Go 1.13 and above: for an error that does not have an errorx type, returns false unless it wraps another error of errorx type.
100102
func IsOfType(err error, t *Type) bool {
101-
e := Cast(err)
102-
return e != nil && e.IsOfType(t)
103+
return isOfType(err, t)
103104
}
104105

105106
// Supertype returns a parent type, if present.

0 commit comments

Comments
 (0)