Skip to content

Commit f3eca56

Browse files
committed
initial state
1 parent 8f01a6e commit f3eca56

29 files changed

+2758
-0
lines changed

builder.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package errorx
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
)
7+
8+
// Utility to compose an error from type
9+
// Typically, a direct usage is not required: either Type methods of helpers like Decorate is sufficient
10+
// Only use builder if no neater alternative is available
11+
type ErrorBuilder struct {
12+
errorType *Type
13+
message string
14+
cause error
15+
underlying []error
16+
mode callStackBuildMode
17+
isTransparent bool
18+
}
19+
20+
// Created error builder from an existing error type
21+
func NewErrorBuilder(t *Type) *ErrorBuilder {
22+
getMode := func() callStackBuildMode {
23+
if t.modifiers.CollectStackTrace() {
24+
return stackTraceCollect
25+
} else {
26+
return stackTraceOmit
27+
}
28+
}
29+
30+
return &ErrorBuilder{
31+
errorType: t,
32+
mode: getMode(),
33+
isTransparent: t.modifiers.Transparent(),
34+
}
35+
}
36+
37+
// Provide an original cause for error
38+
// For non-errorx errors, a stack trace is collected
39+
// Otherwise, it is inherited by default, as error wrapping is typically performed 'en passe'
40+
// Note that even if an original error explicitly omitted the stack trace, it could be added on wrap
41+
func (eb *ErrorBuilder) WithCause(err error) *ErrorBuilder {
42+
eb.cause = err
43+
if Cast(err) != nil {
44+
eb.mode = stackTraceBorrow
45+
}
46+
47+
return eb
48+
}
49+
50+
// Transparent wrap hides the current error type from the type checks and exposes the error type of the cause instead
51+
// The same holds true for traits, and the dynamic properties are visible from both cause and transparent wrapper
52+
// Note that if the cause error is non-errorx, transparency will still hold, type check against wrapper will still fail
53+
func (eb *ErrorBuilder) Transparent() *ErrorBuilder {
54+
if eb.cause == nil {
55+
panic("wrong builder usage: wrap modifier without non-nil cause")
56+
}
57+
58+
eb.isTransparent = true
59+
return eb
60+
}
61+
62+
// Collect the current stack trace along with the original one, and use both in formatting
63+
// If the original error does not hold a stack trace for whatever reason, it will be collected it this point
64+
// This is typically a way to handle an error received from another goroutine - say, a worker pool
65+
// When stack traces overlap, formatting makes a conservative attempt not to repeat itself,
66+
// preserving the *original* stack trace in its entirety
67+
func (eb *ErrorBuilder) EnhanceStackTrace() *ErrorBuilder {
68+
if eb.cause == nil {
69+
panic("wrong builder usage: wrap modifier without non-nil cause")
70+
}
71+
72+
if Cast(eb.cause) != nil {
73+
eb.mode = stackTraceEnhance
74+
} else {
75+
eb.mode = stackTraceCollect
76+
}
77+
78+
return eb
79+
}
80+
81+
// Adds multiple additional (hidden, suppressed) related errors to be used exclusively in error output
82+
// Note that these errors make no other effect whatsoever: their traits, types, properties etc. are lost
83+
func (eb *ErrorBuilder) withUnderlyingErrors(errs ...error) *ErrorBuilder {
84+
eb.underlying = append(eb.underlying, errs...)
85+
return eb
86+
}
87+
88+
// Provides a message for an error in flexible format, to simplify its usages
89+
// Without args, leaves the original message intact, so a message may be generated or provided externally
90+
// With args, a formatting is performed, and it is therefore expected a format string to be constant
91+
func (eb *ErrorBuilder) WithConditionallyFormattedMessage(message string, args ...interface{}) *ErrorBuilder {
92+
if len(args) == 0 {
93+
eb.message = message
94+
} else {
95+
eb.message = fmt.Sprintf(message, args...)
96+
}
97+
98+
return eb
99+
}
100+
101+
// Returns an error with specified params
102+
func (eb *ErrorBuilder) Create() *Error {
103+
return &Error{
104+
errorType: eb.errorType,
105+
message: eb.message,
106+
cause: eb.cause,
107+
underlying: eb.underlying,
108+
transparent: eb.isTransparent,
109+
stackTrace: eb.assembleStackTrace(),
110+
}
111+
}
112+
113+
type callStackBuildMode int
114+
115+
const (
116+
stackTraceCollect callStackBuildMode = 1
117+
stackTraceBorrow callStackBuildMode = 2
118+
stackTraceEnhance callStackBuildMode = 3
119+
stackTraceOmit callStackBuildMode = 4
120+
)
121+
122+
func (eb *ErrorBuilder) assembleStackTrace() *stackTrace {
123+
switch eb.mode {
124+
case stackTraceCollect:
125+
return eb.collectOriginalStackTrace()
126+
case stackTraceBorrow:
127+
return eb.borrowStackTraceFromCause()
128+
case stackTraceEnhance:
129+
return eb.combineStackTraceWithCause()
130+
case stackTraceOmit:
131+
return nil
132+
default:
133+
panic("unknown mode " + strconv.Itoa(int(eb.mode)))
134+
}
135+
}
136+
137+
func (eb *ErrorBuilder) collectOriginalStackTrace() *stackTrace {
138+
return collectStackTrace()
139+
}
140+
141+
func (eb *ErrorBuilder) borrowStackTraceFromCause() *stackTrace {
142+
originalStackTrace := eb.extractStackTraceFromCause(eb.cause)
143+
if originalStackTrace != nil {
144+
return originalStackTrace
145+
} else {
146+
return collectStackTrace()
147+
}
148+
}
149+
150+
func (eb *ErrorBuilder) combineStackTraceWithCause() *stackTrace {
151+
currentStackTrace := collectStackTrace()
152+
153+
originalStackTrace := eb.extractStackTraceFromCause(eb.cause)
154+
if originalStackTrace != nil {
155+
currentStackTrace.enhanceWithCause(originalStackTrace)
156+
}
157+
158+
return currentStackTrace
159+
}
160+
161+
func (eb *ErrorBuilder) extractStackTraceFromCause(cause error) *stackTrace {
162+
if typedCause := Cast(cause); typedCause != nil {
163+
return typedCause.stackTrace
164+
}
165+
166+
return nil
167+
}

builder_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package errorx
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestBuilderTransparency(t *testing.T) {
11+
t.Run("Raw", func(t *testing.T) {
12+
err := NewErrorBuilder(testType).WithCause(errors.New("bad thing")).Transparent().Create()
13+
require.False(t, err.IsOfType(testType))
14+
require.NotEqual(t, testType, err.Type())
15+
})
16+
17+
t.Run("RawWithModifier", func(t *testing.T) {
18+
err := NewErrorBuilder(testTypeTransparent).WithCause(errors.New("bad thing")).Create()
19+
require.False(t, err.IsOfType(testType))
20+
require.NotEqual(t, testType, err.Type())
21+
})
22+
}

common.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package errorx
2+
3+
var (
4+
// General purpose errors to be used universally
5+
// These errors should typically be used in opaque manner, implying no handing in user code
6+
// When handling is required, it is best to use custom error types with both standard and custom traits
7+
CommonErrors = NewNamespace("common")
8+
9+
IllegalArgument = CommonErrors.NewType("illegal_argument")
10+
IllegalState = CommonErrors.NewType("illegal_state")
11+
IllegalFormat = CommonErrors.NewType("illegal_format")
12+
InitializationFailed = CommonErrors.NewType("initialization_failed")
13+
DataUnavailable = CommonErrors.NewType("data_unavailable")
14+
UnsupportedOperation = CommonErrors.NewType("unsupported_operation")
15+
RejectedOperation = CommonErrors.NewType("rejected_operation")
16+
Interrupted = CommonErrors.NewType("interrupted")
17+
AssertionFailed = CommonErrors.NewType("assertion_failed")
18+
InternalError = CommonErrors.NewType("internal_error")
19+
ExternalError = CommonErrors.NewType("external_error")
20+
ConcurrentUpdate = CommonErrors.NewType("concurrent_update")
21+
TimeoutElapsed = CommonErrors.NewType("timeout", Timeout())
22+
NotImplemented = UnsupportedOperation.NewSubtype("not_implemented")
23+
UnsupportedVersion = UnsupportedOperation.NewSubtype("version")
24+
)

0 commit comments

Comments
 (0)