Skip to content

Commit ee59589

Browse files
authored
feat(code): allow any code type (#94)
* feat(code): allow any code type Change Code(...) to accept any and Code() to return any. Update docs/tests for int and string codes. Fix stacktrace path normalization in module mode. BREAKING CHANGE: OopsError.Code() now returns any (was string). * refactor(code): extract getDeepestErrorCode --------- Co-authored-by: Andy <>
1 parent 56a34a1 commit ee59589

File tree

9 files changed

+75
-30
lines changed

9 files changed

+75
-30
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ The `oops.OopsError` builder must finish with either `.Errorf(...)`, `.Wrap(...)
181181
| --------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
182182
| `.With(string, any)` | `err.Context() map[string]any` | Supply a list of attributes key+value. Values of type `func() any {}` are accepted and evaluated lazily. |
183183
| `.WithContext(context.Context, ...any)` | `err.Context() map[string]any` | Supply a list of values declared in context. Values of type `func() any {}` are accepted and evaluated lazily. |
184-
| `.Code(string)` | `err.Code() string` | Set a code or slug that describes the error. Error messages are intended to be read by humans, but such code is expected to be read by machines and be transported over different services |
184+
| `.Code(any)` | `err.Code() any` | Set a code or slug that describes the error. Error messages are intended to be read by humans, but such code is expected to be read by machines and be transported over different services |
185185
| `.Public(string)` | `err.Public() string` | Set a message that is safe to show to an end user |
186186
| `.Time(time.Time)` | `err.Time() time.Time` | Set the error time (default: `time.Now()`) |
187187
| `.Since(time.Time)` | `err.Duration() time.Duration` | Set the error duration |

builder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func newBuilder() OopsErrorBuilder {
6060
return OopsErrorBuilder{
6161
err: nil,
6262
msg: "",
63-
code: "",
63+
code: nil,
6464
time: time.Now(),
6565
duration: 0,
6666

@@ -316,7 +316,7 @@ func (o OopsErrorBuilder) Assertf(condition bool, msg string, args ...any) OopsE
316316
// Example:
317317
//
318318
// oops.Code("database_connection_failed").Errorf("connection timeout")
319-
func (o OopsErrorBuilder) Code(code string) OopsErrorBuilder {
319+
func (o OopsErrorBuilder) Code(code any) OopsErrorBuilder {
320320
o2 := o.copy()
321321
o2.code = code
322322
return o2

docs/API.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The Oops library uses a fluent builder pattern. All methods return an `*oops.Oop
5151

5252
### Context Methods
5353

54-
#### `.Code(code string) OopsErrorBuilder`
54+
#### `.Code(code any) OopsErrorBuilder`
5555
Sets an error code for machine-readable identification.
5656

5757
```go
@@ -251,7 +251,7 @@ The `OopsError` type implements the standard `error` interface and provides addi
251251
```go
252252
type OopsError interface {
253253
error
254-
Code() string
254+
Code() any
255255
Time() time.Time
256256
Duration() time.Duration
257257
Domain() string
@@ -303,4 +303,4 @@ logger.WithField("error", err).Error("file operation failed")
303303
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
304304
err := oops.Errorf("network timeout")
305305
logger.Error("network error", slog.Any("error", err))
306-
```
306+
```

docs/ARCHITECTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ err := oops.
2929
```go
3030
type OopsErrorBuilder interface {
3131
// Context methods
32-
Code(code string) OopsErrorBuilder
32+
Code(code any) OopsErrorBuilder
3333
In(domain string) OopsErrorBuilder
3434
Tags(tags ...string) OopsErrorBuilder
3535
Trace(trace string) OopsErrorBuilder
@@ -193,4 +193,4 @@ The Oops library is designed with a focus on:
193193
4. **Integration**: Works with existing logging systems
194194
5. **Debugging**: Rich context for faster issue resolution
195195

196-
The architecture supports these goals through careful design of interfaces, efficient data structures, and thoughtful integration patterns.
196+
The architecture supports these goals through careful design of interfaces, efficient data structures, and thoughtful integration patterns.

docs/FAQ.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,12 @@ func TestUserProcessing(t *testing.T) {
304304
```go
305305
type MockOopsError struct {
306306
message string
307-
code string
307+
code any
308308
domain string
309309
}
310310

311311
func (m *MockOopsError) Error() string { return m.message }
312-
func (m *MockOopsError) Code() string { return m.code }
312+
func (m *MockOopsError) Code() any { return m.code }
313313
func (m *MockOopsError) Domain() string { return m.domain }
314314
// ... implement other methods as needed
315315
```
@@ -347,7 +347,7 @@ if errors.As(err, &oopsErr) {
347347
var oopsErr oops.OopsError
348348
if errors.As(err, &oopsErr) {
349349
code := oopsErr.Code()
350-
fmt.Printf("Error code: %s\n", code)
350+
fmt.Printf("Error code: %v\n", code)
351351
}
352352
```
353353

@@ -357,7 +357,7 @@ if errors.As(err, &oopsErr) {
357357
var oopsErr oops.OopsError
358358
if errors.As(err, &oopsErr) {
359359
// This is an Oops error
360-
fmt.Printf("Code: %s\n", oopsErr.Code())
360+
fmt.Printf("Code: %v\n", oopsErr.Code())
361361
} else {
362362
// This is not an Oops error
363363
fmt.Printf("Standard error: %s\n", err.Error())

error.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type OopsError struct {
4444
// Core error information
4545
err error // The underlying error being wrapped
4646
msg string // Additional error message
47-
code string // Machine-readable error code/slug
47+
code any // Machine-readable error code/slug (any JSON/log-friendly type)
4848
time time.Time // When the error occurred
4949
duration time.Duration // Duration associated with the error
5050

@@ -111,13 +111,23 @@ func (o OopsError) Error() string {
111111
// Code returns the error code from the deepest error in the chain.
112112
// Error codes are machine-readable identifiers that can be used for
113113
// programmatic error handling and cross-service error correlation.
114-
func (o OopsError) Code() string {
115-
return getDeepestErrorAttribute(
116-
o,
117-
func(e OopsError) string {
118-
return e.code
119-
},
120-
)
114+
func (o OopsError) Code() any {
115+
return getDeepestErrorCode(o)
116+
}
117+
118+
func getDeepestErrorCode(err OopsError) any {
119+
if err.err == nil {
120+
return err.code
121+
}
122+
123+
if child, ok := AsOops(err.err); ok {
124+
deepest := getDeepestErrorCode(child)
125+
if deepest != nil {
126+
return deepest
127+
}
128+
}
129+
130+
return err.code
121131
}
122132

123133
// Time returns the timestamp when the error occurred.
@@ -434,8 +444,8 @@ func (o OopsError) LogValue() slog.Value { //nolint:gocyclo
434444
attrs = append(attrs, slog.String("err", err))
435445
}
436446

437-
if code := o.Code(); code != "" {
438-
attrs = append(attrs, slog.String("code", code))
447+
if code := o.Code(); code != nil {
448+
attrs = append(attrs, slog.Any("code", code))
439449
}
440450

441451
if t := o.Time(); t != (time.Time{}) {
@@ -546,7 +556,7 @@ func (o OopsError) ToMap() map[string]any { //nolint:gocyclo
546556
payload["error"] = err
547557
}
548558

549-
if code := o.Code(); code != "" {
559+
if code := o.Code(); code != nil {
550560
payload["code"] = code
551561
}
552562

@@ -669,8 +679,8 @@ func (o *OopsError) formatVerbose() string { //nolint:gocyclo
669679
var output strings.Builder
670680
_, _ = fmt.Fprintf(&output, "Oops: %s\n", o.Error())
671681

672-
if code := o.Code(); code != "" {
673-
_, _ = fmt.Fprintf(&output, "Code: %s\n", code)
682+
if code := o.Code(); code != nil {
683+
_, _ = fmt.Fprintf(&output, "Code: %v\n", code)
674684
}
675685

676686
if t := o.Time(); t != (time.Time{}) {

helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import "errors"
1515
// err := someFunction()
1616
// if oopsErr, ok := oops.AsOops(err); ok {
1717
// // Access oops-specific information
18-
// fmt.Printf("Error code: %s\n", oopsErr.Code())
18+
// fmt.Printf("Error code: %v\n", oopsErr.Code())
1919
// fmt.Printf("Domain: %s\n", oopsErr.Domain())
2020
// fmt.Printf("Stacktrace: %s\n", oopsErr.Stacktrace())
2121
//

oops.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func Assertf(condition bool, msg string, args ...any) OopsErrorBuilder {
7373
// Code set a code or slug that describes the error.
7474
// Error messages are intended to be read by humans, but such code is expected to
7575
// be read by machines and even transported over different services.
76-
func Code(code string) OopsErrorBuilder {
76+
func Code(code any) OopsErrorBuilder {
7777
return newBuilder().Code(code)
7878
}
7979

oops_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -938,22 +938,57 @@ func TestOopsGetDeepestErrorAttributeEdgeCases(t *testing.T) {
938938

939939
// Test with nil error
940940
result := getDeepestErrorAttribute(OopsError{err: nil}, func(o OopsError) string {
941-
return o.Code()
941+
return o.domain
942942
})
943943
is.Empty(result)
944944
// Test with non-OopsError
945945
result2 := getDeepestErrorAttribute(OopsError{err: assert.AnError}, func(o OopsError) string {
946-
return o.Code()
946+
return o.domain
947947
})
948948
is.Empty(result2)
949949
// Test with OopsError but no context
950950
err := OopsError{err: assert.AnError}
951951
result3 := getDeepestErrorAttribute(err, func(o OopsError) string {
952-
return o.Code()
952+
return o.domain
953953
})
954954
is.Empty(result3)
955955
}
956956

957+
func TestOopsCodeSupportsAny(t *testing.T) {
958+
is := assert.New(t)
959+
t.Parallel()
960+
961+
err := Code(404).Wrap(assert.AnError)
962+
is.Error(err)
963+
is.Equal(404, err.(OopsError).Code())
964+
is.Equal(404, err.(OopsError).ToMap()["code"])
965+
966+
inner := Code(1).Wrap(assert.AnError)
967+
outer := Code(2).Wrap(inner)
968+
is.Equal(1, outer.(OopsError).Code())
969+
is.Equal(2, outer.(OopsError).code)
970+
971+
inner2 := Wrap(assert.AnError)
972+
outer2 := Code(2).Wrap(inner2)
973+
is.Equal(2, outer2.(OopsError).Code())
974+
975+
errCleared := Code(nil).Wrap(assert.AnError)
976+
is.Empty(errCleared.(OopsError).Code())
977+
_, hasCode := errCleared.(OopsError).ToMap()["code"]
978+
is.False(hasCode)
979+
}
980+
981+
func TestOopsCodeNestedLayers(t *testing.T) {
982+
is := assert.New(t)
983+
t.Parallel()
984+
985+
layer1 := newBuilder().Wrap(assert.AnError)
986+
layer2 := newBuilder().Code("layer2_code").Wrap(layer1)
987+
layer3 := newBuilder().Code("layer3_code").Wrap(layer2)
988+
989+
is.Equal("layer2_code", layer3.(OopsError).Code())
990+
}
991+
957992
func TestOopsMergeNestedErrorMapEdgeCases(t *testing.T) {
958993
is := assert.New(t)
959994
t.Parallel()

0 commit comments

Comments
 (0)