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:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|||||||
196
xerror/xerror.go
196
xerror/xerror.go
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user