mediocre-go-lib/merr/merr.go

211 lines
5.2 KiB
Go
Raw Permalink Normal View History

// 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 {
2019-02-27 17:18:56 +00:00
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
}
2019-02-27 17:18:56 +00:00
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...)
}
2019-02-27 17:18:56 +00:00
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.
//
2019-02-27 17:18:56 +00:00
// 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()
}
2019-02-27 17:18:56 +00:00
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
2019-02-27 17:18:56 +00:00
// 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)
}