1
0
mirror of https://github.com/duke-git/lancet.git synced 2026-02-04 12:52:28 +08:00

feat: add XError to support more contextual error handling

This commit is contained in:
dudaodong
2023-02-14 16:32:57 +08:00
parent 57bd64cae7
commit d0260b2841
4 changed files with 305 additions and 12 deletions

View File

@@ -16,7 +16,26 @@ import (
type Stack struct { type Stack struct {
Func string `json:"func"` Func string `json:"func"`
File string `json:"file"` 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()
} }
// --------------------------------------- // ---------------------------------------

View File

@@ -4,10 +4,202 @@
// Package xerror implements helpers for errors // Package xerror implements helpers for errors
package xerror 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. // If err is not nil, Unwrap panics with err.
// Play: https://go.dev/play/p/w84d7Mb3Afk // 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 { if err != nil {
panic(err) panic(err)
} }

View File

@@ -6,8 +6,8 @@ import (
"strconv" "strconv"
) )
func ExampleUnwrap() { func ExampleTryUnwrap() {
result1 := Unwrap(strconv.Atoi("42")) result1 := TryUnwrap(strconv.Atoi("42"))
fmt.Println(result1) fmt.Println(result1)
_, err := strconv.Atoi("4o2") _, err := strconv.Atoi("4o2")
@@ -17,7 +17,7 @@ func ExampleUnwrap() {
fmt.Println(result2) fmt.Println(result2)
}() }()
Unwrap(strconv.Atoi("4o2")) TryUnwrap(strconv.Atoi("4o2"))
// Output: // Output:
// 42 // 42

View File

@@ -1,19 +1,21 @@
package xerror package xerror
import ( import (
"errors"
"strconv" "strconv"
"strings"
"testing" "testing"
"github.com/duke-git/lancet/v2/internal" "github.com/duke-git/lancet/v2/internal"
) )
func TestUnwrap(t *testing.T) { func TestTryUnwrap(t *testing.T) {
assert := internal.NewAssert(t, "TestUnwrap") assert := internal.NewAssert(t, "TestTryUnwrap")
assert.Equal(42, Unwrap(strconv.Atoi("42"))) assert.Equal(42, TryUnwrap(strconv.Atoi("42")))
} }
func TestUnwrapFail(t *testing.T) { func TestTryUnwrapFail(t *testing.T) {
assert := internal.NewAssert(t, "TestUnwrapFail") assert := internal.NewAssert(t, "TestTryUnwrapFail")
_, err := strconv.Atoi("4o2") _, err := strconv.Atoi("4o2")
defer func() { defer func() {
@@ -21,5 +23,85 @@ func TestUnwrapFail(t *testing.T) {
assert.Equal(err.Error(), v.(*strconv.NumError).Error()) 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"])
} }