From d0260b2841407daf595977222891144de83d8be7 Mon Sep 17 00:00:00 2001 From: dudaodong Date: Tue, 14 Feb 2023 16:32:57 +0800 Subject: [PATCH] feat: add XError to support more contextual error handling --- xerror/stack.go | 21 +++- xerror/xerror.go | 196 +++++++++++++++++++++++++++++++++- xerror/xerror_example_test.go | 6 +- xerror/xerror_test.go | 94 ++++++++++++++-- 4 files changed, 305 insertions(+), 12 deletions(-) diff --git a/xerror/stack.go b/xerror/stack.go index 60e092f..f62aaac 100644 --- a/xerror/stack.go +++ b/xerror/stack.go @@ -16,7 +16,26 @@ import ( type Stack struct { Func string `json:"func"` File string `json:"file"` - Line string `json:"line"` + Line int `json:"line"` +} + +// Stacks returns stack trace array generated by pkg/errors +func (e *XError) Stacks() []*Stack { + resp := make([]*Stack, len(*e.stack)) + for i, st := range *e.stack { + f := frame(st) + resp[i] = &Stack{ + Func: f.name(), + File: f.file(), + Line: f.line(), + } + } + return resp +} + +// StackTrace returns stack trace which is compatible with pkg/errors +func (e *XError) StackTrace() StackTrace { + return e.stack.StackTrace() } // --------------------------------------- diff --git a/xerror/xerror.go b/xerror/xerror.go index 41e5171..2432004 100644 --- a/xerror/xerror.go +++ b/xerror/xerror.go @@ -4,10 +4,202 @@ // Package xerror implements helpers for errors package xerror -// Unwrap if err is nil then it returns a valid value +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/duke-git/lancet/v2/random" +) + +// XError is to handle error related information. +type XError struct { + id string + message string + stack *stack + cause error + values map[string]any +} + +// New creates a new XError with message +func New(format string, args ...any) *XError { + err := newXError() + err.message = fmt.Sprintf(format, args...) + return err +} + +// Wrap creates a new XError and add message. +func Wrap(cause error, message ...any) *XError { + err := newXError() + + if len(message) > 0 { + var newMsgs []string + for _, m := range message { + newMsgs = append(newMsgs, fmt.Sprintf("%v", m)) + } + err.message = strings.Join(newMsgs, " ") + } + + err.cause = cause + + return err +} + +// Unwrap returns unwrapped XError from err by errors.As. If no XError, returns nil +func Unwrap(err error) *XError { + var e *XError + if errors.As(err, &e) { + return e + } + return nil +} + +func newXError() *XError { + id, err := random.UUIdV4() + if err != nil { + return nil + } + + return &XError{ + id: id, + stack: callers(), + values: make(map[string]any), + } +} + +func (e *XError) copy(dest *XError) { + dest.message = e.message + dest.id = e.id + dest.cause = e.cause + + for k, v := range e.values { + dest.values[k] = v + } +} + +// Error implements standard error interface. +func (e *XError) Error() string { + msg := e.message + cause := e.cause + + if cause == nil { + return msg + } + + msg = fmt.Sprintf("%s: %v", msg, cause.Error()) + + return msg +} + +// Format returns: +// - %v, %s, %q: formatted message +// - %+v: formatted message with stack trace +func (e *XError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + _, _ = io.WriteString(s, e.Error()) + e.stack.Format(s, verb) + return + } + fallthrough + case 's': + _, _ = io.WriteString(s, e.Error()) + case 'q': + fmt.Fprintf(s, "%q", e.Error()) + } +} + +// Wrap creates a new XError and copy message and id to new one. +func (e *XError) Wrap(cause error) *XError { + err := newXError() + e.copy(err) + err.cause = cause + return err +} + +// Unwrap compatible with github.com/pkg/errors +func (e *XError) Unwrap() error { + return e.cause +} + +// With adds key and value related to the error object +func (e *XError) With(key string, value any) *XError { + e.values[key] = value + return e +} + +// Is checks if target error is XError and Error.id of two errors are matched. +func (e *XError) Is(target error) bool { + var err *XError + + if errors.As(target, &err) { + if e.id != "" && e.id == err.id { + return true + } + } + + return e == target +} + +// Id sets id to check equality in XError.Is +func (e *XError) Id(id string) *XError { + e.id = id + return e +} + +// Values returns map of key and value that is set by With. All wrapped xerror.XError key and values will be merged. +// Key and values of wrapped error is overwritten by upper xerror.XError. +func (e *XError) Values() map[string]any { + var values map[string]any + + if cause := e.Unwrap(); cause != nil { + if err, ok := cause.(*XError); ok { + values = err.Values() + } + } + + if values == nil { + values = make(map[string]any) + } + + for key, value := range e.values { + values[key] = value + } + + return values +} + +type errInfo struct { + Message string `json:"message"` + Id string `json:"id"` + StackTrace []*Stack `json:"stacktrace"` + Cause error `json:"cause"` + Values map[string]any `json:"values"` +} + +// Info returns information of xerror, which can be printed. +func (e *XError) Info() *errInfo { + errInfo := &errInfo{ + Message: e.message, + Id: e.id, + StackTrace: e.Stacks(), + Cause: e.cause, + Values: make(map[string]any), + } + + for k, v := range e.values { + errInfo.Values[k] = v + } + + return errInfo +} + +// TryUnwrap if err is nil then it returns a valid value // If err is not nil, Unwrap panics with err. // Play: https://go.dev/play/p/w84d7Mb3Afk -func Unwrap[T any](val T, err error) T { +func TryUnwrap[T any](val T, err error) T { if err != nil { panic(err) } diff --git a/xerror/xerror_example_test.go b/xerror/xerror_example_test.go index 512655f..20ca29a 100644 --- a/xerror/xerror_example_test.go +++ b/xerror/xerror_example_test.go @@ -6,8 +6,8 @@ import ( "strconv" ) -func ExampleUnwrap() { - result1 := Unwrap(strconv.Atoi("42")) +func ExampleTryUnwrap() { + result1 := TryUnwrap(strconv.Atoi("42")) fmt.Println(result1) _, err := strconv.Atoi("4o2") @@ -17,7 +17,7 @@ func ExampleUnwrap() { fmt.Println(result2) }() - Unwrap(strconv.Atoi("4o2")) + TryUnwrap(strconv.Atoi("4o2")) // Output: // 42 diff --git a/xerror/xerror_test.go b/xerror/xerror_test.go index d14e5ad..36a7037 100644 --- a/xerror/xerror_test.go +++ b/xerror/xerror_test.go @@ -1,19 +1,21 @@ package xerror import ( + "errors" "strconv" + "strings" "testing" "github.com/duke-git/lancet/v2/internal" ) -func TestUnwrap(t *testing.T) { - assert := internal.NewAssert(t, "TestUnwrap") - assert.Equal(42, Unwrap(strconv.Atoi("42"))) +func TestTryUnwrap(t *testing.T) { + assert := internal.NewAssert(t, "TestTryUnwrap") + assert.Equal(42, TryUnwrap(strconv.Atoi("42"))) } -func TestUnwrapFail(t *testing.T) { - assert := internal.NewAssert(t, "TestUnwrapFail") +func TestTryUnwrapFail(t *testing.T) { + assert := internal.NewAssert(t, "TestTryUnwrapFail") _, err := strconv.Atoi("4o2") defer func() { @@ -21,5 +23,85 @@ func TestUnwrapFail(t *testing.T) { assert.Equal(err.Error(), v.(*strconv.NumError).Error()) }() - Unwrap(strconv.Atoi("4o2")) + TryUnwrap(strconv.Atoi("4o2")) +} + +func TestNew(t *testing.T) { + assert := internal.NewAssert(t, "TestNew") + + err := New("error occurs") + assert.Equal("error occurs", err.Error()) +} + +func TestWrap(t *testing.T) { + assert := internal.NewAssert(t, "TestWrap") + + err := New("wrong password") + wrapErr := Wrap(err, "error") + + assert.Equal("error: wrong password", wrapErr.Error()) +} + +func TestXError_Wrap(t *testing.T) { + assert := internal.NewAssert(t, "TestXError_Wrap") + + err1 := New("error").With("level", "high") + err2 := err1.Wrap(errors.New("bad")) + + assert.Equal("error: bad", err2.Error()) +} + +func TestXError_Unwrap(t *testing.T) { + assert := internal.NewAssert(t, "TestXError_Unwrap") + + err1 := New("error").With("level", "high") + + err2 := err1.Wrap(errors.New("bad")) + + err := err2.Unwrap() + + assert.Equal("bad", err.Error()) +} + +func TestXError_StackTrace(t *testing.T) { + assert := internal.NewAssert(t, "TestXError_StackTrace") + + err := New("error") + + stacks := err.Stacks() + + assert.Equal(3, len(stacks)) + assert.Equal("github.com/duke-git/lancet/v2/xerror.TestXError_StackTrace", stacks[0].Func) + assert.Equal(69, stacks[0].Line) + assert.Equal(true, strings.Contains(stacks[0].File, "xerror_test.go")) +} + +func TestXError_With_Id_Is_Values(t *testing.T) { + assert := internal.NewAssert(t, "TestXError_With_Id_Is_Values") + + baseErr := New("baseError") + err1 := New("error1").Id("e001").With("level", "high") + err2 := New("error2").Id("e002").With("level", "low") + + err := err1.Wrap(baseErr).With("v", "1.0") + + assert.Equal(true, errors.Is(err, baseErr)) + assert.NotEqual(err, err1) + assert.IsNotNil(err.Values()["v"]) + assert.IsNil(err1.Values()["v"]) + + assert.Equal(false, errors.Is(err, err2)) +} + +func TestXError_Info(t *testing.T) { + assert := internal.NewAssert(t, "TestXError_Info") + + cause := errors.New("error") + err := Wrap(cause, "invalid username").Id("e001").With("level", "high") + + errInfo := err.Info() + assert.Equal("invalid username", errInfo.Message) + assert.Equal("e001", errInfo.Id) + assert.Equal(cause, errInfo.Cause) + assert.Equal("high", errInfo.Values["level"]) }