gotry/try/try.go
Theta-Dev 34a01c6279
All checks were successful
continuous-integration/drone/push Build is passing
Pass through try-compatible errors
2021-10-17 10:59:33 +02:00

290 lines
6.8 KiB
Go

package try
/*
try is a fork of lainio's err2 package
Copyright (c) 2019 Lainio, MIT License
https://github.com/lainio/err2
*/
import (
"errors"
"fmt"
"os"
"regexp"
"runtime"
"strings"
)
// Err is an interface for an error with extended stack info.
type Err interface {
Error() string
Unwrap() error
CallStack() []call
CallStackString() string
PrintCallStack()
GetData() *tryErrData
Annotate(msg string)
}
// tryErr is an extended error struct.
type tryErr struct {
err error
annotations []string
callStack []call
}
// tryErrData is exported error data. Can be converted to JSON.
type tryErrData struct {
Msg string `json:"msg"`
Annotations []string `json:"annotations"`
CallStack []call `json:"call_stack"`
}
type call struct {
File string `json:"file"`
Function string `json:"fn"`
Line int `json:"l"`
}
// FromErr wraps a standard error into a try-compatible one with extended stack info.
func FromErr(err error) Err {
// If the error is already try-compatible, return
//nolint:errorlint
cterr, ok := err.(Err)
if ok {
return cterr
}
terr := &tryErr{err: err, callStack: []call{}}
pc, _, _, ok := runtime.Caller(0)
if !ok {
return terr
}
thisFnc := runtime.FuncForPC(pc)
if thisFnc == nil {
return terr
}
rexp := regexp.MustCompile(`.+\/`)
thisPkg := rexp.FindString(thisFnc.Name())
for i := 1; ; i++ {
pc, f, l, ok := runtime.Caller(i)
if !ok {
break
}
fnc := runtime.FuncForPC(pc).Name()
if strings.HasPrefix(fnc, thisPkg) && !strings.HasSuffix(f, "_test.go") {
continue
}
terr.callStack = append(terr.callStack, call{File: f, Function: fnc, Line: l})
}
return terr
}
// NewErr creates a new try-compatible error with extended stack info.
func NewErr(msg string) Err {
return FromErr(errors.New(msg))
}
// Error returns the standard go error message (initial message + annotations).
func (e *tryErr) Error() string {
msgs := e.annotations
msgs = append(msgs, e.err.Error())
return strings.Join(msgs, ": ")
}
// Unwrap returns the standard go error.
func (e *tryErr) Unwrap() error {
return e.err
}
// CallStack returns a slice of function calls leading to the error,
// with the most recent being first.
func (e *tryErr) CallStack() []call {
return e.callStack
}
// CallStackString outputs the call stack as a printable multiline string.
func (e *tryErr) CallStackString() string {
res := fmt.Sprintf("ERROR: %s\n", e.err.Error())
for _, msg := range e.annotations {
res += fmt.Sprintln(msg)
}
res += fmt.Sprintln("\nTraceback (most recent call first):")
for _, c := range e.callStack {
res += fmt.Sprintf("%s:%d\n", c.File, c.Line)
res += fmt.Sprintf(" %s\n", c.Function)
}
return res
}
// PrintCallStack prints the call stack to stderr.
func (e *tryErr) PrintCallStack() {
_, _ = fmt.Fprintln(os.Stderr, e.CallStackString())
}
// GetData returns a serializable error data object.
func (e *tryErr) GetData() *tryErrData {
return &tryErrData{
Msg: e.err.Error(),
CallStack: e.callStack,
Annotations: e.annotations,
}
}
// Annotate adds additional text to the error message.
func (e *tryErr) Annotate(msg string) {
e.annotations = append(e.annotations, msg)
}
// Empty is a helper method to handle errors of func() (string, error) functions.
func Empty(_ interface{}, err error) {
Check(err)
}
// Any is as similar as proposed Go2 Try macro, but it's a function and it
// returns slice of interfaces. It has quite big performance penalty when
// compared to Check function.
func Any(args ...interface{}) []interface{} {
check(args)
return args
}
// Check performs the error check for the given argument. If the err is nil,
// it does nothing. According the measurements, it's as fast as if err != nil
// {return err} on happy path.
func Check(err error) {
if err != nil {
panic(FromErr(err))
}
}
// Checks the error status of the last argument. It panics with "wrong
// signature" if the last calling parameter is not error. In case of error it
// delivers it by panicking.
func check(args []interface{}) {
argCount := len(args)
last := argCount - 1
if args[last] != nil {
err, ok := args[last].(error)
if !ok {
panic("wrong signature")
}
panic(FromErr(err))
}
}
// Handle is for adding an error handler to a function by defer. It's for
// functions returning errors them self. For those functions that doesn't
// return errors there is a Catch function. Note! The handler function f is
// called only when err != nil.
func Handle(err *Err, f func()) {
// We put real panic objects back and keep only those which are
// carrying our errors. We must also call all of the handlers in defer
// stack.
switch r := recover(); r.(type) {
case nil:
// Defers are in the stack and the first from the stack gets the
// opportunity to get panic object's error (below). We still must
// call handler functions to the rest of the handlers if there is
// an error.
if *err != nil {
f()
}
case Err:
// We or someone did transport this error thru panic.
*err = r.(Err)
f()
default:
panic(r)
}
}
// Catch is a convenient helper to those functions that doesn't return errors.
// Go's main function is a good example. Note! There can be only one deferred
// Catch function per non error returning function. See Handle for more
// information.
func Catch(f func(err Err)) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r)
}
f(e)
}
}
// CatchAll is a helper function to catch and write handlers for all errors and
// all panics thrown in the current go routine.
func CatchAll(errorHandler func(err Err), panicHandler func(v interface{})) {
if r := recover(); r != nil {
e, ok := r.(Err)
if ok {
errorHandler(e)
} else {
panicHandler(r)
}
}
}
// CatchTrace catches all errors and prints their call stack.
// Setting the exit parameter to a value above 0 will make the program
// exit in case of an error.
func CatchTrace(exit int) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r)
}
e.PrintCallStack()
if exit > 0 {
os.Exit(exit)
}
}
}
// Return is same as Handle but it's for functions which don't wrap or annotate
// their errors. If you want to annotate errors see Annotate for more
// information.
func Return(err *Err) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r) // Not ours, carry on panicking
}
*err = e
}
}
// ReturnStd is like Return, but it returns the Go standard error type.
func ReturnStd(err *error) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r) // Not ours, carry on panicking
}
*err = e.Unwrap()
}
}
// Annotate adds additional messages to the error.
func Annotate(err *Err, msg string) {
if r := recover(); r != nil {
e, ok := r.(Err)
if !ok {
panic(r) // Not ours, carry on panicking
}
*err = e
e.Annotate(msg)
} else if *err != nil { // if other handlers call recovery() we still..
(*err).Annotate(msg)
}
}