Skip to content

Commit 1701fbb

Browse files
refactor: Refactor to conform to DESIGN.md
Refs: MK8S-183 Signed-off-by: Anthony TREUILLIER <anthony.treuillier@scality.com>
1 parent 0a9fcaf commit 1701fbb

File tree

2 files changed

+441
-792
lines changed

2 files changed

+441
-792
lines changed

errors.go

Lines changed: 129 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"path"
99
"path/filepath"
1010
"runtime"
11+
"slices"
12+
"strconv"
1113
"strings"
1214
"time"
1315
)
@@ -36,97 +38,88 @@ type Trace struct {
3638
type 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.
5759
func 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.
123116
func (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.
136129
func 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.
139143
func (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-
279263
func 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

Comments
 (0)