Skip to content

Commit 4a2de11

Browse files
authored
Implemented support for checkable errors (#131)
1 parent fa71420 commit 4a2de11

File tree

6 files changed

+260
-25
lines changed

6 files changed

+260
-25
lines changed

codec.go

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@
2121

2222
package uuid
2323

24-
import (
25-
"errors"
26-
"fmt"
27-
)
24+
import "fmt"
2825

2926
// FromBytes returns a UUID generated from the raw byte slice input.
3027
// It will return an error if the slice isn't 16 bytes long.
@@ -44,8 +41,6 @@ func FromBytesOrNil(input []byte) UUID {
4441
return uuid
4542
}
4643

47-
var errInvalidFormat = errors.New("uuid: invalid UUID format")
48-
4944
func fromHexChar(c byte) byte {
5045
switch {
5146
case '0' <= c && c <= '9':
@@ -66,21 +61,21 @@ func (u *UUID) Parse(s string) error {
6661
case 36: // canonical
6762
case 34, 38:
6863
if s[0] != '{' || s[len(s)-1] != '}' {
69-
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
64+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s)
7065
}
7166
s = s[1 : len(s)-1]
7267
case 41, 45:
7368
if s[:9] != "urn:uuid:" {
74-
return fmt.Errorf("uuid: incorrect UUID format in string %q", s[:9])
69+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s[:9])
7570
}
7671
s = s[9:]
7772
default:
78-
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(s), s)
73+
return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(s), s)
7974
}
8075
// canonical
8176
if len(s) == 36 {
8277
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
83-
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
78+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s)
8479
}
8580
for i, x := range [16]byte{
8681
0, 2, 4, 6,
@@ -92,7 +87,7 @@ func (u *UUID) Parse(s string) error {
9287
v1 := fromHexChar(s[x])
9388
v2 := fromHexChar(s[x+1])
9489
if v1|v2 == 255 {
95-
return errInvalidFormat
90+
return ErrInvalidFormat
9691
}
9792
u[i] = (v1 << 4) | v2
9893
}
@@ -103,7 +98,7 @@ func (u *UUID) Parse(s string) error {
10398
v1 := fromHexChar(s[i])
10499
v2 := fromHexChar(s[i+1])
105100
if v1|v2 == 255 {
106-
return errInvalidFormat
101+
return ErrInvalidFormat
107102
}
108103
u[i/2] = (v1 << 4) | v2
109104
}
@@ -175,20 +170,20 @@ func (u *UUID) UnmarshalText(b []byte) error {
175170
case 36: // canonical
176171
case 34, 38:
177172
if b[0] != '{' || b[len(b)-1] != '}' {
178-
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
173+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b)
179174
}
180175
b = b[1 : len(b)-1]
181176
case 41, 45:
182177
if string(b[:9]) != "urn:uuid:" {
183-
return fmt.Errorf("uuid: incorrect UUID format in string %q", b[:9])
178+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b[:9])
184179
}
185180
b = b[9:]
186181
default:
187-
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(b), b)
182+
return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(b), b)
188183
}
189184
if len(b) == 36 {
190185
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
191-
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
186+
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b)
192187
}
193188
for i, x := range [16]byte{
194189
0, 2, 4, 6,
@@ -200,7 +195,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
200195
v1 := fromHexChar(b[x])
201196
v2 := fromHexChar(b[x+1])
202197
if v1|v2 == 255 {
203-
return errInvalidFormat
198+
return ErrInvalidFormat
204199
}
205200
u[i] = (v1 << 4) | v2
206201
}
@@ -210,7 +205,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
210205
v1 := fromHexChar(b[i])
211206
v2 := fromHexChar(b[i+1])
212207
if v1|v2 == 255 {
213-
return errInvalidFormat
208+
return ErrInvalidFormat
214209
}
215210
u[i/2] = (v1 << 4) | v2
216211
}
@@ -226,7 +221,7 @@ func (u UUID) MarshalBinary() ([]byte, error) {
226221
// It will return an error if the slice isn't 16 bytes long.
227222
func (u *UUID) UnmarshalBinary(data []byte) error {
228223
if len(data) != Size {
229-
return fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data))
224+
return fmt.Errorf("%w, got %d bytes", ErrIncorrectByteLength, len(data))
230225
}
231226
copy(u[:], data)
232227

error.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package uuid
2+
3+
// Error is a custom error type for UUID-related errors
4+
type Error string
5+
6+
// The strings defined in the errors is matching the previous behavior before
7+
// the custom error type was implemented. The reason is that some people might
8+
// be relying on the exact string representation to handle errors in their code.
9+
const (
10+
// ErrInvalidFormat is returned when the UUID string representation does not
11+
// match the expected format. See also ErrIncorrectFormatInString.
12+
ErrInvalidFormat = Error("uuid: invalid UUID format")
13+
14+
// ErrIncorrectFormatInString can be returned instead of ErrInvalidFormat.
15+
// A separate error type is used because of how errors used to be formatted
16+
// before custom error types were introduced.
17+
ErrIncorrectFormatInString = Error("uuid: incorrect UUID format in string")
18+
19+
// ErrIncorrectLength is returned when the UUID does not have the
20+
// appropriate string length for parsing the UUID.
21+
ErrIncorrectLength = Error("uuid: incorrect UUID length")
22+
23+
// ErrIncorrectByteLength indicates the UUID byte slice length is invalid.
24+
ErrIncorrectByteLength = Error("uuid: UUID must be exactly 16 bytes long")
25+
26+
// ErrNoHwAddressFound is returned when a hardware (MAC) address cannot be
27+
// found for UUID generation.
28+
ErrNoHwAddressFound = Error("uuid: no HW address found")
29+
30+
// ErrTypeConvertError is returned for type conversion operation fails.
31+
ErrTypeConvertError = Error("uuid: cannot convert")
32+
33+
// ErrInvalidVersion indicates an unsupported or invalid UUID version.
34+
ErrInvalidVersion = Error("uuid:")
35+
)
36+
37+
// Error returns the string representation of the UUID error.
38+
func (e Error) Error() string {
39+
return string(e)
40+
}

error_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package uuid
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
"testing"
8+
)
9+
10+
func TestIsAsError(t *testing.T) {
11+
tcs := []struct {
12+
err error
13+
expected string
14+
expectedErr error
15+
}{
16+
{
17+
err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123),
18+
expected: "uuid: sample error: 123",
19+
expectedErr: ErrInvalidVersion,
20+
},
21+
{
22+
err: fmt.Errorf("%w", ErrInvalidFormat),
23+
expected: "uuid: invalid UUID format",
24+
expectedErr: ErrInvalidFormat,
25+
},
26+
{
27+
err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"),
28+
expected: "uuid: incorrect UUID format in string \"test\"",
29+
expectedErr: ErrIncorrectFormatInString,
30+
},
31+
}
32+
for i, tc := range tcs {
33+
t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
34+
if tc.err.Error() != tc.expected {
35+
t.Errorf("expected err.Error() to be '%s' but was '%s'", tc.expected, tc.err.Error())
36+
}
37+
var uuidErr Error
38+
if !errors.As(tc.err, &uuidErr) {
39+
t.Error("expected errors.As() to work")
40+
}
41+
if !errors.Is(tc.err, tc.expectedErr) {
42+
t.Errorf("expected error to be, or wrap, the %v sentinel error", tc.expectedErr)
43+
}
44+
})
45+
}
46+
}
47+
48+
func TestParseErrors(t *testing.T) {
49+
tcs := []struct {
50+
function string
51+
uuidStr string
52+
expected string
53+
}{
54+
{ // 34 chars - With brackets
55+
function: "parse",
56+
uuidStr: "..................................",
57+
expected: "uuid: incorrect UUID format in string \"..................................\"",
58+
},
59+
{ // 41 chars - urn:uuid:
60+
function: "parse",
61+
uuidStr: "123456789................................",
62+
expected: "uuid: incorrect UUID format in string \"123456789\"",
63+
},
64+
{ // other
65+
function: "parse",
66+
uuidStr: "....",
67+
expected: "uuid: incorrect UUID length 4 in string \"....\"",
68+
},
69+
{ // 36 chars - canonical, but not correct format
70+
function: "parse",
71+
uuidStr: "....................................",
72+
expected: "uuid: incorrect UUID format in string \"....................................\"",
73+
},
74+
{ // 36 chars - canonical, invalid data
75+
function: "parse",
76+
uuidStr: "xx00ae9e-dae3-459f-ad0e-6b574be3f950",
77+
expected: "uuid: invalid UUID format",
78+
},
79+
{ // Hash like
80+
function: "parse",
81+
uuidStr: "................................",
82+
expected: "uuid: invalid UUID format",
83+
},
84+
{ // Hash like, invalid
85+
function: "parse",
86+
uuidStr: "xx00ae9edae3459fad0e6b574be3f950",
87+
expected: "uuid: invalid UUID format",
88+
},
89+
{ // Hash like, invalid
90+
function: "parse",
91+
uuidStr: "xx00ae9edae3459fad0e6b574be3f950",
92+
expected: "uuid: invalid UUID format",
93+
},
94+
}
95+
for i, tc := range tcs {
96+
t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
97+
id := UUID{}
98+
err := id.Parse(tc.uuidStr)
99+
if err == nil {
100+
t.Error("expected an error")
101+
return
102+
}
103+
if err.Error() != tc.expected {
104+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), tc.expected)
105+
}
106+
err = id.UnmarshalText([]byte(tc.uuidStr))
107+
if err == nil {
108+
t.Error("expected an error")
109+
return
110+
}
111+
if err.Error() != tc.expected {
112+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), tc.expected)
113+
}
114+
})
115+
}
116+
}
117+
118+
func TestUnmarshalBinaryError(t *testing.T) {
119+
id := UUID{}
120+
b := make([]byte, 33)
121+
expectedErr := "uuid: UUID must be exactly 16 bytes long, got 33 bytes"
122+
err := id.UnmarshalBinary([]byte(b))
123+
if err == nil {
124+
t.Error("expected an error")
125+
return
126+
}
127+
if err.Error() != expectedErr {
128+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
129+
}
130+
}
131+
132+
func TestScanError(t *testing.T) {
133+
id := UUID{}
134+
err := id.Scan(123)
135+
if err == nil {
136+
t.Error("expected an error")
137+
return
138+
}
139+
expectedErr := "uuid: cannot convert int to UUID"
140+
if err.Error() != expectedErr {
141+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
142+
}
143+
}
144+
145+
func TestUUIDVersionErrors(t *testing.T) {
146+
// UUId V1 Version
147+
id := FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
148+
_, err := TimestampFromV1(id)
149+
if err == nil {
150+
t.Error("expected an error")
151+
return
152+
}
153+
expectedErr := "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 1"
154+
if err.Error() != expectedErr {
155+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
156+
}
157+
158+
// UUId V2 Version
159+
id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
160+
_, err = TimestampFromV6(id)
161+
if err == nil {
162+
t.Error("expected an error")
163+
return
164+
}
165+
expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 6"
166+
if err.Error() != expectedErr {
167+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
168+
}
169+
170+
// UUId V7 Version
171+
id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
172+
_, err = TimestampFromV7(id)
173+
if err == nil {
174+
t.Error("expected an error")
175+
return
176+
}
177+
expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 7"
178+
if err.Error() != expectedErr {
179+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
180+
}
181+
}
182+
183+
// This test cannot be run in parallel with other tests since it modifies the
184+
// global state
185+
func TestErrNoHwAddressFound(t *testing.T) {
186+
netInterfaces = func() ([]net.Interface, error) {
187+
return nil, nil
188+
}
189+
defer func() {
190+
netInterfaces = net.Interfaces
191+
}()
192+
_, err := defaultHWAddrFunc()
193+
if err == nil {
194+
t.Error("expected an error")
195+
return
196+
}
197+
expectedErr := "uuid: no HW address found"
198+
if err.Error() != expectedErr {
199+
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
200+
}
201+
}

generator.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"crypto/rand"
2727
"crypto/sha1"
2828
"encoding/binary"
29-
"fmt"
3029
"hash"
3130
"io"
3231
"net"
@@ -446,5 +445,5 @@ func defaultHWAddrFunc() (net.HardwareAddr, error) {
446445
return iface.HardwareAddr, nil
447446
}
448447
}
449-
return []byte{}, fmt.Errorf("uuid: no HW address found")
448+
return []byte{}, ErrNoHwAddressFound
450449
}

sql.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (u *UUID) Scan(src interface{}) error {
5656
return err
5757
}
5858

59-
return fmt.Errorf("uuid: cannot convert %T to UUID", src)
59+
return fmt.Errorf("%w %T to UUID", ErrTypeConvertError, src)
6060
}
6161

6262
// NullUUID can be used with the standard sql package to represent a

0 commit comments

Comments
 (0)