88 "path"
99 "path/filepath"
1010 "runtime"
11+ "slices"
12+ "strconv"
1113 "strings"
1214 "time"
1315)
@@ -36,97 +38,88 @@ type Trace struct {
3638type Error struct {
3739 // The error title
3840 Title string `json:"title" yaml:"title"`
41+ // Options for the error
42+ Options Opts `json:"options,omitempty" yaml:"options,omitempty"`
43+ // A trace of where the error was thrown
44+ Stack []* Trace `json:"stack,omitempty" yaml:"stack,omitempty"`
45+ }
3946
47+ type Opts struct {
4048 // A numeric error code for programmatic handling
41- Identifier int32 `json:"identifier,omitempty" yaml:"identifier,omitempty"`
42-
49+ Identifier []uint32 `json:"identifier,omitempty" yaml:"identifier,omitempty"`
4350 // Additional context as a list of strings
4451 Details []string `json:"details,omitempty" yaml:"details,omitempty"`
45-
4652 // Key-value pairs for arbitrary metadata
4753 Properties map [string ]any `json:"properties,omitempty" yaml:"properties,omitempty"`
48-
4954 // The underlying error that caused this error
5055 Cause error `json:"cause,omitempty" yaml:"cause,omitempty"`
51-
52- // A trace of where the error was thrown
53- Stack []* Trace `json:"stack,omitempty" yaml:"stack,omitempty"`
5456}
5557
5658// New creates a new Error with the given title.
5759func New (title string ) error {
5860 return & Error {
5961 Title : title ,
62+ Options : Opts {
63+ Details : []string {},
64+ Properties : make (map [string ]any ),
65+ Cause : errors .New (title ),
66+ },
6067 }
6168}
6269
63- // Wrap wraps an error with a message.
64- func Wrap (err error , msg string ) error {
65- trace := trace ()
66- return from (err , true ).WithDetail (msg ).throw (trace )
67- }
70+ // Option is a function type that modifies the Options struct.
71+ type Option func (* Opts )
6872
69- // Wrapf wraps an error with a formatted message.
70- func Wrapf (err error , format string , args ... any ) error {
71- trace := trace ()
72- return from (err , true ).WithDetailf (format , args ... ).throw (trace )
73- }
74-
75- // From creates a new *Error from any error type.
76- // If the error is not an *Error, it creates a new error with title "unknown error"
77- // and sets the original error as the cause.
78- // If the error is an *Error, it returns a copy of the original error with the same
79- // title, identifier, details, properties.
80- func From (err error ) * Error {
81- return from (err , false )
73+ // WithIdentifier sets a numeric identifier for the error.
74+ func WithIdentifier (id uint32 ) Option {
75+ return func (c * Opts ) {
76+ c .Identifier = append (c .Identifier , id )
77+ }
8278}
8379
84- func from (err error , copyStack bool ) * Error {
85- var t * Error
86-
87- ok := errors .As (err , & t )
88- if ! ok {
89- t , _ = New ("unknown error" ).(* Error )
80+ // WithDetail sets a detail string for the error.
81+ func WithDetail (msg string ) Option {
82+ return func (c * Opts ) {
83+ c .Details = append (c .Details , msg )
9084 }
85+ }
9186
92- e := & Error {
93- Title : t .Title ,
94- Identifier : t .Identifier ,
95- Details : t .Details ,
96- Properties : t .Properties ,
97- }
98- if copyStack {
99- e .Stack = t .Stack
87+ // WithDetailf sets a detail string for the error using a format string.
88+ func WithDetailf (format string , args ... any ) Option {
89+ return func (c * Opts ) {
90+ c .Details = append (c .Details , fmt .Sprintf (format , args ... ))
10091 }
92+ }
10193
102- if ! ok {
103- e .Cause = err
94+ // WithIdentifier sets a numeric identifier for the error.
95+ func WithProperty (key string , value any ) Option {
96+ return func (c * Opts ) {
97+ c .Properties [key ] = value
10498 }
105-
106- return e
10799}
108100
109- // Intercept converts any error into an *Error type.
110- // If the provided error is already an *Error, it returns it as-is.
111- // Otherwise, it creates a new *Error wrapping the original error using From().
112- func Intercept (err error ) * Error {
113- var e * Error
114- if errors .As (err , & e ) {
115- return e
101+ // CausedBy sets the underlying cause of this error.
102+ func CausedBy (err error ) Option {
103+ return func (c * Opts ) {
104+ c .Cause = err
116105 }
106+ }
117107
118- return From (err )
108+ // Wrap wraps an error with a message.
109+ func Wrap (err error , opts ... Option ) error {
110+ trace := trace ()
111+ return from (err , true , opts ... ).throw (trace )
119112}
120113
121114// Is compares this error with another error for equality.
122- // Two errors are considered equal if they have the same Title and Identifier .
115+ // Two errors are considered equal if they have the same Title and Identifiers .
123116func (e * Error ) Is (err error ) bool {
124117 other := new (Error )
125118 if ok := errors .As (err , & other ); ! ok {
126119 return false
127120 }
128121
129- return e .Title == other .Title && e . Identifier == other .Identifier
122+ return e .Title == other .Title && slices . Equal ( e . Options . Identifier , other .Options . Identifier )
130123}
131124
132125// Is is a wrapper around errors.Is to compare two errors for equality.
@@ -135,13 +128,26 @@ func Is(err, target error) bool { return errors.Is(err, target) }
135128// As is a wrapper around errors.As to check if the error is of a specific type.
136129func As (err error , target any ) bool { return errors .As (err , target ) }
137130
131+ // Unwrap returns the underlying cause of this error, nil if no cause.
132+ func Unwrap (err error ) error {
133+ u , ok := err .(interface {
134+ Unwrap () error
135+ })
136+ if ! ok {
137+ return nil
138+ }
139+ return u .Unwrap ()
140+ }
141+
138142// Unwrap returns the underlying cause of this error, nil if no cause.
139143func (e * Error ) Unwrap () error {
140144 if e == nil {
141145 return nil
142146 }
143-
144- return e .Cause
147+ if e .Options .Cause != nil {
148+ return e .Options .Cause
149+ }
150+ return nil
145151}
146152
147153// Error returns a formatted string representation of the error,
@@ -155,21 +161,21 @@ func (e *Error) Error() string {
155161
156162 fmt .Fprintf (
157163 b ,
158- "%s (%d ):" ,
164+ "%s (%s ):" ,
159165 strings .ToLower (e .Title ),
160- e . Identifier ,
166+ concatenateUint32Slice ( e . Options . Identifier ) ,
161167 )
162168
163- if len (e .Details ) > 0 {
169+ if len (e .Options . Details ) > 0 {
164170 fmt .Fprintf (
165171 b ,
166172 " %s:" ,
167173
168- strings .Join (e .Details , ": " ),
174+ strings .Join (e .Options . Details , ": " ),
169175 )
170176 }
171177
172- for k , v := range e .Properties {
178+ for k , v := range e .Options . Properties {
173179 fmt .Fprintf (b , " %s='%v'," , k , v )
174180 }
175181
@@ -187,8 +193,8 @@ func (e *Error) Error() string {
187193 }
188194 }
189195
190- if e .Cause != nil {
191- fmt .Fprintf (b , " caused by: %v" , e .Cause .Error ())
196+ if e .Options . Cause != nil {
197+ fmt .Fprintf (b , " caused by: %v" , e .Options . Cause .Error ())
192198 }
193199
194200 return string (bytes .TrimSuffix (bytes .TrimSuffix (b .Bytes (), []byte ("," )), []byte (":" )))
@@ -206,55 +212,40 @@ func (e *Error) String() string {
206212 return b .String ()
207213}
208214
209- // Stamp adds a stack trace entry to an existing error.
210- func Stamp (err error ) error {
211- trace := trace ()
212-
213- return Intercept (err ).throw (trace )
214- }
215-
216- // WithIdentifier sets a numeric identifier for the error.
217- func (e * Error ) WithIdentifier (id int32 ) * Error {
218- e .Identifier = id
219-
220- return e
221- }
222-
223- // WithDetail adds a detail string to the error for additional context.
224- func (e * Error ) WithDetail (detail string ) * Error {
225- e .Details = append (e .Details , strings .TrimSuffix (detail , "." ))
226-
227- return e
228- }
229-
230- // WithDetailf adds a detail string to the error for additional context using a format string.
231- func (e * Error ) WithDetailf (format string , args ... any ) * Error {
232- return e .WithDetail (fmt .Sprintf (format , args ... ))
233- }
215+ func from (err error , copyStack bool , opts ... Option ) * Error {
216+ var t * Error
234217
235- // WithProperties adds multiple key-value properties to the error.
236- func (e * Error ) WithProperties (properties map [string ]any ) * Error {
237- for k , v := range properties {
238- e .WithProperty (k , v ) // nolint: errcheck // No way to get wrong here.
218+ ok := errors .As (err , & t )
219+ if ! ok {
220+ t , _ = New ("unknown error" ).(* Error )
239221 }
240222
241- return e
242- }
243-
244- // WithProperty adds a single key-value property to the error.
245- func (e * Error ) WithProperty (key string , value any ) * Error {
246- if e .Properties == nil {
247- e .Properties = make (map [string ]any )
223+ props := make (map [string ]any )
224+ for k , v := range t .Options .Properties {
225+ props [k ] = v
226+ }
227+ o := Opts {
228+ Details : t .Options .Details ,
229+ Properties : props ,
230+ Cause : t .Options .Cause ,
231+ }
232+ if t .Options .Identifier != nil {
233+ o .Identifier = t .Options .Identifier
234+ }
235+ for _ , opt := range opts {
236+ opt (& o )
237+ }
238+ e := & Error {
239+ Title : t .Title ,
240+ Options : o ,
241+ }
242+ if copyStack {
243+ e .Stack = t .Stack
248244 }
249245
250- e .Properties [key ] = value
251-
252- return e
253- }
254-
255- // CausedBy sets the underlying cause of this error.
256- func (e * Error ) CausedBy (err error ) * Error {
257- e .Cause = err
246+ if ! ok {
247+ e .Options .Cause = err
248+ }
258249
259250 return e
260251}
@@ -269,13 +260,6 @@ func (e *Error) throw(trace *Trace) error {
269260 return e
270261}
271262
272- // Throw adds a stack trace entry to the error and returns it as an error interface.
273- func (e * Error ) Throw () error {
274- trace := trace ()
275-
276- return e .throw (trace )
277- }
278-
279263func trace () * Trace {
280264 // 2 is the depth of the caller.
281265 pc , file , line , ok := runtime .Caller (2 )
@@ -295,3 +279,35 @@ func trace() *Trace {
295279 Timestamp : time .Now ().UTC (),
296280 }
297281}
282+
283+ // concatenateUint32Slice takes a slice of uint32 and returns a single string
284+ // with all elements joined by a hyphen ("-").
285+ func concatenateUint32Slice (nums []uint32 ) string {
286+ if len (nums ) == 0 {
287+ return ""
288+ }
289+
290+ slices .Reverse (nums )
291+
292+ // Use a strings.Builder for efficient string concatenation.
293+ var builder strings.Builder
294+
295+ // Iterate over the slice elements reversly.
296+ for i , num := range nums {
297+ // Convert the int32 to its string representation.
298+ // We use base 10 (decimal) and specify 32-bit type for clarity,
299+ // though 'FormatInt' takes an int64 internally (int32 is safely converted).
300+ str := strconv .FormatInt (int64 (num ), 10 )
301+
302+ // Write the string representation to the builder.
303+ builder .WriteString (str )
304+
305+ // Append the separator for all elements except the last one.
306+ if i < len (nums )- 1 {
307+ builder .WriteString ("-" )
308+ }
309+ }
310+
311+ // Return the final concatenated string.
312+ return builder .String ()
313+ }
0 commit comments