Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented support for checkable errors #131

Merged
merged 9 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions codec.go
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package uuid

// Error is a custom error type for UUID-related errors
type Error string
PatrLind marked this conversation as resolved.
Show resolved Hide resolved

// 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
Original file line number Diff line number Diff line change
@@ -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) {
dylan-bourque marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
Loading