diff --git a/codec.go b/codec.go index 6650264..8087955 100644 --- a/codec.go +++ b/codec.go @@ -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. @@ -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': @@ -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, @@ -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 } @@ -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 } @@ -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, @@ -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 } @@ -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 } @@ -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) diff --git a/error.go b/error.go new file mode 100644 index 0000000..7ca0be4 --- /dev/null +++ b/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) +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..7d58e74 --- /dev/null +++ b/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) + } +} diff --git a/generator.go b/generator.go index 461322a..7bfe50c 100644 --- a/generator.go +++ b/generator.go @@ -26,7 +26,6 @@ import ( "crypto/rand" "crypto/sha1" "encoding/binary" - "fmt" "hash" "io" "net" @@ -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 } diff --git a/sql.go b/sql.go index 01d5d88..cdc7518 100644 --- a/sql.go +++ b/sql.go @@ -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 diff --git a/uuid.go b/uuid.go index 8ad7642..e6fc3fc 100644 --- a/uuid.go +++ b/uuid.go @@ -100,7 +100,7 @@ func (t Timestamp) Time() (time.Time, error) { // Returns an error if the UUID is any version other than 1. func TimestampFromV1(u UUID) (Timestamp, error) { if u.Version() != 1 { - err := fmt.Errorf("uuid: %s is version %d, not version 1", u, u.Version()) + err := fmt.Errorf("%w %s is version %d, not version 1", ErrInvalidVersion, u, u.Version()) return 0, err } @@ -115,7 +115,7 @@ func TimestampFromV1(u UUID) (Timestamp, error) { // function returns an error if the UUID is any version other than 6. func TimestampFromV6(u UUID) (Timestamp, error) { if u.Version() != 6 { - return 0, fmt.Errorf("uuid: %s is version %d, not version 6", u, u.Version()) + return 0, fmt.Errorf("%w %s is version %d, not version 6", ErrInvalidVersion, u, u.Version()) } hi := binary.BigEndian.Uint32(u[0:4]) @@ -129,7 +129,7 @@ func TimestampFromV6(u UUID) (Timestamp, error) { // function returns an error if the UUID is any version other than 7. func TimestampFromV7(u UUID) (Timestamp, error) { if u.Version() != 7 { - return 0, fmt.Errorf("uuid: %s is version %d, not version 7", u, u.Version()) + return 0, fmt.Errorf("%w %s is version %d, not version 7", ErrInvalidVersion, u, u.Version()) } t := 0 |