Skip to content

Commit 8a0e206

Browse files
committed
Initial commit
0 parents  commit 8a0e206

File tree

6 files changed

+416
-0
lines changed

6 files changed

+416
-0
lines changed

error.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cerrors
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func New(msg string) error {
8+
return &cerror{
9+
msg: msg,
10+
frame: Caller(1),
11+
}
12+
}
13+
14+
func Newf(msg string, args ...interface{}) error {
15+
return &cerror{
16+
msg: fmt.Sprintf(msg, args...),
17+
frame: Caller(1),
18+
}
19+
}
20+
21+
func Wrap(err error, msg string) error {
22+
return &cerror{
23+
msg: msg,
24+
next: err,
25+
frame: Caller(1),
26+
}
27+
}
28+
29+
func Wrapf(err error, msg string, args ...interface{}) error {
30+
return &cerror{
31+
msg: fmt.Sprintf(msg, args...),
32+
next: err,
33+
frame: Caller(1),
34+
}
35+
}
36+
37+
type cerror struct {
38+
msg string
39+
next error
40+
frame Frame
41+
}
42+
43+
func (c *cerror) Format(s fmt.State, verb rune) {
44+
switch verb {
45+
case 'v':
46+
if s.Flag('+') || s.Flag('#') {
47+
Format(c, s, s.Flag('#'))
48+
return
49+
}
50+
fallthrough
51+
case 's':
52+
fmt.Fprint(s, c.Error())
53+
default:
54+
fmt.Fprintf(s, "%%!%s(cerror)", string(verb))
55+
}
56+
}
57+
58+
func (c *cerror) OwnMessage() string {
59+
return c.msg
60+
}
61+
62+
func (c *cerror) Frame() Frame {
63+
return c.frame
64+
}
65+
66+
func (c *cerror) Error() string {
67+
if c.next != nil {
68+
return c.msg + ": " + c.next.Error()
69+
}
70+
return c.msg
71+
}
72+
73+
func (c *cerror) Unwrap() error {
74+
return c.next
75+
}

error_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package cerrors_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/1debit/cerrors"
13+
)
14+
15+
func TestUnwrapMessage(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
err error
19+
result string
20+
}{
21+
{
22+
name: "at the end with a colon",
23+
err: fmt.Errorf("one: %w", fmt.Errorf("two: %w", errors.New("three"))),
24+
result: "one",
25+
},
26+
{
27+
name: "at the end without colon",
28+
err: fmt.Errorf("one %w", fmt.Errorf("two: %w", errors.New("three"))),
29+
result: "one",
30+
},
31+
{
32+
name: "in the middle",
33+
err: fmt.Errorf("foo %w bar", fmt.Errorf("two: %w", errors.New("three"))),
34+
result: "foo * bar",
35+
},
36+
{
37+
name: "chained without wrapping",
38+
err: fmt.Errorf("one: %v", fmt.Errorf("two: %w", errors.New("three"))),
39+
result: "one: two: three",
40+
},
41+
{
42+
name: "not chained",
43+
err: errors.New("foo bar"),
44+
result: "foo bar",
45+
},
46+
{
47+
name: "empty",
48+
err: errors.New(""),
49+
result: "",
50+
},
51+
}
52+
53+
for _, test := range tests {
54+
t.Run(test.name, func(t *testing.T) {
55+
assert.Equal(t, test.result, cerrors.UnwrapMessage(test.err))
56+
})
57+
}
58+
}
59+
60+
func TestCerrorFormat(t *testing.T) {
61+
var (
62+
location = ` \S*error_test.go:\d+`
63+
locationIndented = ` \S*error_test.go:\d+`
64+
function = ` github.com/1debit/cerrors_test.TestCerrorFormat.*`
65+
)
66+
67+
tests := []struct {
68+
name string
69+
chain []func(error) error
70+
verb string
71+
root error
72+
result []string
73+
}{
74+
{
75+
name: "single error, short",
76+
root: cerrors.New("foo"),
77+
verb: "%v",
78+
result: []string{"foo"},
79+
},
80+
{
81+
name: "multiple levels wrapped, short",
82+
root: cerrors.New("one"),
83+
chain: []func(error) error{
84+
func(e error) error { return cerrors.Wrap(e, "two") },
85+
func(e error) error { return cerrors.Wrap(e, "three") },
86+
},
87+
verb: "%v",
88+
result: []string{"three: two: one"},
89+
},
90+
{
91+
name: "including non-cerror, short",
92+
root: cerrors.New("one"),
93+
chain: []func(error) error{
94+
func(e error) error { return fmt.Errorf("two: %w", e) },
95+
func(e error) error { return cerrors.Wrap(e, "three") },
96+
},
97+
verb: "%v",
98+
result: []string{"three: two: one"},
99+
},
100+
{
101+
name: "single error, detailed",
102+
root: cerrors.New("foo"),
103+
verb: "%+v",
104+
result: []string{
105+
"foo",
106+
location,
107+
},
108+
},
109+
{
110+
name: "multiple levels wrapped, detailed",
111+
root: cerrors.New("one"),
112+
chain: []func(error) error{
113+
func(e error) error { return cerrors.Wrap(e, "two") },
114+
func(e error) error { return cerrors.Wrap(e, "three") },
115+
},
116+
verb: "%+v",
117+
result: []string{
118+
"three",
119+
location,
120+
" - two",
121+
location,
122+
" - one",
123+
location,
124+
},
125+
},
126+
{
127+
name: "including non-cerror, detailed",
128+
root: cerrors.New("one"),
129+
chain: []func(error) error{
130+
func(e error) error { return fmt.Errorf("two: %w", e) },
131+
func(e error) error { return cerrors.Wrap(e, "three") },
132+
},
133+
verb: "%+v",
134+
result: []string{
135+
"three",
136+
location,
137+
" - two",
138+
" - one",
139+
location,
140+
},
141+
},
142+
{
143+
name: "including non-cerror, detailed with functions",
144+
root: cerrors.New("one"),
145+
chain: []func(error) error{
146+
func(e error) error { return fmt.Errorf("two: %w", e) },
147+
func(e error) error { return cerrors.Wrap(e, "three") },
148+
},
149+
verb: "%#v",
150+
result: []string{
151+
"three",
152+
function,
153+
locationIndented,
154+
" - two",
155+
" - one",
156+
function,
157+
locationIndented,
158+
},
159+
},
160+
{
161+
name: "invalid verb",
162+
root: cerrors.New("one"),
163+
verb: "%d",
164+
result: []string{`%!d\(cerror\)`},
165+
},
166+
}
167+
168+
for _, test := range tests {
169+
t.Run(test.name, func(t *testing.T) {
170+
err := test.root
171+
for _, f := range test.chain {
172+
err = f(err)
173+
}
174+
175+
result := strings.Split(strings.TrimSpace(fmt.Sprintf(test.verb, err)), "\n")
176+
require.Len(t, result, len(test.result))
177+
for idx, line := range test.result {
178+
assert.Regexp(t, "^"+line+"$", result[idx], "at line %d", idx)
179+
}
180+
})
181+
}
182+
}

format.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cerrors
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
)
8+
9+
type withOwnMessage interface {
10+
OwnMessage() string
11+
}
12+
13+
type withFrame interface {
14+
Frame() Frame
15+
}
16+
17+
type withUnwrap interface {
18+
Unwrap() error
19+
}
20+
21+
func Format(err error, s io.Writer, withFunctions bool) {
22+
var prefix string
23+
24+
for err != nil {
25+
next := getNext(err)
26+
fmt.Fprintln(s, prefix+getOwnMessage(err, next))
27+
28+
if wFrame, ok := err.(withFrame); ok {
29+
wFrame.Frame().Format(s, withFunctions)
30+
}
31+
32+
prefix = " - "
33+
err = next
34+
}
35+
}
36+
37+
func getOwnMessage(err, next error) string {
38+
if wMessage, ok := err.(withOwnMessage); ok {
39+
return wMessage.OwnMessage()
40+
} else if next != nil {
41+
return unwrapMessageStr(err.Error(), next.Error())
42+
} else {
43+
return err.Error()
44+
}
45+
}
46+
47+
func getNext(err error) error {
48+
if wUnwrap, ok := err.(withUnwrap); ok {
49+
return wUnwrap.Unwrap()
50+
}
51+
return nil
52+
}
53+
54+
// UnwrapMessage returns the portion of the error message which does not repeat
55+
// in the wrapped error.
56+
//
57+
// Examples:
58+
//
59+
// err := errors.New("couldn't")
60+
//
61+
// UnwrapMessage(fmt.Errorf("doing foo: %w", err)) // => "doing foo"
62+
// UnwrapMessage(fmt.Errorf("doing foo %w", err)) // => "doing foo"
63+
// UnwrapMessage(fmt.Errorf("failed (%w) while doing foo", err))) // => "failed (*) while doing foo"
64+
func UnwrapMessage(err error) string {
65+
msg := err.Error()
66+
if wrapper, ok := err.(withUnwrap); ok {
67+
wrapped := wrapper.Unwrap()
68+
if wrapped != nil {
69+
if withOwnMessage, ok := wrapped.(withOwnMessage); ok {
70+
return withOwnMessage.OwnMessage()
71+
}
72+
wrappedMsg := wrapped.Error()
73+
return unwrapMessageStr(msg, wrappedMsg)
74+
}
75+
}
76+
77+
return msg
78+
}
79+
80+
func unwrapMessageStr(parent, child string) string {
81+
msg := parent
82+
if idx := strings.Index(parent, child); idx > 0 {
83+
if idx+len(child) == len(parent) {
84+
msg = strings.TrimRight(parent[:idx], ": ")
85+
} else {
86+
msg = parent[:idx] + "*" + parent[idx+len(child):]
87+
}
88+
}
89+
return msg
90+
}

0 commit comments

Comments
 (0)