mediocre-go-lib/merr/merr.go

171 lines
3.7 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 (
"errors"
"fmt"
"sort"
"strings"
"sync"
)
var strBuilderPool = sync.Pool{
New: func() interface{} { return new(strings.Builder) },
}
func putStrBuilder(sb *strings.Builder) {
sb.Reset()
strBuilderPool.Put(sb)
}
////////////////////////////////////////////////////////////////////////////////
type val struct {
visible bool
val interface{}
}
func (v val) String() string {
return fmt.Sprint(v.val)
}
type err struct {
err error
attr map[interface{}]val
}
// attr keys internal to this package
type attrKey string
func wrap(e error, cp bool, skip int) *err {
if e == nil {
return nil
}
er, ok := e.(*err)
if !ok {
er := &err{err: e, attr: map[interface{}]val{}}
if skip >= 0 {
setStack(er, skip+1)
}
return er
} else if !cp {
return er
}
er2 := &err{
err: er.err,
attr: make(map[interface{}]val, len(er.attr)),
}
for k, v := range er.attr {
er2.attr[k] = v
}
if _, ok := er2.attr[attrKeyStack]; !ok && skip >= 0 {
setStack(er, skip+1)
}
return er2
}
// Wrap takes in an error and returns one wrapping it in merr's inner type,
// which embeds information like the stack trace.
func Wrap(e error) error {
return wrap(e, false, 1)
}
// New returns a new error with the given string as its error string. New
// automatically wraps the error in merr's inner type, which embeds information
// like the stack trace.
//
// For convenience, visible key/values may be passed into New at this point. For
// example, the following two are equivalent:
//
// merr.WithValue(merr.New("foo"), "bar", "baz", true)
// merr.New("foo", "bar", "baz")
//
func New(str string, kvs ...interface{}) error {
if len(kvs)%2 != 0 {
panic("key passed in without corresponding value")
}
err := wrap(errors.New(str), false, 1)
for i := 0; i < len(kvs); i += 2 {
err.attr[kvs[i]] = val{
visible: true,
val: kvs[i+1],
}
}
return err
}
func (er *err) visibleAttrs() [][2]string {
out := make([][2]string, 0, len(er.attr))
for k, v := range er.attr {
if !v.visible {
continue
}
out = append(out, [2]string{
strings.Trim(fmt.Sprintf("%q", k), `"`),
fmt.Sprint(v.val),
})
}
sort.Slice(out, func(i, j int) bool {
return out[i][0] < out[j][0]
})
return out
}
func (er *err) Error() string {
visAttrs := er.visibleAttrs()
if len(visAttrs) == 0 {
return er.err.Error()
}
sb := strBuilderPool.Get().(*strings.Builder)
defer putStrBuilder(sb)
sb.WriteString(strings.TrimSpace(er.err.Error()))
for _, attr := range visAttrs {
k, v := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[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()
}
// 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
}
// Equal is a shortcut for Base(e1) == Base(e2).
func Equal(e1, e2 error) bool {
return Base(e1) == Base(e2)
}