From 43566bbcfdccd9591ce809baf18d12afba463957 Mon Sep 17 00:00:00 2001 From: yusing Date: Thu, 24 Apr 2025 06:19:22 +0800 Subject: [PATCH] feat: enhanced error handling library --- internal/gperr/builder.go | 110 ++++++++++++++++++++----------- internal/gperr/log.go | 2 +- internal/gperr/multiline.go | 45 +++++++++++++ internal/gperr/multiline_test.go | 38 +++++++++++ internal/gperr/nested_error.go | 13 ++-- internal/gperr/subject.go | 3 +- internal/gperr/utils.go | 12 ++++ 7 files changed, 176 insertions(+), 47 deletions(-) create mode 100644 internal/gperr/multiline.go create mode 100644 internal/gperr/multiline_test.go diff --git a/internal/gperr/builder.go b/internal/gperr/builder.go index 4eeee60c..60bc661f 100644 --- a/internal/gperr/builder.go +++ b/internal/gperr/builder.go @@ -5,44 +5,69 @@ import ( "sync" ) +type noLock struct{} + +func (noLock) Lock() {} +func (noLock) Unlock() {} +func (noLock) RLock() {} +func (noLock) RUnlock() {} + +type rwLock interface { + sync.Locker + RLock() + RUnlock() +} + type Builder struct { about string errs []error - sync.Mutex + rwLock } -func NewBuilder(about string) *Builder { - return &Builder{about: about} +type multiline struct { + *Builder +} + +// NewBuilder creates a new Builder. +// +// If about is not provided, the Builder will not have a subject +// and will expand when adding to another builder. +func NewBuilder(about ...string) *Builder { + if len(about) == 0 { + return &Builder{rwLock: noLock{}} + } + return &Builder{about: about[0], rwLock: noLock{}} +} + +func NewBuilderWithConcurrency(about ...string) *Builder { + if len(about) == 0 { + return &Builder{rwLock: new(sync.RWMutex)} + } + return &Builder{about: about[0], rwLock: new(sync.RWMutex)} +} + +func (b *Builder) EnableConcurrency() { + b.rwLock = new(sync.RWMutex) } func (b *Builder) About() string { - if !b.HasError() { - return "" - } return b.about } -//go:inline func (b *Builder) HasError() bool { + // no need to lock, when this is called, the Builder is not used anymore return len(b.errs) > 0 } -func (b *Builder) error() Error { - if !b.HasError() { +func (b *Builder) Error() Error { + if len(b.errs) == 0 { return nil } return &nestedError{Err: New(b.about), Extras: b.errs} } -func (b *Builder) Error() Error { - if len(b.errs) == 1 { - return wrap(b.errs[0]) - } - return b.error() -} - func (b *Builder) String() string { - err := b.error() + err := b.Error() if err == nil { return "" } @@ -52,15 +77,19 @@ func (b *Builder) String() string { // Add adds an error to the Builder. // // adding nil is no-op. -func (b *Builder) Add(err error) *Builder { +func (b *Builder) Add(err error) { if err == nil { - return b + return } b.Lock() defer b.Unlock() - switch err := wrap(err).(type) { + b.add(err) +} + +func (b *Builder) add(err error) { + switch err := err.(type) { case *baseError: b.errs = append(b.errs, err.Err) case *nestedError: @@ -69,21 +98,20 @@ func (b *Builder) Add(err error) *Builder { } else { b.errs = append(b.errs, err) } + case *MultilineError: + b.add(&err.nestedError) default: - panic("bug: should not reach here") + b.errs = append(b.errs, err) } - - return b } -func (b *Builder) Adds(err string) *Builder { +func (b *Builder) Adds(err string) { b.Lock() defer b.Unlock() b.errs = append(b.errs, newError(err)) - return b } -func (b *Builder) Addf(format string, args ...any) *Builder { +func (b *Builder) Addf(format string, args ...any) { if len(args) > 0 { b.Lock() defer b.Unlock() @@ -91,13 +119,11 @@ func (b *Builder) Addf(format string, args ...any) *Builder { } else { b.Adds(format) } - - return b } -func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder { +func (b *Builder) AddFrom(other *Builder, flatten bool) { if other == nil || !other.HasError() { - return b + return } b.Lock() @@ -105,26 +131,32 @@ func (b *Builder) AddFrom(other *Builder, flatten bool) *Builder { if flatten { b.errs = append(b.errs, other.errs...) } else { - b.errs = append(b.errs, other.error()) + b.errs = append(b.errs, other.Error()) } - return b } -func (b *Builder) AddRange(errs ...error) *Builder { - b.Lock() - defer b.Unlock() - +func (b *Builder) AddRange(errs ...error) { + nonNilErrs := make([]error, 0, len(errs)) for _, err := range errs { if err != nil { - b.errs = append(b.errs, err) + nonNilErrs = append(nonNilErrs, err) } } - return b + b.Lock() + defer b.Unlock() + + for _, err := range nonNilErrs { + b.add(err) + } } func (b *Builder) ForEach(fn func(error)) { - for _, err := range b.errs { + b.RLock() + errs := b.errs + b.RUnlock() + + for _, err := range errs { fn(err) } } diff --git a/internal/gperr/log.go b/internal/gperr/log.go index 94c2d15b..1888d924 100644 --- a/internal/gperr/log.go +++ b/internal/gperr/log.go @@ -13,7 +13,7 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger) } else { l = logging.GetLogger() } - l.WithLevel(level).Msg(msg + ": " + err.Error()) + l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error()) } func LogFatal(msg string, err error, logger ...*zerolog.Logger) { diff --git a/internal/gperr/multiline.go b/internal/gperr/multiline.go new file mode 100644 index 00000000..6fff371f --- /dev/null +++ b/internal/gperr/multiline.go @@ -0,0 +1,45 @@ +package gperr + +import ( + "fmt" + "reflect" +) + +type MultilineError struct { + nestedError +} + +func Multiline() *MultilineError { + return &MultilineError{} +} + +func (m *MultilineError) add(err error) { + m.Extras = append(m.Extras, err) +} + +func (m *MultilineError) Addf(format string, args ...any) *MultilineError { + m.add(fmt.Errorf(format, args...)) + return m +} + +func (m *MultilineError) Adds(s string) *MultilineError { + m.add(newError(s)) + return m +} + +func (m *MultilineError) AddLines(lines any) *MultilineError { + v := reflect.ValueOf(lines) + if v.Kind() == reflect.Slice { + for i := range v.Len() { + switch v := v.Index(i).Interface().(type) { + case string: + m.add(newError(v)) + case error: + m.add(v) + default: + m.add(fmt.Errorf("%v", v)) + } + } + } + return m +} diff --git a/internal/gperr/multiline_test.go b/internal/gperr/multiline_test.go new file mode 100644 index 00000000..8fb3a831 --- /dev/null +++ b/internal/gperr/multiline_test.go @@ -0,0 +1,38 @@ +package gperr + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMultiline(t *testing.T) { + multiline := Multiline() + multiline.Addf("line 1 %s", "test") + multiline.Adds("line 2") + multiline.AddLines([]any{1, "2", 3.0, net.IPv4(127, 0, 0, 1)}) + t.Error(New("result").With(multiline)) + t.Error(multiline.Subject("subject").Withf("inner")) +} + +func TestWrapMultiline(t *testing.T) { + multiline := Multiline() + var wrapper error = wrap(multiline) + _, ok := wrapper.(*MultilineError) + if !ok { + t.Errorf("wrapper is not a MultilineError") + } +} + +func TestPrependSubjectMultiline(t *testing.T) { + multiline := Multiline() + multiline.Addf("line 1 %s", "test") + multiline.Adds("line 2") + multiline.AddLines([]any{1, "2", 3.0, net.IPv4(127, 0, 0, 1)}) + multiline.Subject("subject") + + builder := NewBuilder("") + builder.Add(multiline) + require.Equal(t, len(builder.errs), len(multiline.Extras), builder.errs) +} diff --git a/internal/gperr/nested_error.go b/internal/gperr/nested_error.go index dc97d87e..12dbc19c 100644 --- a/internal/gperr/nested_error.go +++ b/internal/gperr/nested_error.go @@ -15,7 +15,7 @@ type nestedError struct { func (err nestedError) Subject(subject string) Error { if err.Err == nil { - err.Err = newError(subject) + err.Err = PrependSubject(subject, errStr("")) } else { err.Err = PrependSubject(subject, err.Err) } @@ -72,14 +72,13 @@ func (err *nestedError) Error() string { return makeLine("", 0) } - lines := make([]string, 0, 1+len(err.Extras)) if err.Err != nil { + lines := make([]string, 0, 1+len(err.Extras)) lines = append(lines, makeLine(err.Err.Error(), 0)) lines = append(lines, makeLines(err.Extras, 1)...) - } else { - lines = append(lines, makeLines(err.Extras, 0)...) + return strutils.JoinLines(lines) } - return strutils.JoinLines(lines) + return strutils.JoinLines(makeLines(err.Extras, 0)) } //go:inline @@ -103,8 +102,10 @@ func makeLines(errs []error, level int) []string { case *nestedError: if err.Err != nil { lines = append(lines, makeLine(err.Err.Error(), level)) + lines = append(lines, makeLines(err.Extras, level+1)...) + } else { + lines = append(lines, makeLines(err.Extras, level)...) } - lines = append(lines, makeLines(err.Extras, level+1)...) default: lines = append(lines, makeLine(err.Error(), level)) } diff --git a/internal/gperr/subject.go b/internal/gperr/subject.go index 293b648d..ffec4553 100644 --- a/internal/gperr/subject.go +++ b/internal/gperr/subject.go @@ -2,6 +2,7 @@ package gperr import ( "encoding/json" + "errors" "strings" "github.com/yusing/go-proxy/internal/utils/strutils/ansi" @@ -59,7 +60,7 @@ func (err *withSubject) Prepend(subject string) *withSubject { } func (err *withSubject) Is(other error) bool { - return err.Err == other + return errors.Is(other, err.Err) } func (err *withSubject) Unwrap() error { diff --git a/internal/gperr/utils.go b/internal/gperr/utils.go index 4ac1d640..5c0776dc 100644 --- a/internal/gperr/utils.go +++ b/internal/gperr/utils.go @@ -53,6 +53,18 @@ func wrap(err error) Error { return &baseError{err} } +func Unwrap(err error) Error { + //nolint:errorlint + switch err := err.(type) { + case interface{ Unwrap() []error }: + return &nestedError{Extras: err.Unwrap()} + case interface{ Unwrap() error }: + return &baseError{err.Unwrap()} + default: + return &baseError{err} + } +} + func IsJSONMarshallable(err error) bool { switch err := err.(type) { case *nestedError, *withSubject: