From a3a30810802e1a41a9b1c5f61cfc7e97c1c82837 Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Wed, 27 Mar 2024 16:12:50 +0100 Subject: [PATCH 1/7] Implemented ErrUUID and ErrUUIDInvalidFormat error types --- codec.go | 33 +++++++++++++------------------ error.go | 45 ++++++++++++++++++++++++++++++++++++++++++ error_test.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ generator.go | 2 +- sql.go | 2 +- uuid.go | 6 +++--- 6 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 error.go create mode 100644 error_test.go diff --git a/codec.go b/codec.go index 6650264..c35b9d2 100644 --- a/codec.go +++ b/codec.go @@ -21,11 +21,6 @@ package uuid -import ( - "errors" - "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) { @@ -44,8 +39,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 +59,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 invalidFormatf("incorrect UUID format in string %q", 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 invalidFormatf("incorrect UUID format in string %q", s[:9]) } s = s[9:] default: - return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(s), s) + return invalidFormatf("incorrect UUID length %d in string %q", 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 invalidFormatf("incorrect UUID format in string %q", s) } for i, x := range [16]byte{ 0, 2, 4, 6, @@ -92,7 +85,7 @@ func (u *UUID) Parse(s string) error { v1 := fromHexChar(s[x]) v2 := fromHexChar(s[x+1]) if v1|v2 == 255 { - return errInvalidFormat + return invalidFormat() } u[i] = (v1 << 4) | v2 } @@ -103,7 +96,7 @@ func (u *UUID) Parse(s string) error { v1 := fromHexChar(s[i]) v2 := fromHexChar(s[i+1]) if v1|v2 == 255 { - return errInvalidFormat + return invalidFormat() } u[i/2] = (v1 << 4) | v2 } @@ -175,20 +168,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 invalidFormatf("incorrect UUID format in string %q", 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 invalidFormatf("incorrect UUID format in string %q", b[:9]) } b = b[9:] default: - return fmt.Errorf("uuid: incorrect UUID length %d in string %q", len(b), b) + return invalidFormatf("incorrect UUID length %d in string %q", 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 invalidFormatf("incorrect UUID format in string %q", b) } for i, x := range [16]byte{ 0, 2, 4, 6, @@ -200,7 +193,7 @@ func (u *UUID) UnmarshalText(b []byte) error { v1 := fromHexChar(b[x]) v2 := fromHexChar(b[x+1]) if v1|v2 == 255 { - return errInvalidFormat + return invalidFormat() } u[i] = (v1 << 4) | v2 } @@ -210,7 +203,7 @@ func (u *UUID) UnmarshalText(b []byte) error { v1 := fromHexChar(b[i]) v2 := fromHexChar(b[i+1]) if v1|v2 == 255 { - return errInvalidFormat + return invalidFormat() } u[i/2] = (v1 << 4) | v2 } @@ -226,7 +219,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 invalidFormatf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data)) } copy(u[:], data) diff --git a/error.go b/error.go new file mode 100644 index 0000000..18b8d7e --- /dev/null +++ b/error.go @@ -0,0 +1,45 @@ +package uuid + +import ( + "fmt" +) + +// 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 *ErrUUIDInvalidFormat) Is(target error) bool { + _, ok := target.(*ErrUUIDInvalidFormat) + 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), + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..59e71ea --- /dev/null +++ b/error_test.go @@ -0,0 +1,54 @@ +package uuid + +import ( + "errors" + "fmt" + "reflect" + "testing" +) + +func TestError(t *testing.T) { + tcs := []struct { + err error + expected string + expectedTarget error + }{ + { + err: fmt.Errorf("%w: sample error: %v", &ErrUUID{}, 123), + expected: "uuid: sample error: 123", + expectedTarget: &ErrUUID{}, + }, + { + err: invalidFormat(), + expected: "uuid: invalid UUID format", + expectedTarget: &ErrUUIDInvalidFormat{}, + }, + { + 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{}, + }, + } + for i, tc := range tcs { + t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) { + if !errors.Is(tc.err, &ErrUUID{}) { + t.Error("expected error to be of a wrapped type of Error") + } + if !errors.Is(tc.err, tc.expectedTarget) { + t.Errorf("expected error to be of type %v, but was %v", reflect.TypeOf(tc.expectedTarget), reflect.TypeOf(tc.err)) + } + if tc.err.Error() != tc.expected { + t.Errorf("expected err.Error() to be '%s' but was '%s'", tc.expected, tc.err.Error()) + } + uuidErr := &ErrUUID{} + if !errors.As(tc.err, &uuidErr) { + t.Error("expected errors.As() to work") + } + }) + } +} diff --git a/generator.go b/generator.go index 44be9e1..2a56d33 100644 --- a/generator.go +++ b/generator.go @@ -452,5 +452,5 @@ func defaultHWAddrFunc() (net.HardwareAddr, error) { return iface.HardwareAddr, nil } } - return []byte{}, fmt.Errorf("uuid: no HW address found") + return []byte{}, fmt.Errorf("%w: no HW address found", &ErrUUID{}) } diff --git a/sql.go b/sql.go index 01d5d88..c6c8724 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: cannot convert %T to UUID", &ErrUUID{}, src) } // NullUUID can be used with the standard sql package to represent a diff --git a/uuid.go b/uuid.go index fbe6645..dbabb2d 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", &ErrUUID{}, u, u.Version()) return 0, err } @@ -121,7 +121,7 @@ func TimestampFromV1(u UUID) (Timestamp, error) { // releases until the spec is final. 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", &ErrUUID{}, u, u.Version()) } hi := binary.BigEndian.Uint32(u[0:4]) @@ -141,7 +141,7 @@ func TimestampFromV6(u UUID) (Timestamp, error) { // releases until the spec is final. func TimestampFromV7(u UUID) (Timestamp, error) { if u.Version() != 7 { - 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", &ErrUUID{}, u, u.Version()) } t := 0 | From 09b5842f552a102f07d848f96d401e02954131a0 Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Mon, 8 Apr 2024 17:42:37 +0200 Subject: [PATCH 2/7] Updated error type based on feedback --- codec.go | 28 ++++---- error.go | 51 ++++----------- error_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++---- generator.go | 3 +- sql.go | 2 +- uuid.go | 6 +- 6 files changed, 194 insertions(+), 70 deletions(-) diff --git a/codec.go b/codec.go index c35b9d2..8087955 100644 --- a/codec.go +++ b/codec.go @@ -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) { @@ -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, @@ -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 } @@ -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 } @@ -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, @@ -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 } @@ -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 } @@ -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) diff --git a/error.go b/error.go index 18b8d7e..bb4dd6c 100644 --- a/error.go +++ b/error.go @@ -1,45 +1,22 @@ package uuid -import ( - "fmt" +type Error string + +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 { + _, 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), - } -} diff --git a/error_test.go b/error_test.go index 59e71ea..2fa5a44 100644 --- a/error_test.go +++ b/error_test.go @@ -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) { @@ -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) { t.Error("expected errors.As() to work") } }) } } + +func TestAllErrorMessages(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) + } + }) + } + + // 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) { + 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) + } +} diff --git a/generator.go b/generator.go index 2a56d33..f643dae 100644 --- a/generator.go +++ b/generator.go @@ -26,7 +26,6 @@ import ( "crypto/rand" "crypto/sha1" "encoding/binary" - "fmt" "hash" "io" "net" @@ -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 } diff --git a/sql.go b/sql.go index c6c8724..cdc7518 100644 --- a/sql.go +++ b/sql.go @@ -56,7 +56,7 @@ func (u *UUID) Scan(src interface{}) error { return err } - return fmt.Errorf("%w: cannot convert %T to UUID", &ErrUUID{}, 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 dbabb2d..f6dfc4c 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("%w: %s is version %d, not version 1", &ErrUUID{}, u, u.Version()) + err := fmt.Errorf("%w %s is version %d, not version 1", ErrInvalidVersion, u, u.Version()) return 0, err } @@ -121,7 +121,7 @@ func TimestampFromV1(u UUID) (Timestamp, error) { // releases until the spec is final. func TimestampFromV6(u UUID) (Timestamp, error) { if u.Version() != 6 { - return 0, fmt.Errorf("%w: %s is version %d, not version 6", &ErrUUID{}, 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]) @@ -141,7 +141,7 @@ func TimestampFromV6(u UUID) (Timestamp, error) { // releases until the spec is final. func TimestampFromV7(u UUID) (Timestamp, error) { if u.Version() != 7 { - return 0, fmt.Errorf("%w: %s is version %d, not version 6", &ErrUUID{}, u, u.Version()) + return 0, fmt.Errorf("%w %s is version %d, not version 6", ErrInvalidVersion, u, u.Version()) } t := 0 | From 56acf313e20e154c3c2ed5523a5d5d9c1e95bba7 Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Tue, 9 Apr 2024 11:22:38 +0200 Subject: [PATCH 3/7] Added missing documentation --- error.go | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/error.go b/error.go index bb4dd6c..cd06f7f 100644 --- a/error.go +++ b/error.go @@ -1,21 +1,45 @@ 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 = Error("uuid: invalid UUID format") + // 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 = 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:") + + // 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) } +// Is checks if the target error is a UUID Error func (e Error) Is(target error) bool { _, ok := target.(*Error) return ok From fcc3947d543dc092406224459ce4b122e03d8ed6 Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Tue, 9 Apr 2024 11:22:54 +0200 Subject: [PATCH 4/7] Restructured the tests --- error_test.go | 59 ++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/error_test.go b/error_test.go index 2fa5a44..7509aac 100644 --- a/error_test.go +++ b/error_test.go @@ -8,7 +8,7 @@ import ( "testing" ) -func TestError(t *testing.T) { +func TestIsAsError(t *testing.T) { var e Error tcs := []struct { err error @@ -51,7 +51,7 @@ func TestError(t *testing.T) { } } -func TestAllErrorMessages(t *testing.T) { +func TestParseErrors(t *testing.T) { tcs := []struct { function string uuidStr string @@ -119,8 +119,9 @@ func TestAllErrorMessages(t *testing.T) { } }) } +} - // Unmarshal binary +func TestUnmarshalBinaryError(t *testing.T) { id := UUID{} b := make([]byte, 33) expectedErr := "uuid: UUID must be exactly 16 bytes long, got 33 bytes" @@ -132,68 +133,74 @@ func TestAllErrorMessages(t *testing.T) { if err.Error() != expectedErr { t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr) } +} - // no hw address error - netInterfaces = func() ([]net.Interface, error) { - return nil, nil - } - defer func() { - netInterfaces = net.Interfaces - }() - _, err = defaultHWAddrFunc() +func TestScanError(t *testing.T) { + id := UUID{} + err := id.Scan(123) if err == nil { t.Error("expected an error") return } - expectedErr = "uuid: no HW address found" + expectedErr := "uuid: cannot convert int to UUID" if err.Error() != expectedErr { t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr) } +} - // scan error - err = id.Scan(123) +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: cannot convert int to UUID" + 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 V1 Version + // UUId V2 Version id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e") - _, err = TimestampFromV1(id) + _, err = TimestampFromV6(id) if err == nil { t.Error("expected an error") return } - expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 1" + 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 V2 Version + // UUId V7 Version id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e") - _, err = TimestampFromV6(id) + _, err = TimestampFromV7(id) if err == nil { t.Error("expected an error") return } - expectedErr = "uuid: e86160d3-beff-443c-b9b5-1f8197ccb12e is version 4, not version 6" + 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) } +} - // UUId V7 Version - id = FromStringOrNil("e86160d3-beff-443c-b9b5-1f8197ccb12e") - _, err = TimestampFromV7(id) +// 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 } - // 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" + expectedErr := "uuid: no HW address found" if err.Error() != expectedErr { t.Errorf("unexpected error '%s' != '%s'", err.Error(), expectedErr) } From 0342a6ae14e2dccd383d30e6768e764bf28b89ac Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Tue, 9 Apr 2024 17:01:49 +0200 Subject: [PATCH 5/7] Removed Error.Is() --- error.go | 6 ------ error_test.go | 27 +++++++++------------------ 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/error.go b/error.go index cd06f7f..7ca0be4 100644 --- a/error.go +++ b/error.go @@ -38,9 +38,3 @@ const ( func (e Error) Error() string { return string(e) } - -// Is checks if the target error is a UUID Error -func (e Error) Is(target error) bool { - _, ok := target.(*Error) - return ok -} diff --git a/error_test.go b/error_test.go index 7509aac..5fa195f 100644 --- a/error_test.go +++ b/error_test.go @@ -4,42 +4,33 @@ import ( "errors" "fmt" "net" - "reflect" "testing" ) func TestIsAsError(t *testing.T) { - var e Error tcs := []struct { - err error - expected string - expectedTarget error + err error + expected string }{ { - err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123), - expected: "uuid: sample error: 123", - expectedTarget: &e, + err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123), + expected: "uuid: sample error: 123", }, { - err: fmt.Errorf("%w", ErrInvalidFormat), - expected: "uuid: invalid UUID format", - expectedTarget: ErrInvalidFormat, + err: fmt.Errorf("%w", ErrInvalidFormat), + expected: "uuid: invalid UUID format", }, { - err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"), - expected: "uuid: incorrect UUID format in string \"test\"", - expectedTarget: ErrIncorrectFormatInString, + err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"), + expected: "uuid: incorrect UUID format in string \"test\"", }, } for i, tc := range tcs { t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) { var e2 Error - if !errors.Is(tc.err, &e2) { + if !errors.As(tc.err, &e2) { t.Error("expected error to be of a wrapped type of Error") } - if !errors.Is(tc.err, tc.expectedTarget) { - t.Errorf("expected error to be of type %v, but was %v", reflect.TypeOf(tc.expectedTarget), reflect.TypeOf(tc.err)) - } if tc.err.Error() != tc.expected { t.Errorf("expected err.Error() to be '%s' but was '%s'", tc.expected, tc.err.Error()) } From e38c6aed5616eee641062665e8f7c8890b00147e Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Wed, 10 Apr 2024 07:24:29 +0200 Subject: [PATCH 6/7] Added expected err errors.Is() checking --- error_test.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/error_test.go b/error_test.go index 5fa195f..8cdcad1 100644 --- a/error_test.go +++ b/error_test.go @@ -9,20 +9,24 @@ import ( func TestIsAsError(t *testing.T) { tcs := []struct { - err error - expected string + err error + expected string + expectedErr error }{ { - err: fmt.Errorf("%w sample error: %v", ErrInvalidVersion, 123), - expected: "uuid: sample error: 123", + 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", + 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\"", + err: fmt.Errorf("%w %q", ErrIncorrectFormatInString, "test"), + expected: "uuid: incorrect UUID format in string \"test\"", + expectedErr: ErrIncorrectFormatInString, }, } for i, tc := range tcs { @@ -38,6 +42,9 @@ func TestIsAsError(t *testing.T) { 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) + } }) } } From f71328556bb055587845907e85b861923349c532 Mon Sep 17 00:00:00 2001 From: Patrik Lindahl Date: Thu, 11 Apr 2024 19:14:01 +0200 Subject: [PATCH 7/7] Removed redundant error check --- error_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/error_test.go b/error_test.go index 8cdcad1..7d58e74 100644 --- a/error_test.go +++ b/error_test.go @@ -31,10 +31,6 @@ func TestIsAsError(t *testing.T) { } for i, tc := range tcs { t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) { - var e2 Error - if !errors.As(tc.err, &e2) { - t.Error("expected error to be of a wrapped type of Error") - } if tc.err.Error() != tc.expected { t.Errorf("expected err.Error() to be '%s' but was '%s'", tc.expected, tc.err.Error()) }