211 lines
5.2 KiB
Go
211 lines
5.2 KiB
Go
// Package merr extends the errors package with features like key-value
|
|
// attributes for errors, embedded stacktraces, and multi-errors.
|
|
//
|
|
// merr functions takes in generic errors of the built-in type. The returned
|
|
// errors are wrapped by a type internal to merr, and appear to also be of the
|
|
// generic error type. This means that equality checking will not work, unless
|
|
// the Base function is used. If any functions are given nil they will also
|
|
// return nil.
|
|
package merr
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/mediocregopher/mediocre-go-lib/mctx"
|
|
)
|
|
|
|
var strBuilderPool = sync.Pool{
|
|
New: func() interface{} { return new(strings.Builder) },
|
|
}
|
|
|
|
func putStrBuilder(sb *strings.Builder) {
|
|
sb.Reset()
|
|
strBuilderPool.Put(sb)
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type err struct {
|
|
err error
|
|
attr map[interface{}]interface{}
|
|
}
|
|
|
|
// attr keys internal to this package
|
|
type attrKey int
|
|
|
|
const (
|
|
attrKeyCtx attrKey = iota
|
|
)
|
|
|
|
func wrap(e error, cp bool) *err {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
|
|
er, ok := e.(*err)
|
|
if !ok {
|
|
return &err{
|
|
err: e,
|
|
attr: make(map[interface{}]interface{}, 1),
|
|
}
|
|
} else if !cp {
|
|
return er
|
|
}
|
|
|
|
er2 := &err{
|
|
err: er.err,
|
|
attr: make(map[interface{}]interface{}, len(er.attr)+1),
|
|
}
|
|
for k, v := range er.attr {
|
|
er2.attr[k] = v
|
|
}
|
|
|
|
return er2
|
|
}
|
|
|
|
// Base takes in an error and checks if it is merr's internal error type. If it
|
|
// is then the underlying error which is being wrapped is returned. If it's not
|
|
// then the passed in error is returned as-is.
|
|
func Base(e error) error {
|
|
if er, ok := e.(*err); ok {
|
|
return er.err
|
|
}
|
|
return e
|
|
}
|
|
|
|
// WithValue returns a copy of the original error, automatically wrapping it if
|
|
// the error is not from merr (see Wrap). The returned error has an attribute
|
|
// value set on it for the given key.
|
|
func WithValue(e error, k, v interface{}) error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
er := wrap(e, true)
|
|
if er.attr == nil {
|
|
er.attr = map[interface{}]interface{}{}
|
|
}
|
|
er.attr[k] = v
|
|
return er
|
|
}
|
|
|
|
// Value returns the value embedded in the error for the given key, or nil if
|
|
// the error isn't from this package or doesn't have that key embedded.
|
|
func Value(e error, k interface{}) interface{} {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return wrap(e, false).attr[k]
|
|
}
|
|
|
|
// WrapSkip is like Wrap but also allows for skipping extra stack frames when
|
|
// embedding the stack into the error.
|
|
func WrapSkip(e error, skip int, ctxs ...context.Context) error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
|
|
er := wrap(e, true)
|
|
if _, ok := getStack(er); !ok {
|
|
setStack(er, skip+1)
|
|
}
|
|
|
|
ctx, _ := er.attr[attrKeyCtx].(context.Context)
|
|
if ctx != nil {
|
|
ctx = mctx.MergeAnnotationsInto(ctx, ctxs...)
|
|
} else if len(ctxs) > 0 {
|
|
ctx = mctx.MergeAnnotations(ctxs...)
|
|
}
|
|
|
|
er.attr[attrKeyCtx] = ctx
|
|
return er
|
|
}
|
|
|
|
// Wrap returns a copy of the given error wrapped in merr's inner type. It will
|
|
// perform an mctx.MergeAnnotations on the given Contexts to create a new
|
|
// Context, and embed that in the returned error. If the given error already has
|
|
// an embedded Context then ctxs will be merged into that.
|
|
//
|
|
// This function automatically embeds stack information into the error as it's
|
|
// being stored, using WithStack, unless the error already has stack information
|
|
// in it.
|
|
//
|
|
// Wrapping nil returns nil.
|
|
func Wrap(e error, ctx ...context.Context) error {
|
|
return WrapSkip(e, 1, ctx...)
|
|
}
|
|
|
|
// New is a shortcut for:
|
|
// merr.Wrap(errors.New(str), ctxs...)
|
|
func New(str string, ctxs ...context.Context) error {
|
|
return WrapSkip(errors.New(str), 1, ctxs...)
|
|
}
|
|
|
|
type annotateKey string
|
|
|
|
func ctx(e error) context.Context {
|
|
ctx, _ := Value(e, attrKeyCtx).(context.Context)
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
if stack, ok := Stack(e); ok {
|
|
ctx = mctx.Annotate(ctx, annotateKey("errLoc"), stack.String())
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
// Context returns the Context embedded in this error from the last call to Wrap
|
|
// or New. If none is embedded this uses context.Background().
|
|
//
|
|
// The returned Context will have annotated on it (see mctx.Annotate) the
|
|
// underlying error's string (as returned by Error()) and the error's stack
|
|
// location. Stack locations are automatically added by New and Wrap via
|
|
// WithStack.
|
|
//
|
|
// If this error is nil this returns context.Background().
|
|
func Context(e error) context.Context {
|
|
if e == nil {
|
|
return context.Background()
|
|
}
|
|
ctx := ctx(e)
|
|
ctx = mctx.Annotate(ctx, annotateKey("err"), Base(e).Error())
|
|
return ctx
|
|
}
|
|
|
|
func (er *err) Error() string {
|
|
ctx := ctx(er)
|
|
|
|
sb := strBuilderPool.Get().(*strings.Builder)
|
|
defer putStrBuilder(sb)
|
|
sb.WriteString(strings.TrimSpace(er.err.Error()))
|
|
|
|
annotations := mctx.Annotations(ctx).StringSlice(true)
|
|
for _, kve := range annotations {
|
|
k, v := strings.TrimSpace(kve[0]), strings.TrimSpace(kve[1])
|
|
sb.WriteString("\n\t* ")
|
|
sb.WriteString(k)
|
|
sb.WriteString(": ")
|
|
|
|
// if there's no newlines then print v inline with k
|
|
if strings.Index(v, "\n") < 0 {
|
|
sb.WriteString(v)
|
|
continue
|
|
}
|
|
|
|
for _, vLine := range strings.Split(v, "\n") {
|
|
sb.WriteString("\n\t\t")
|
|
sb.WriteString(strings.TrimSpace(vLine))
|
|
}
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// Equal is a shortcut for Base(e1) == Base(e2).
|
|
func Equal(e1, e2 error) bool {
|
|
return Base(e1) == Base(e2)
|
|
}
|