From 81f23172c67886313664a26ff710a7083a872ce3 Mon Sep 17 00:00:00 2001 From: Anthony TREUILLIER Date: Tue, 26 May 2026 11:40:53 +0200 Subject: [PATCH] feat: Add IdentifierStartsWith Signed-off-by: Anthony TREUILLIER --- README.md | 19 +++++++++++++++++++ errors.go | 26 ++++++++++++++++++++++++-- errors_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aef3bea..c758f57 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,25 @@ e2.Is(e1) return True // e1 is a parent of e2 e1.Is(e2) return False ``` +### Specific use-case with IdentifierStartsWith() + +`IdentifierStartsWith(error, prefix)` checks whether this error's identifier, formatted as a string, starts with the given prefix. + +For example: +If e.Identifier: "3-2-1", then +```go +IdentifierStartsWith(e, "3-2") return True +IdentifierStartsWith(e, "2-1") return False +``` + +### Is() and IdentifierStartsWith() comparison + +```go +e := Wrap(Wrap(Wrap(ErrForbidden, WithIdentifier(1)), WithIdentifier(2)), WithIdentifier(3)) +IdentifierStartsWith(e, "3-2") // true — matches the most recent wraps (outermost) +Is(e, ErrForbidden) // matches an ancestor (innermost) +``` + ## Output Format The `Error()` method produces output in the following format: diff --git a/errors.go b/errors.go index 35186d4..f0d0275 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,8 @@ import ( "strings" ) +const separator = "-" + // Trace represents a single entry in an error's stack trace. // It captures the location and time when an error was thrown or stamped. type Trace struct { @@ -178,6 +180,26 @@ func Is(err, target error) bool { return errors.Is(err, target) } // As is a wrapper around errors.As to check if the error is of a specific type. func As(err error, target any) bool { return errors.As(err, target) } +// IdentifierStartsWith checks if the error's identifier string starts with the given prefix. +func IdentifierStartsWith(err error, prefix string) bool { + var e *Error + if !errors.As(err, &e) { + return false + } + + if prefix == "" { + return true + } + + // To avoid uint32 conversion and errors handling, we decided to compare: + // * the identifier, suffixed with an hyphen + // * the prefix, suffixed with an hyphen + return strings.HasPrefix( + e.GetIdentifier()+separator, + prefix+separator, + ) +} + // Unwrap returns the underlying cause of this error, nil if no cause. func Unwrap(err error) error { u, ok := err.(interface { @@ -329,7 +351,7 @@ func trace() *Trace { } } -// GetIdentifier returns a string with all identifiers reversed and joined by a hyphen ("-"). +// GetIdentifier returns a string with all identifiers reversed and joined by a hyphen (-). func (e *Error) GetIdentifier() string { if len(e.Identifier) == 0 { return "" @@ -354,7 +376,7 @@ func (e *Error) GetIdentifier() string { // Append the separator for all elements except the last one. if i < len(clone)-1 { - builder.WriteString("-") + builder.WriteString(separator) } } diff --git a/errors_test.go b/errors_test.go index 67cea83..fa5cac9 100644 --- a/errors_test.go +++ b/errors_test.go @@ -445,6 +445,53 @@ var _ = Describe("Errors", func() { }) }) + Context("When comparing error with IdentifierStartsWith", func() { + It("should return true when error's identifier starts with the prefix", func() { + e := Wrap(ErrForbidden, WithIdentifier(1)) + Expect(IdentifierStartsWith(e, "1")).To(BeTrue()) + }) + + It("should return true when error's identifiers start with the prefix", func() { + e1_1 := Wrap(ErrForbidden, WithIdentifier(12)) + e1_2 := Wrap(e1_1, WithIdentifier(22)) + e1_3 := Wrap(e1_2, WithIdentifier(31)) + Expect(IdentifierStartsWith(e1_3, "3")).To(BeFalse()) + Expect(IdentifierStartsWith(e1_3, "31")).To(BeTrue()) + Expect(IdentifierStartsWith(e1_3, "31-2")).To(BeFalse()) + Expect(IdentifierStartsWith(e1_3, "31-22")).To(BeTrue()) + Expect(IdentifierStartsWith(e1_3, "31-22-1")).To(BeFalse()) + Expect(IdentifierStartsWith(e1_3, "31-22-12")).To(BeTrue()) + }) + + It("should return false when error's identifier does not start with the prefix", func() { + e := Wrap(ErrForbidden, WithIdentifier(1)) + Expect(IdentifierStartsWith(e, "2")).To(BeFalse()) + }) + + It("should return false when error's identifiers do not start with the prefix", func() { + e1_1 := Wrap(ErrForbidden, WithIdentifier(1)) + e1_2 := Wrap(e1_1, WithIdentifier(2)) + e1_3 := Wrap(e1_2, WithIdentifier(3)) + Expect(IdentifierStartsWith(e1_3, "1")).To(BeFalse()) + Expect(IdentifierStartsWith(e1_3, "2")).To(BeFalse()) + Expect(IdentifierStartsWith(e1_3, "2-1")).To(BeFalse()) + }) + + It("should return false when error is not an *Error", func() { + e := errTest + Expect(IdentifierStartsWith(e, "1")).To(BeFalse()) + }) + + It("should return false when error is nil", func() { + Expect(IdentifierStartsWith(nil, "1")).To(BeFalse()) + }) + + It("should return true when empty prefix", func() { + e := Wrap(ErrForbidden, WithIdentifier(1)) + Expect(IdentifierStartsWith(e, "")).To(BeTrue()) + }) + }) + Context("When unwrapping errors", func() { It("should return the cause when present", func() { e := Wrap(ErrForbidden, CausedBy(errPerm))