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