Skip to content

Commit

Permalink
Implemented support for checkable errors (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrLind committed May 12, 2024
1 parent fa71420 commit 4a2de11
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 25 deletions.
33 changes: 14 additions & 19 deletions codec.go
Expand Up @@ -21,10 +21,7 @@

package uuid

import (
"errors"
"fmt"
)
import "fmt"

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

var errInvalidFormat = errors.New("uuid: invalid UUID format")

func fromHexChar(c byte) byte {
switch {
case '0' <= c && c <= '9':
Expand All @@ -66,21 +61,21 @@ func (u *UUID) Parse(s string) error {
case 36: // canonical
case 34, 38:
if s[0] != '{' || s[len(s)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s)
}
s = s[1 : len(s)-1]
case 41, 45:
if s[:9] != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s[:9])
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s[:9])
}
s = s[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(s), s)
return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(s), s)
}
// canonical
if len(s) == 36 {
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", s)
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s)
}
for i, x := range [16]byte{
0, 2, 4, 6,
Expand All @@ -92,7 +87,7 @@ func (u *UUID) Parse(s string) error {
v1 := fromHexChar(s[x])
v2 := fromHexChar(s[x+1])
if v1|v2 == 255 {
return errInvalidFormat
return ErrInvalidFormat
}
u[i] = (v1 << 4) | v2
}
Expand All @@ -103,7 +98,7 @@ func (u *UUID) Parse(s string) error {
v1 := fromHexChar(s[i])
v2 := fromHexChar(s[i+1])
if v1|v2 == 255 {
return errInvalidFormat
return ErrInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
Expand Down Expand Up @@ -175,20 +170,20 @@ func (u *UUID) UnmarshalText(b []byte) error {
case 36: // canonical
case 34, 38:
if b[0] != '{' || b[len(b)-1] != '}' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b)
}
b = b[1 : len(b)-1]
case 41, 45:
if string(b[:9]) != "urn:uuid:" {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b[:9])
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b[:9])
}
b = b[9:]
default:
return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(b), b)
return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(b), b)
}
if len(b) == 36 {
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return fmt.Errorf("uuid: incorrect UUID format in string %q", b)
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b)
}
for i, x := range [16]byte{
0, 2, 4, 6,
Expand All @@ -200,7 +195,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
v1 := fromHexChar(b[x])
v2 := fromHexChar(b[x+1])
if v1|v2 == 255 {
return errInvalidFormat
return ErrInvalidFormat
}
u[i] = (v1 << 4) | v2
}
Expand All @@ -210,7 +205,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
v1 := fromHexChar(b[i])
v2 := fromHexChar(b[i+1])
if v1|v2 == 255 {
return errInvalidFormat
return ErrInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
Expand All @@ -226,7 +221,7 @@ func (u UUID) MarshalBinary() ([]byte, error) {
// It will return an error if the slice isn't 16 bytes long.
func (u *UUID) UnmarshalBinary(data []byte) error {
if len(data) != Size {
return fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data))
return fmt.Errorf("%w, got %d bytes", ErrIncorrectByteLength, len(data))
}
copy(u[:], data)

Expand Down
40 changes: 40 additions & 0 deletions error.go
@@ -0,0 +1,40 @@
package uuid

// Error is a custom error type for UUID-related errors
type Error string

// The strings defined in the errors is matching the previous behavior before
// the custom error type was implemented. The reason is that some people might
// be relying on the exact string representation to handle errors in their code.
const (
// ErrInvalidFormat is returned when the UUID string representation does not
// match the expected format. See also ErrIncorrectFormatInString.
ErrInvalidFormat = Error("uuid: invalid UUID format")

// ErrIncorrectFormatInString can be returned instead of ErrInvalidFormat.
// A separate error type is used because of how errors used to be formatted
// before custom error types were introduced.
ErrIncorrectFormatInString = Error("uuid: incorrect UUID format in string")

// ErrIncorrectLength is returned when the UUID does not have the
// appropriate string length for parsing the UUID.
ErrIncorrectLength = Error("uuid: incorrect UUID length")

// ErrIncorrectByteLength indicates the UUID byte slice length is invalid.
ErrIncorrectByteLength = Error("uuid: UUID must be exactly 16 bytes long")

// ErrNoHwAddressFound is returned when a hardware (MAC) address cannot be
// found for UUID generation.
ErrNoHwAddressFound = Error("uuid: no HW address found")

// ErrTypeConvertError is returned for type conversion operation fails.
ErrTypeConvertError = Error("uuid: cannot convert")

// ErrInvalidVersion indicates an unsupported or invalid UUID version.
ErrInvalidVersion = Error("uuid:")
)

// Error returns the string representation of the UUID error.
func (e Error) Error() string {
return string(e)
}
201 changes: 201 additions & 0 deletions error_test.go
@@ -0,0 +1,201 @@
package uuid

import (
"errors"
"fmt"
"net"
"testing"
)

func TestIsAsError(t *testing.T) {
tcs := []struct {
err error
expected string
expectedErr error
}{
{
err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123),
expected: "uuid: sample error: 123",
expectedErr: ErrInvalidVersion,
},
{
err: fmt.Errorf("%w", ErrInvalidFormat),
expected: "uuid: invalid UUID format",
expectedErr: ErrInvalidFormat,
},
{
err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"),
expected: "uuid: incorrect UUID format in string \"test\"",
expectedErr: ErrIncorrectFormatInString,
},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
if tc.err.Error() != tc.expected {
t.Errorf("expected err.Error() to be '%s' but was '%s'", tc.expected, tc.err.Error())
}
var uuidErr Error
if !errors.As(tc.err, &uuidErr) {
t.Error("expected errors.As() to work")
}
if !errors.Is(tc.err, tc.expectedErr) {
t.Errorf("expected error to be, or wrap, the %v sentinel error", tc.expectedErr)
}
})
}
}

func TestParseErrors(t *testing.T) {
tcs := []struct {
function string
uuidStr string
expected string
}{
{ // 34 chars - With brackets
function: "parse",
uuidStr: "..................................",
expected: "uuid: incorrect UUID format in string \"..................................\"",
},
{ // 41 chars - urn:uuid:
function: "parse",
uuidStr: "123456789................................",
expected: "uuid: incorrect UUID format in string \"123456789\"",
},
{ // other
function: "parse",
uuidStr: "....",
expected: "uuid: incorrect UUID length 4 in string \"....\"",
},
{ // 36 chars - canonical, but not correct format
function: "parse",
uuidStr: "....................................",
expected: "uuid: incorrect UUID format in string \"....................................\"",
},
{ // 36 chars - canonical, invalid data
function: "parse",
uuidStr: "xx00ae9e-dae3-459f-ad0e-6b574be3f950",
expected: "uuid: invalid UUID format",
},
{ // Hash like
function: "parse",
uuidStr: "................................",
expected: "uuid: invalid UUID format",
},
{ // Hash like, invalid
function: "parse",
uuidStr: "xx00ae9edae3459fad0e6b574be3f950",
expected: "uuid: invalid UUID format",
},
{ // Hash like, invalid
function: "parse",
uuidStr: "xx00ae9edae3459fad0e6b574be3f950",
expected: "uuid: invalid UUID format",
},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
id := UUID{}
err := id.Parse(tc.uuidStr)
if err == nil {
t.Error("expected an error")
return
}
if err.Error() != tc.expected {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), tc.expected)
}
err = id.UnmarshalText([]byte(tc.uuidStr))
if err == nil {
t.Error("expected an error")
return
}
if err.Error() != tc.expected {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), tc.expected)
}
})
}
}

func TestUnmarshalBinaryError(t *testing.T) {
id := UUID{}
b := make([]byte, 33)
expectedErr := "uuid: UUID must be exactly 16 bytes long, got 33 bytes"
err := id.UnmarshalBinary([]byte(b))
if err == nil {
t.Error("expected an error")
return
}
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}
}

func TestScanError(t *testing.T) {
id := UUID{}
err := id.Scan(123)
if err == nil {
t.Error("expected an error")
return
}
expectedErr := "uuid: cannot convert int to UUID"
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}
}

func TestUUIDVersionErrors(t *testing.T) {
// UUId V1 Version
id := FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
_, err := TimestampFromV1(id)
if err == nil {
t.Error("expected an error")
return
}
expectedErr := "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 1"
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}

// UUId V2 Version
id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
_, err = TimestampFromV6(id)
if err == nil {
t.Error("expected an error")
return
}
expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 6"
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}

// UUId V7 Version
id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e")
_, err = TimestampFromV7(id)
if err == nil {
t.Error("expected an error")
return
}
expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 7"
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}
}

// This test cannot be run in parallel with other tests since it modifies the
// global state
func TestErrNoHwAddressFound(t *testing.T) {
netInterfaces = func() ([]net.Interface, error) {
return nil, nil
}
defer func() {
netInterfaces = net.Interfaces
}()
_, err := defaultHWAddrFunc()
if err == nil {
t.Error("expected an error")
return
}
expectedErr := "uuid: no HW address found"
if err.Error() != expectedErr {
t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr)
}
}
3 changes: 1 addition & 2 deletions generator.go
Expand Up @@ -26,7 +26,6 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"fmt"
"hash"
"io"
"net"
Expand Down Expand Up @@ -446,5 +445,5 @@ func defaultHWAddrFunc() (net.HardwareAddr, error) {
return iface.HardwareAddr, nil
}
}
return []byte{}, fmt.Errorf("uuid: no HW address found")
return []byte{}, ErrNoHwAddressFound
}
2 changes: 1 addition & 1 deletion sql.go
Expand Up @@ -56,7 +56,7 @@ func (u *UUID) Scan(src interface{}) error {
return err
}

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

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

0 comments on commit 4a2de11

Please sign in to comment.