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 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 15 additions & 13 deletions codec.go
Expand Up @@ -21,6 +21,8 @@

package uuid

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.
func FromBytes(input []byte) (UUID, error) {
Expand Down Expand Up @@ -59,21 +61,21 @@ func (u *UUID) Parse(s string) error {
case 36: // canonical
case 34, 38:
if s[0] != '{' || s[len(s)-1] != '}' {
return invalidFormatf("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 invalidFormatf("incorrect UUID format in string %q", s[:9])
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s[:9])
}
s = s[9:]
default:
return invalidFormatf("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 invalidFormatf("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 @@ -85,7 +87,7 @@ func (u *UUID) Parse(s string) error {
v1 := fromHexChar(s[x])
v2 := fromHexChar(s[x+1])
if v1|v2 == 255 {
return invalidFormat()
return ErrInvalidFormat
}
u[i] = (v1 << 4) | v2
}
Expand All @@ -96,7 +98,7 @@ func (u *UUID) Parse(s string) error {
v1 := fromHexChar(s[i])
v2 := fromHexChar(s[i+1])
if v1|v2 == 255 {
return invalidFormat()
return ErrInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
Expand Down Expand Up @@ -168,20 +170,20 @@ func (u *UUID) UnmarshalText(b []byte) error {
case 36: // canonical
case 34, 38:
if b[0] != '{' || b[len(b)-1] != '}' {
return invalidFormatf("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 invalidFormatf("incorrect UUID format in string %q", b[:9])
return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b[:9])
}
b = b[9:]
default:
return invalidFormatf("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 invalidFormatf("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 @@ -193,7 +195,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
v1 := fromHexChar(b[x])
v2 := fromHexChar(b[x+1])
if v1|v2 == 255 {
return invalidFormat()
return ErrInvalidFormat
}
u[i] = (v1 << 4) | v2
}
Expand All @@ -203,7 +205,7 @@ func (u *UUID) UnmarshalText(b []byte) error {
v1 := fromHexChar(b[i])
v2 := fromHexChar(b[i+1])
if v1|v2 == 255 {
return invalidFormat()
return ErrInvalidFormat
}
u[i/2] = (v1 << 4) | v2
}
Expand All @@ -219,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 invalidFormatf("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
51 changes: 14 additions & 37 deletions error.go
@@ -1,45 +1,22 @@
package uuid

import (
"fmt"
type Error string
PatrLind marked this conversation as resolved.
Show resolved Hide resolved

const (
ErrInvalidFormat = Error("uuid: invalid UUID format")
ErrIncorrectFormatInString = Error("uuid: incorrect UUID format in string")
ErrIncorrectLength = Error("uuid: incorrect UUID length")
ErrIncorrectByteLength = Error("uuid: UUID must be exactly 16 bytes long")
ErrNoHwAddressFound = Error("uuid: no HW address found")
ErrTypeConvertError = Error("uuid: cannot convert")
ErrInvalidVersion = Error("uuid:")
)

// ErrUUID is the base error type for errors generated by this package
type ErrUUID struct{}

func (e *ErrUUID) Error() string {
return "uuid"
}

// ErrUUIDInvalidFormat is a specialized error that is returned when there is
// a problem parsing UUID data
type ErrUUIDInvalidFormat struct {
err error
}

func (e *ErrUUIDInvalidFormat) Error() string {
return e.err.Error()
}

func (e *ErrUUIDInvalidFormat) Unwrap() error {
return e.err
func (e Error) Error() string {
return string(e)
}

func (e *ErrUUIDInvalidFormat) Is(target error) bool {
_, ok := target.(*ErrUUIDInvalidFormat)
func (e Error) Is(target error) bool {
PatrLind marked this conversation as resolved.
Show resolved Hide resolved
_, ok := target.(*Error)
return ok
}

func invalidFormat() error {
return &ErrUUIDInvalidFormat{
err: fmt.Errorf("%w: invalid UUID format", &ErrUUID{}),
}
}

func invalidFormatf(format string, a ...any) error {
formattedError := fmt.Errorf(format, a...)

return &ErrUUIDInvalidFormat{
err: fmt.Errorf("%w: %v", &ErrUUID{}, formattedError),
}
}
174 changes: 160 additions & 14 deletions error_test.go
Expand Up @@ -3,40 +3,38 @@ package uuid
import (
"errors"
"fmt"
"net"
"reflect"
"testing"
)

func TestError(t *testing.T) {
var e Error
tcs := []struct {
err error
expected string
expectedTarget error
}{
{
err: fmt.Errorf("%w: sample error: %v", &ErrUUID{}, 123),
err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123),
expected: "uuid: sample error: 123",
expectedTarget: &ErrUUID{},
expectedTarget: &e,
},
{
err: invalidFormat(),
err: fmt.Errorf("%w", ErrInvalidFormat),
expected: "uuid: invalid UUID format",
expectedTarget: &ErrUUIDInvalidFormat{},
expectedTarget: ErrInvalidFormat,
},
{
err: invalidFormatf("sample error: %v", 123),
expected: "uuid: sample error: 123",
expectedTarget: &ErrUUIDInvalidFormat{},
},
{
err: fmt.Errorf("uuid error: %w", invalidFormatf("sample error: %v", 123)),
expected: "uuid error: uuid: sample error: 123",
expectedTarget: &ErrUUIDInvalidFormat{},
err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"),
expected: "uuid: incorrect UUID format in string \"test\"",
expectedTarget: ErrIncorrectFormatInString,
},
}
for i, tc := range tcs {
t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
if !errors.Is(tc.err, &ErrUUID{}) {
var e2 Error
if !errors.Is(tc.err, &e2) {
t.Error("expected error to be of a wrapped type of Error")
}
if !errors.Is(tc.err, tc.expectedTarget) {
Expand All @@ -45,10 +43,158 @@ func TestError(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())
}
uuidErr := &ErrUUID{}
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")
}
})
}
}

func TestAllErrorMessages(t *testing.T) {
PatrLind marked this conversation as resolved.
Show resolved Hide resolved
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)
}
})
}

// Unmarshal binary
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)
}

// no hw address error
netInterfaces = func() ([]net.Interface, error) {
PatrLind marked this conversation as resolved.
Show resolved Hide resolved
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)
}

// scan error
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)
}

// 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
}
// There is a "bug" in the error message, this should probably be fixed (6 -> 7)
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)
}
}
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 @@ -452,5 +451,5 @@ func defaultHWAddrFunc() (net.HardwareAddr, error) {
return iface.HardwareAddr, nil
}
}
return []byte{}, fmt.Errorf("%w: no HW address found", &ErrUUID{})
return []byte{}, ErrNoHwAddressFound
}