From 4e5775fcfabc19fe37a3d774a2c529b6ae544c5c Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Wed, 9 Sep 2020 11:48:46 +0200 Subject: [PATCH] initial commit --- calltrace.go | 46 ++++++++++++++++++++ doc.go | 34 +++++++++++++++ error.go | 104 +++++++++++++++++++++++++++++++++++++++++++++ error_test.go | 17 ++++++++ error_tmpl.go | 35 +++++++++++++++ error_tmpl_test.go | 19 +++++++++ globals.go | 9 ++++ utils.go | 28 ++++++++++++ 8 files changed, 292 insertions(+) create mode 100644 calltrace.go create mode 100644 doc.go create mode 100644 error.go create mode 100644 error_test.go create mode 100644 error_tmpl.go create mode 100644 error_tmpl_test.go create mode 100644 globals.go create mode 100644 utils.go diff --git a/calltrace.go b/calltrace.go new file mode 100644 index 0000000..4a99722 --- /dev/null +++ b/calltrace.go @@ -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() +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..bf988d4 --- /dev/null +++ b/doc.go @@ -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 diff --git a/error.go b/error.go new file mode 100644 index 0000000..bc99bae --- /dev/null +++ b/error.go @@ -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) + } +} diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..2c9b384 --- /dev/null +++ b/error_test.go @@ -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")))) +} diff --git a/error_tmpl.go b/error_tmpl.go new file mode 100644 index 0000000..11e088c --- /dev/null +++ b/error_tmpl.go @@ -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), + } +} diff --git a/error_tmpl_test.go b/error_tmpl_test.go new file mode 100644 index 0000000..32629f5 --- /dev/null +++ b/error_tmpl_test.go @@ -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") +} diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..4c6de23 --- /dev/null +++ b/globals.go @@ -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 diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..dba9e69 --- /dev/null +++ b/utils.go @@ -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) +}