Skip to content

Commit 9594a7d

Browse files
committed
refactor: pull out independent code into separate files
1 parent 1d9a9fe commit 9594a7d

6 files changed

Lines changed: 329 additions & 331 deletions

File tree

error.go

Lines changed: 4 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,9 @@ package rogerr
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
7-
"runtime"
8-
"runtime/debug"
9-
"strings"
106
)
117

12-
// Frame represents a single frame in a stacktrace.
13-
type Frame struct {
14-
File string // Full file path
15-
Line int // Line number
16-
Function string // Function or method name
17-
InApp bool // true if application code, false if dependency
18-
}
19-
20-
// ErrorHandler provides configurable error handling with optional stacktrace capture.
21-
type ErrorHandler struct {
22-
stacktrace bool
23-
}
24-
25-
// Option is a function that configures an ErrorHandler.
26-
type Option func(*ErrorHandler)
27-
288
type rError struct {
299
err error
3010
ctx context.Context
@@ -54,157 +34,9 @@ func (e *rError) Unwrap() error {
5434
return e.err
5535
}
5636

57-
// getModulePath returns the main module path for determining if frames are in-app.
58-
func getModulePath() string {
59-
if bi, ok := debug.ReadBuildInfo(); ok {
60-
return bi.Main.Path
61-
}
62-
return ""
63-
}
64-
65-
// captureStacktrace captures the current call stack, excluding rogerr internal frames.
66-
func captureStacktrace(modulePath string) []Frame {
67-
const maxFrames = 64
68-
ptrs := [maxFrames]uintptr{}
69-
70-
// Skip 0 frames as we'll filter manually
71-
pcs := ptrs[0:runtime.Callers(0, ptrs[:])]
72-
73-
allFrames := make([]Frame, 0, len(pcs))
74-
iter := runtime.CallersFrames(pcs)
75-
76-
// First, collect all frames
77-
for {
78-
frame, more := iter.Next()
79-
80-
// Determine if this is application code
81-
inApp := isInApp(frame.Function, modulePath)
82-
83-
allFrames = append(allFrames, Frame{
84-
File: frame.File,
85-
Line: frame.Line,
86-
Function: frame.Function,
87-
InApp: inApp,
88-
})
89-
90-
if !more {
91-
break
92-
}
93-
}
94-
95-
// Now filter out rogerr frames, but keep everything after the last rogerr frame
96-
lastRogerrIndex := -1
97-
for i, frame := range allFrames {
98-
// Only filter out the main rogerr package, not internal modules
99-
if strings.HasPrefix(frame.Function, "github.com/kinbiko/rogerr.") {
100-
lastRogerrIndex = i
101-
}
102-
}
103-
104-
// Return frames after the last rogerr frame
105-
if lastRogerrIndex >= 0 && lastRogerrIndex+1 < len(allFrames) {
106-
return allFrames[lastRogerrIndex+1:]
107-
}
108-
109-
// If no rogerr frames found, return all frames (shouldn't happen)
110-
return allFrames
111-
}
112-
113-
// isInApp determines if a function belongs to the application or a dependency.
114-
func isInApp(function, modulePath string) bool {
115-
if modulePath == "" {
116-
return false
117-
}
118-
119-
// Special case for main.main
120-
if strings.Contains(function, "main.main") {
121-
return true
122-
}
123-
124-
// Check if function belongs to the main module
125-
if strings.HasPrefix(function, modulePath) {
126-
return true
127-
}
128-
129-
// Handle case where the binary is built from a module and function names
130-
// start with "main." - check if the module path contains the current module
131-
if strings.HasPrefix(function, "main.") {
132-
return true
133-
}
134-
135-
return false
136-
}
137-
138-
// WithStacktrace configures whether stacktraces should be captured.
139-
func WithStacktrace(enabled bool) Option {
140-
return func(h *ErrorHandler) {
141-
h.stacktrace = enabled
142-
}
143-
}
144-
145-
// NewErrorHandler creates a new ErrorHandler with the given options.
146-
// By default, stacktrace capture is enabled.
147-
func NewErrorHandler(opts ...Option) *ErrorHandler {
148-
h := &ErrorHandler{
149-
stacktrace: true, // stacktrace enabled by default
150-
}
151-
for _, opt := range opts {
152-
opt(h)
153-
}
154-
return h
155-
}
156-
157-
// Stacktrace extracts the stacktrace from an error if it was created with ErrorHandler.
158-
func (h *ErrorHandler) Stacktrace(err error) []Frame {
159-
rErr := &rError{}
160-
if errors.As(err, &rErr) {
161-
return rErr.stacktrace
162-
}
163-
return nil
164-
}
165-
166-
// Wrap attaches ctx data and wraps the given error with message, optionally capturing stacktrace.
167-
// ctx, err, and msgAndFmtArgs are all optional, but at least one must be given
168-
// for this function to return a non-nil error.
169-
// Any attached diagnostic data from this ctx will be preserved should you
170-
// pass the returned error further up the stack.
171-
func (h *ErrorHandler) Wrap(ctx context.Context, err error, msgAndFmtArgs ...interface{}) error {
172-
if ctx == nil && err == nil && msgAndFmtArgs == nil {
173-
return nil
174-
}
175-
176-
e := &rError{err: err, ctx: ctx}
177-
178-
if l := len(msgAndFmtArgs); l > 0 {
179-
if msg, ok := msgAndFmtArgs[0].(string); ok {
180-
e.msg = fmt.Sprintf(msg, msgAndFmtArgs[1:]...)
181-
}
182-
}
183-
184-
// Capture stacktrace if enabled
185-
if h.stacktrace {
186-
e.stacktrace = captureStacktrace(getModulePath())
187-
}
188-
189-
return e
190-
}
191-
192-
// Wrap attaches ctx data and wraps the given error with message.
193-
// ctx, err, and msgAndFmtArgs are all optional, but at least one must be given
194-
// for this function to return a non-nil error.
195-
// Any attached diagnostic data from this ctx will be preserved should you
196-
// pass the returned error further up the stack.
37+
// Wrap wraps errors with the default error handler settings.
38+
// See ErrorHandler.Wrap for more details.
19739
// Deprecated: Use ErrorHandler.Wrap instead.
198-
func Wrap(ctx context.Context, err error, msgAndFmtArgs ...interface{}) error {
199-
if ctx == nil && err == nil && msgAndFmtArgs == nil {
200-
return nil
201-
}
202-
e := &rError{err: err, ctx: ctx}
203-
204-
if l := len(msgAndFmtArgs); l > 0 {
205-
if msg, ok := msgAndFmtArgs[0].(string); ok {
206-
e.msg = fmt.Sprintf(msg, msgAndFmtArgs[1:]...)
207-
}
208-
}
209-
return e
40+
func Wrap(ctx context.Context, err error, msgAndFmtArgs ...any) error {
41+
return NewErrorHandler().Wrap(ctx, err, msgAndFmtArgs...)
21042
}

error_handler.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package rogerr
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
// ErrorHandler provides configurable error handling with optional stacktrace capture.
10+
type ErrorHandler struct {
11+
stacktrace bool
12+
}
13+
14+
// Option is a function that configures an ErrorHandler.
15+
type Option func(*ErrorHandler)
16+
17+
// WithStacktrace configures whether stacktraces should be captured.
18+
func WithStacktrace(enabled bool) Option {
19+
return func(h *ErrorHandler) {
20+
h.stacktrace = enabled
21+
}
22+
}
23+
24+
// NewErrorHandler creates a new ErrorHandler with the given options.
25+
// By default, stacktrace capture is enabled.
26+
func NewErrorHandler(opts ...Option) *ErrorHandler {
27+
h := &ErrorHandler{
28+
stacktrace: true, // stacktrace enabled by default
29+
}
30+
for _, opt := range opts {
31+
opt(h)
32+
}
33+
return h
34+
}
35+
36+
// Stacktrace extracts the stacktrace from an error if it was created with ErrorHandler.
37+
func (h *ErrorHandler) Stacktrace(err error) []Frame {
38+
rErr := &rError{}
39+
if errors.As(err, &rErr) {
40+
return rErr.stacktrace
41+
}
42+
return nil
43+
}
44+
45+
// Wrap attaches ctx data and wraps the given error with message, optionally capturing stacktrace.
46+
// ctx, err, and msgAndFmtArgs are all optional, but at least one must be given
47+
// for this function to return a non-nil error.
48+
// Any attached diagnostic data from this ctx will be preserved should you
49+
// pass the returned error further up the stack.
50+
func (h *ErrorHandler) Wrap(ctx context.Context, err error, msgAndFmtArgs ...interface{}) error {
51+
if ctx == nil && err == nil && msgAndFmtArgs == nil {
52+
return nil
53+
}
54+
e := &rError{err: err, ctx: ctx}
55+
56+
if l := len(msgAndFmtArgs); l > 0 {
57+
if msg, ok := msgAndFmtArgs[0].(string); ok {
58+
e.msg = fmt.Sprintf(msg, msgAndFmtArgs[1:]...)
59+
}
60+
}
61+
if h.stacktrace {
62+
e.stacktrace = captureStacktrace(getModulePath())
63+
}
64+
65+
return e
66+
}

error_handler_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package rogerr
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestNewErrorHandler(t *testing.T) {
9+
t.Run("default configuration", func(t *testing.T) {
10+
if !NewErrorHandler().stacktrace {
11+
t.Error("expected stacktrace to be enabled by default")
12+
}
13+
})
14+
15+
t.Run("with stacktrace enabled", func(t *testing.T) {
16+
if !NewErrorHandler(WithStacktrace(true)).stacktrace {
17+
t.Error("expected stacktrace to be enabled")
18+
}
19+
})
20+
21+
t.Run("with stacktrace disabled", func(t *testing.T) {
22+
if NewErrorHandler(WithStacktrace(false)).stacktrace {
23+
t.Error("expected stacktrace to be disabled")
24+
}
25+
})
26+
}
27+
28+
func TestErrorHandlerWrap(t *testing.T) {
29+
handler := NewErrorHandler()
30+
ctx := t.Context()
31+
32+
t.Run("basic error wrapping with stacktrace enabled", func(t *testing.T) {
33+
baseErr := errors.New("base error")
34+
35+
err := handler.Wrap(ctx, baseErr, "wrapped error")
36+
if err == nil {
37+
t.Fatal("expected non-nil error")
38+
}
39+
40+
rErr := err.(*rError)
41+
if rErr.err != baseErr {
42+
t.Error("expected wrapped error to contain base error")
43+
}
44+
if rErr.ctx != ctx {
45+
t.Error("expected wrapped error to contain context")
46+
}
47+
if rErr.msg != "wrapped error" {
48+
t.Errorf("expected message 'wrapped error', got '%s'", rErr.msg)
49+
}
50+
if len(rErr.stacktrace) == 0 {
51+
t.Error("expected stacktrace to be captured when enabled")
52+
}
53+
})
54+
55+
t.Run("error wrapping with stacktrace disabled", func(t *testing.T) {
56+
baseErr := errors.New("base error")
57+
err := NewErrorHandler(WithStacktrace(false)).Wrap(ctx, baseErr, "wrapped error")
58+
if err == nil {
59+
t.Fatal("expected non-nil error")
60+
}
61+
62+
rErr := err.(*rError)
63+
if len(rErr.stacktrace) != 0 {
64+
t.Error("expected no stacktrace when disabled")
65+
}
66+
})
67+
68+
t.Run("nil inputs return nil", func(t *testing.T) {
69+
err := handler.Wrap(t.Context(), nil)
70+
if err == nil {
71+
t.Error("expected non-nil error when context is provided")
72+
}
73+
})
74+
75+
t.Run("message formatting works", func(t *testing.T) {
76+
err := handler.Wrap(t.Context(), nil, "user %d failed: %s", 123, "timeout")
77+
78+
if err == nil {
79+
t.Fatal("expected non-nil error")
80+
}
81+
82+
expected := "user 123 failed: timeout"
83+
if err.Error() != expected {
84+
t.Errorf("expected '%s', got '%s'", expected, err.Error())
85+
}
86+
})
87+
}
88+
89+
func TestErrorHandlerStacktrace(t *testing.T) {
90+
handler := NewErrorHandler()
91+
t.Run("extract stacktrace from error", func(t *testing.T) {
92+
if len(handler.Stacktrace(handler.Wrap(t.Context(), nil, "test error"))) == 0 {
93+
t.Error("expected stacktrace frames to be extracted")
94+
}
95+
})
96+
97+
t.Run("return nil for non-rogerr error", func(t *testing.T) {
98+
if handler.Stacktrace(errors.New("regular error")) != nil {
99+
t.Error("expected nil stacktrace for non-rogerr error")
100+
}
101+
})
102+
103+
t.Run("return nil for nil error", func(t *testing.T) {
104+
if handler.Stacktrace(nil) != nil {
105+
t.Error("expected nil stacktrace for nil error")
106+
}
107+
})
108+
}

0 commit comments

Comments
 (0)