From af582bc8ef3651f543fdda8cbf147d5e4d7f05d9 Mon Sep 17 00:00:00 2001 From: anantadwi13 Date: Sun, 19 Dec 2021 13:40:47 +0700 Subject: [PATCH] add example docs, add ErrorWrapper interface, add & fix some methods --- errorwrap.go | 160 ++++++++++++++++++++----- errorwrap_test.go | 4 +- example/main.go | 14 ++- example_test.go | 289 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 6 + stack.go | 80 +++++++++++-- 6 files changed, 517 insertions(+), 36 deletions(-) create mode 100644 example_test.go diff --git a/errorwrap.go b/errorwrap.go index 5bfb85e..94567c6 100644 --- a/errorwrap.go +++ b/errorwrap.go @@ -6,15 +6,63 @@ import ( "io" ) -type errorContainer struct { +// ErrorWrapper is a multi level error. It is built like a stack (from bottom to top). If there is a 3 level, then each +// level will have ErrorWrapper instance with an error (CurrentError). +// +// ErrorWrapper Z (CurrentError contains any object that implements error, RootCause contains ErrorWrapper X, ParentError contains ErrorWrapper Y) +// V +// ErrorWrapper Y (CurrentError contains any object that implements error, RootCause contains ErrorWrapper X, ParentError contains ErrorWrapper X) +// V +// ErrorWrapper X (CurrentError contains any object that implements error, RootCause contains nil, ParentError contains nil) +// +type ErrorWrapper interface { + // CurrentError represents as the current level error (the top level of the stack). + CurrentError() error + // ContextMessage is used as a modifier to the CurrentError. It is used for elaborating the CurrentError. + ContextMessage() string + // RootCause will return the bottom level of the stack. + RootCause() ErrorWrapper + // ParentError will return an ErrorWrapper below the current level. + ParentError() ErrorWrapper + // StackTrace return a StackTrace of the current ErrorWrapper level only. + StackTrace() StackTrace + + error + Unwrap() error + Is(target error) bool + As(target interface{}) bool + fmt.Formatter +} + +type errorWrapper struct { error contextMsg string - rootCause error - parentError error + rootCause ErrorWrapper + parentError ErrorWrapper *stack } -func (e *errorContainer) Error() string { +func (e *errorWrapper) CurrentError() error { + return e.error +} + +func (e *errorWrapper) ContextMessage() string { + return e.contextMsg +} + +func (e *errorWrapper) RootCause() ErrorWrapper { + return e.rootCause +} + +func (e *errorWrapper) ParentError() ErrorWrapper { + return e.parentError +} + +func (e *errorWrapper) StackTrace() StackTrace { + return e.stack.StackTrace() +} + +func (e *errorWrapper) Error() string { str := e.error.Error() if e.contextMsg != "" { str += ": " + e.contextMsg @@ -22,11 +70,11 @@ func (e *errorContainer) Error() string { return str } -func (e *errorContainer) fullError() string { +func (e *errorWrapper) fullError() string { str := e.Error() if e.parentError != nil { str += "\n" - if ec, ok := e.parentError.(*errorContainer); ok && ec != nil { + if ec, ok := e.parentError.(*errorWrapper); ok && ec != nil { str += ec.fullError() } else { str += e.parentError.Error() @@ -35,15 +83,15 @@ func (e *errorContainer) fullError() string { return str } -func (e *errorContainer) Unwrap() error { +func (e *errorWrapper) Unwrap() error { return e.parentError } -func (e *errorContainer) Is(target error) bool { +func (e *errorWrapper) Is(target error) bool { return e.error == target } -func (e *errorContainer) As(target interface{}) bool { +func (e *errorWrapper) As(target interface{}) bool { if target == nil { return e.error == target } @@ -54,12 +102,12 @@ func (e *errorContainer) As(target interface{}) bool { return false } -func (e *errorContainer) Format(s fmt.State, verb rune) { +func (e *errorWrapper) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { fmt.Fprintf(s, "%+v", e.fullError()) - if rc, ok := e.rootCause.(*errorContainer); ok && rc != nil { + if rc, ok := e.rootCause.(*errorWrapper); ok && rc != nil { rc.stack.Format(s, verb) } else { e.stack.Format(s, verb) @@ -78,34 +126,99 @@ func (e *errorContainer) Format(s fmt.State, verb rune) { } } +// New creates a base or root ErrorWrapper. +// +// ErrorWrapper.CurrentError is populated with errors.New(message). func New(message string) error { - return &errorContainer{ + return &errorWrapper{ error: errors.New(message), stack: callStack(), } } +// NewError creates a base or root ErrorWrapper. +// +// ErrorWrapper.CurrentError is populated with err. If err is nil then NewError returns nil. +func NewError(err error) error { + if err == nil { + return nil + } + + if ew, ok := err.(*errorWrapper); ok { + if ew == nil { + return nil + } + return ew + } + + return &errorWrapper{ + error: err, + stack: callStack(), + } +} + +// NewErrorWithMessage creates a base or root ErrorWrapper. +// +// ErrorWrapper.CurrentError is populated with err. +// ErrorWrapper.ContextMessage is populated with contextMessage. +func NewErrorWithMessage(err error, contextMessage string) error { + if err == nil { + return nil + } + + if ew, ok := err.(*errorWrapper); ok { + if ew == nil { + return nil + } + ew.contextMsg = contextMessage + return ew + } + + return &errorWrapper{ + error: err, + contextMsg: contextMessage, + stack: callStack(), + } +} + +// Wrap returns an ErrorWrapper{CurrentError: wrapper, ParentError: parent, RootCause: parent.RootCause} func Wrap(parent error, wrapper error) error { ec := WrapWithMessage(parent, wrapper, "") - ec.(*errorContainer).parentError.(*errorContainer).stack = callStack() + + if ec == nil { + return nil + } + + if _, ok := parent.(*errorWrapper); !ok { + ec.(*errorWrapper).parentError.(*errorWrapper).stack = callStack() + } + ec.(*errorWrapper).stack = callStack() return ec } +// WrapWithMessage returns an ErrorWrapper{CurrentError: wrapper, ParentError: parent, RootCause: parent.RootCause, ContextMessage: contextMessage} func WrapWithMessage(parent error, wrapper error, contextMessage string) error { - parentContainer, ok := parent.(*errorContainer) - var rootCause error + if parent == nil || wrapper == nil { + return nil + } + parentContainer, ok := parent.(*errorWrapper) if !ok { - parentContainer = &errorContainer{ + parentContainer = &errorWrapper{ error: parent, stack: callStack(), } + } + + var rootCause ErrorWrapper + + if parentContainer.rootCause == nil { rootCause = parentContainer } else { rootCause = parentContainer.rootCause } - currError := &errorContainer{ + currError := &errorWrapper{ error: wrapper, contextMsg: contextMessage, rootCause: rootCause, @@ -115,20 +228,15 @@ func WrapWithMessage(parent error, wrapper error, contextMessage string) error { return currError } -func WrapString(parent error, wrapperMsg string) error { - ec := Wrap(parent, errors.New(wrapperMsg)) - ec.(*errorContainer).parentError.(*errorContainer).stack = callStack() - return ec -} - -func Wrapper(err error, target error) error { +// Wrapper returns ErrorWrapper instance of err in target level +func Wrapper(err error, target error) ErrorWrapper { if target == nil || err == nil { return nil } for { - if curContainer, ok := err.(*errorContainer); ok && curContainer.Is(target) { - return err + if curContainer, ok := err.(ErrorWrapper); ok && (curContainer == target || curContainer.Is(target)) { + return curContainer } if err = Unwrap(err); err == nil { diff --git a/errorwrap_test.go b/errorwrap_test.go index 626e265..aa2e8bd 100644 --- a/errorwrap_test.go +++ b/errorwrap_test.go @@ -23,9 +23,9 @@ func infraLayer(intType ...int) error { } switch it { case 1: - return WrapString(ErrorRootCause, "database not found") + return NewErrorWithMessage(ErrorRootCause, "database not found") case 2: - return WrapString(ErrorInfra, "redis not found") + return NewErrorWithMessage(ErrorInfra, "redis not found") default: return Wrap(ErrorRootCause, ErrorInfra) } diff --git a/example/main.go b/example/main.go index 9ae4fca..001a776 100644 --- a/example/main.go +++ b/example/main.go @@ -29,9 +29,21 @@ func main() { fmt.Println("is ErrorInfraNotFound") // go to this section } + if err := errorwrap.Wrapper(err, ErrorInfraNotFound); err != nil { + fmt.Printf("%+v\n", err) + fmt.Println("Print file stack trace") + fmt.Printf("%s\n", err.StackTrace()) + fmt.Println("Print file+line stack trace") + fmt.Printf("%v\n", err.StackTrace()) + fmt.Println("Print function+file+line stack trace") + fmt.Printf("%+v\n", err.StackTrace()) + } + fmt.Println(errorwrap.Is(err, ErrorDomain)) // true fmt.Println(errorwrap.Is(err, ErrorUseCase)) // true + + fmt.Printf("%+v\n", errorwrap.Wrapper(err, ErrorDomain).StackTrace()) // print stack trace until domain layer only } func infraLayer(intType int) error { @@ -49,7 +61,7 @@ func infraLayer(intType int) error { case 3: err := errors.New("another error") if err != nil { - return errorwrap.WrapString(err, "using wrapper message") + return errorwrap.NewErrorWithMessage(err, "using wrapper message") } case 4: err := errors.New("error again") diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..c7e91f3 --- /dev/null +++ b/example_test.go @@ -0,0 +1,289 @@ +package errorwrap_test + +import ( + "errors" + "fmt" + "github.com/anantadwi13/errorwrap" +) + +func ExampleNew() { + err := errorwrap.New("error message") + + fmt.Println("\nFirst") + fmt.Printf("%s\n", err) + fmt.Println("\nSecond") + fmt.Printf("%+s\n", err) + fmt.Println("\nThird") + fmt.Printf("%v\n", err) + fmt.Println("\nFourth") + fmt.Printf("%+v\n", err) + + // Example output: + // + // First + // error message + // + // Second + // error message + // + // Third + // error message + // + // Fourth + // error message + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:10 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} + +func ExampleNewError() { + err := errorwrap.NewError(errors.New("error message")) + + fmt.Println("\nFirst") + fmt.Printf("%s\n", err) + fmt.Println("\nSecond") + fmt.Printf("%+s\n", err) + fmt.Println("\nThird") + fmt.Printf("%v\n", err) + fmt.Println("\nFourth") + fmt.Printf("%+v\n", err) + + // Example output: + // + // First + // error message + // + // Second + // error message + // + // Third + // error message + // + // Fourth + // error message + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:43 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} + +func ExampleNewErrorWithMessage() { + err := errorwrap.NewErrorWithMessage(errors.New("error message"), "context message") + + fmt.Println("\nFirst") + fmt.Printf("%s\n", err) + fmt.Println("\nSecond") + fmt.Printf("%+s\n", err) + fmt.Println("\nThird") + fmt.Printf("%v\n", err) + fmt.Println("\nFourth") + fmt.Printf("%+v\n", err) + + // Example output: + // + // First + // error message: context message + // + // Second + // error message: context message + // + // Third + // error message: context message + // + // Fourth + // error message: context message + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:76 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} + +func ExampleWrap() { + ErrorStd := errors.New("standard error") + ErrorA := errorwrap.New("Error A") + ErrorB := errorwrap.New("Error B") + err := errorwrap.Wrap(ErrorA, ErrorB) + err2 := errorwrap.Wrap(ErrorStd, ErrorB) + + fmt.Println("\nFirst") + fmt.Printf("%s\n", err) + fmt.Println("\nSecond") + fmt.Printf("%+s\n", err) + fmt.Println("\nThird") + fmt.Printf("%v\n", err) + fmt.Println("\nFourth") + fmt.Printf("%+v\n", err) + fmt.Println("\nFifth") + fmt.Printf("%+v\n", err2) + + // Example output: + // + // First + // Error B + // + // Second + // Error B + // Error A + // + // Third + // Error B + // + // Fourth + // Error B + // Error A + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:110 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 + // + // Fifth + // Error B + // standard error + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:113 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} + +func ExampleWrapWithMessage() { + ErrorStd := errors.New("standard error") + ErrorA := errorwrap.New("Error A") + ErrorB := errorwrap.New("Error B") + err := errorwrap.WrapWithMessage(ErrorA, ErrorB, "context message") + err2 := errorwrap.WrapWithMessage(ErrorStd, ErrorB, "context message") + + fmt.Println("\nFirst") + fmt.Printf("%s\n", err) + fmt.Println("\nSecond") + fmt.Printf("%+s\n", err) + fmt.Println("\nThird") + fmt.Printf("%v\n", err) + fmt.Println("\nFourth") + fmt.Printf("%+v\n", err) + fmt.Println("\nFifth") + fmt.Printf("%+v\n", err2) + + // Example output: + // + // First + // Error B: context message + // + // Second + // Error B: context message + // Error A + // + // Third + // Error B: context message + // + // Fourth + // Error B: context message + // Error A + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:161 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 + // + // Fifth + // Error B: context message + // standard error + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:164 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} + +func ExampleWrapper() { + ErrorA := errorwrap.New("Error A") + ErrorB := errorwrap.New("Error B") + err := errorwrap.WrapWithMessage(ErrorA, ErrorB, "context message") + + wrapErrorA := errorwrap.Wrapper(ErrorA, ErrorA) + fmt.Println("wrapErrorA") + fmt.Println(wrapErrorA == nil) + fmt.Println(wrapErrorA.RootCause(), wrapErrorA.ParentError(), wrapErrorA.CurrentError()) + fmt.Println("wrapErrorA message:") + fmt.Printf("%v\n", wrapErrorA) + fmt.Println("wrapErrorA message + stack trace:") + fmt.Printf("%+v\n", wrapErrorA) + fmt.Println() + + errWrapper1 := errorwrap.Wrapper(err, ErrorA) + fmt.Println("errWrapper1") + fmt.Println(errWrapper1 == nil) + fmt.Println(errWrapper1.RootCause(), errWrapper1.ParentError(), errWrapper1.CurrentError()) + fmt.Println("errWrapper1 message:") + fmt.Printf("%v\n", errWrapper1) + fmt.Println("errWrapper1 message + stack trace:") + fmt.Printf("%+v\n", errWrapper1) + fmt.Println() + + errWrapper2 := errorwrap.Wrapper(err, ErrorB) + fmt.Println("errWrapper2") + fmt.Println(errWrapper2 == nil) + fmt.Println(errWrapper2.RootCause(), errWrapper2.ParentError(), errWrapper2.CurrentError()) + fmt.Println("errWrapper2 message:") + fmt.Printf("%v\n", errWrapper2) + fmt.Println("errWrapper2 message + stack trace:") + fmt.Printf("%+v\n", errWrapper2) + fmt.Println() + + // Example output: + // + // wrapErrorA + // false + // Error A + // wrapErrorA message: + // Error A + // wrapErrorA message + stack trace: + // Error A + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:183 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 + // + // errWrapper1 + // false + // Error A + // errWrapper1 message: + // Error A + // errWrapper1 message + stack trace: + // Error A + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:183 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 + // + // errWrapper2 + // false + // Error A Error A Error B + // errWrapper2 message: + // Error B: context message + // errWrapper2 message + stack trace: + // Error B: context message + // Error A + // main.main + // /Users/go/src/github.com/anantadwi13/errorwrap/example_test.go:183 + // runtime.main + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/proc.go:255 + // runtime.goexit + // /usr/local/Cellar/go/1.17.5/libexec/src/runtime/asm_amd64.s:1581 +} diff --git a/go.mod b/go.mod index b868b6d..490826d 100644 --- a/go.mod +++ b/go.mod @@ -6,3 +6,9 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.0 ) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/stack.go b/stack.go index 29fac1d..b6a0c8a 100644 --- a/stack.go +++ b/stack.go @@ -9,6 +9,8 @@ import ( "strings" ) +// copied code from https://github.com/pkg/errors + type stack []uintptr func (st *stack) Format(s fmt.State, verb rune) { @@ -17,13 +19,21 @@ func (st *stack) Format(s fmt.State, verb rune) { switch { case s.Flag('+'): for _, pc := range *st { - f := frame(pc) + f := Frame(pc) fmt.Fprintf(s, "\n%+v", f) } } } } +func (st *stack) StackTrace() StackTrace { + f := make([]Frame, len(*st)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*st)[i]) + } + return f +} + func callStack() *stack { pcs := make([]uintptr, 32) n := runtime.Callers(3, pcs) @@ -31,13 +41,16 @@ func callStack() *stack { return &st } -type frame uintptr +// Frame represents a program counter inside a stack frame. +// For historical reasons if Frame is interpreted as a uintptr +// its value represents the program counter + 1. +type Frame uintptr -func (f frame) pc() uintptr { +func (f Frame) pc() uintptr { return uintptr(f) - 1 } -func (f frame) fileName() string { +func (f Frame) fileName() string { fn := runtime.FuncForPC(f.pc()) if fn == nil { return "unknown" @@ -46,7 +59,7 @@ func (f frame) fileName() string { return file } -func (f frame) lineNumber() int { +func (f Frame) lineNumber() int { fn := runtime.FuncForPC(f.pc()) if fn == nil { return 0 @@ -55,7 +68,7 @@ func (f frame) lineNumber() int { return line } -func (f frame) functionName() string { +func (f Frame) functionName() string { fn := runtime.FuncForPC(f.pc()) if fn == nil { return "unknown" @@ -75,7 +88,7 @@ func (f frame) functionName() string { // %+s function name and path of source file relative to the compile time // GOPATH separated by \n\t (\n\t) // %+v equivalent to %+s:%d -func (f frame) Format(s fmt.State, verb rune) { +func (f Frame) Format(s fmt.State, verb rune) { switch verb { case 's': switch { @@ -97,6 +110,16 @@ func (f frame) Format(s fmt.State, verb rune) { } } +// MarshalText formats a stacktrace Frame as a text string. The output is the +// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs. +func (f Frame) MarshalText() ([]byte, error) { + name := f.functionName() + if name == "unknown" { + return []byte(name), nil + } + return []byte(fmt.Sprintf("%s %s:%d", name, f.fileName(), f.lineNumber())), nil +} + // funcname removes the path prefix component of a function's name reported by func.Name(). func funcname(name string) string { i := strings.LastIndex(name, "/") @@ -104,3 +127,46 @@ func funcname(name string) string { i = strings.Index(name, ".") return name[i+1:] } + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + io.WriteString(s, "\n") + f.Format(s, verb) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + st.formatSlice(s, verb) + } + case 's': + st.formatSlice(s, verb) + } +} + +// formatSlice will format this StackTrace into the given buffer as a slice of +// Frame, only valid when called with '%s' or '%v'. +func (st StackTrace) formatSlice(s fmt.State, verb rune) { + io.WriteString(s, "[") + for i, f := range st { + if i > 0 { + io.WriteString(s, " ") + } + f.Format(s, verb) + } + io.WriteString(s, "]") +}