initial commit

This commit is contained in:
Timon Ringwald 2020-09-09 11:48:46 +02:00
commit 4e5775fcfa
8 changed files with 292 additions and 0 deletions

46
calltrace.go Normal file
View File

@ -0,0 +1,46 @@
package adverr
import (
"runtime"
"strconv"
"strings"
)
// CallTrace represents a call stack trace similar to Java's stack trace
type CallTrace struct {
frames *runtime.Frames
}
// Trace returns a new CallTrace starting from this call
// Use skip to skip the first entries in the trace
func Trace(skip int) *CallTrace {
if !TraceCallStack {
return nil
}
pc := make([]uintptr, CallStackLength)
n := runtime.Callers(skip+1, pc)
pc = pc[:n]
return &CallTrace{runtime.CallersFrames(pc)}
}
func (ct *CallTrace) String() string {
if ct == nil {
return ""
}
b := new(strings.Builder)
for frame, ok := ct.frames.Next(); ok; frame, ok = ct.frames.Next() {
b.WriteString("\tat ")
b.WriteString(frame.Function)
b.WriteString(" (")
b.WriteString(frame.File)
b.WriteString(":")
b.WriteString(strconv.Itoa(frame.Line))
b.WriteString(")")
b.WriteString("\n")
}
return b.String()
}

34
doc.go Normal file
View File

@ -0,0 +1,34 @@
/*
Package adverr implements errors with call stack traces
as well as error templates for error equality
Usage examples
Creating templates:
var (
ErrDoStuffFailed = adverr.NewErrTmpl("ErrDoStuffFailed", "Could'nt do stuff because of %s")
)
Creating independent error (without error template):
func doStuffWithIndependentErr() error {
return adverr.New("Could'nt do stuff")
}
Creating error based on template:
func doStuff() error {
return ErrDoStuffFailed.New("reasons")
}
Printing errors on stderr convieniently:
Print(myErr)
Println(myErr)
Printing errors on stderr and exit with exitcode:
Fatal(myErr, 1)
Fatalln(myErr, 1)
Advantages of error templates
two errors made by the same template will return true when called with errors.Is()
*/
package adverr

104
error.go Normal file
View File

@ -0,0 +1,104 @@
package adverr
import (
"errors"
"reflect"
"strings"
)
// Error is a wrapper for error with stack trace
type Error struct {
msg string
callTrace *CallTrace
tmpl *ErrTmpl
cause error
}
// New returns a new Error with the given message
func New(msg string) *Error {
return &Error{
msg: msg,
cause: nil,
tmpl: nil,
callTrace: Trace(2),
}
}
// Wrap returns a new Error with the given message which is caused by cause
func Wrap(msg string, cause error) *Error {
return &Error{
msg: msg,
cause: cause,
callTrace: Trace(2),
}
}
func errtype(err error) string {
if e, ok := err.(*Error); ok && e.tmpl != nil {
return errtype(e.tmpl)
} else if tmpl, ok := err.(*ErrTmpl); ok {
t := reflect.TypeOf(tmpl)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t.PkgPath() + "." + tmpl.name
}
t := reflect.TypeOf(err)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t.PkgPath() + "." + t.Name()
}
func (e *Error) Unwrap() error {
return e.cause
}
func (e *Error) Error() string {
b := new(strings.Builder)
printErr(e, b)
return b.String()
}
// Is implements the error equality function used by errors.Is()
// It returns true if the error is the same instance or is created using the same ErrTmpl
func (e *Error) Is(target error) bool {
// same error instance
if target == e {
return true
}
// no template used, therefore no equality possible
if e.tmpl == nil {
return false
}
// same template, therefore errors are equal to another
if tErr, ok := target.(*Error); ok {
return tErr.tmpl == e.tmpl
}
return false
}
func printErr(err error, b *strings.Builder) {
if e, ok := err.(*Error); ok {
b.WriteString(errtype(e))
b.WriteString(": ")
b.WriteString(e.msg)
b.WriteString("\n")
b.WriteString(e.callTrace.String())
} else {
b.WriteString(errtype(err))
b.WriteString(": ")
b.WriteString(err.Error())
b.WriteString("\n")
}
cause := errors.Unwrap(err)
if cause != nil {
b.WriteString("Caused by ")
printErr(cause, b)
}
}

17
error_test.go Normal file
View File

@ -0,0 +1,17 @@
package adverr
import (
"errors"
"fmt"
"testing"
)
func TestErr(t *testing.T) {
TraceCallStack = false
err := doStuff()
fmt.Println(err)
}
func doStuff() error {
return Wrap("wrapped error", Wrap("test error", fmt.Errorf("asd: %w", errors.New("test"))))
}

35
error_tmpl.go Normal file
View File

@ -0,0 +1,35 @@
package adverr
import "fmt"
var (
// ErrTmplUsedAsErr is returned from ErrTmpl.Error() because an error template should never be used as an actual error value
ErrTmplUsedAsErr = NewErrTmpl("ErrTmplUsedAsErr", "Error template used as error value: %s")
)
// ErrTmpl is an error template to define equalities between different errors
type ErrTmpl struct {
name string
format string
}
// NewErrTmpl returns a new error template with the given format string as a predefined error message
func NewErrTmpl(name, format string) *ErrTmpl {
return &ErrTmpl{name, format}
}
// Error implementation just for satisfying the error interface
// Please dont use ErrTmpls as actual errors
func (t *ErrTmpl) Error() string {
return ErrTmplUsedAsErr.New(errtype(t)).Error()
}
// New returns a new Error in which the given values are being formatted into the format string of its template
func (t *ErrTmpl) New(args ...interface{}) *Error {
return &Error{
msg: fmt.Sprintf(t.format, args...),
cause: nil,
tmpl: t,
callTrace: Trace(2),
}
}

19
error_tmpl_test.go Normal file
View File

@ -0,0 +1,19 @@
package adverr
import (
"fmt"
"testing"
)
var (
ErrDoStuffFailed = NewErrTmpl("ErrDoStuffFailed", "test error: %s")
)
func TestErrTmpl(t *testing.T) {
err := doTemplateStuff()
fmt.Println(err)
}
func doTemplateStuff() error {
return ErrDoStuffFailed.New("because of reasons")
}

9
globals.go Normal file
View File

@ -0,0 +1,9 @@
package adverr
// TraceCallStack decides if any call stack traces will be gathered when creating Errors
// If your application is doing performance-heavy tasks with lots of Error creations, you may consider setting this to false
// If set to false, all CallTraces in Error will be nil
var TraceCallStack bool = true
// CallStackLength decides how many calls from the call stack should be gathered at most
var CallStackLength int = 100

28
utils.go Normal file
View File

@ -0,0 +1,28 @@
package adverr
import (
"fmt"
"os"
)
// Println prints the given err to stderr followed by a newline
func Println(err error) {
fmt.Fprintln(os.Stderr, err)
}
// Print prints the given err to stderr
func Print(err error) {
fmt.Fprint(os.Stderr, err)
}
// Fatalln prints the given err to stderr followed by a newline and exits immediately with the given exit code
func Fatalln(err error, exitcode int) {
fmt.Fprintln(os.Stderr, err)
os.Exit(exitcode)
}
// Fatal prints the given err to stderr and exits immediately with the given exit code
func Fatal(err error, exitcode int) {
fmt.Fprint(os.Stderr, err)
os.Exit(exitcode)
}