Skip to content
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Comment on lines +185 to +195
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to explain the differences between Is() and IdentifierStartsWith()

 e := Wrap(Wrap(Wrap(Base, WithIdentifier(1)), WithIdentifier(2)), WithIdentifier(3))
 IdentifierStartsWith(e, 3, 2) // true — matches the most recent wraps (outermost)
 e.Is(parent)                  // matches an ancestor (innermost)

### 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:
Expand Down
26 changes: 24 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ""
Expand All @@ -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)
}
}

Expand Down
47 changes: 47 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down