From 4615d7dd4e7a1f2bdbeaa4c10888c090a7f662c8 Mon Sep 17 00:00:00 2001 From: yusing Date: Wed, 16 Apr 2025 12:10:54 +0800 Subject: [PATCH] feat: enhanced error handling module --- internal/gperr/base.go | 18 +++---------- internal/gperr/builder.go | 24 +++++++++++------ internal/gperr/log.go | 4 +-- internal/gperr/multiline.go | 45 ++++++++++++++++++++++++++++++++ internal/gperr/multiline_test.go | 38 +++++++++++++++++++++++++++ internal/gperr/nested_error.go | 2 +- internal/gperr/subject.go | 8 +++--- internal/gperr/utils.go | 3 ++- 8 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 internal/gperr/multiline.go create mode 100644 internal/gperr/multiline_test.go diff --git a/internal/gperr/base.go b/internal/gperr/base.go index 96a36b3..65c5860 100644 --- a/internal/gperr/base.go +++ b/internal/gperr/base.go @@ -1,9 +1,10 @@ package gperr import ( - "encoding/json" "errors" "fmt" + + "github.com/yusing/go-proxy/pkg/json" ) // baseError is an immutable wrapper around an error. @@ -48,17 +49,6 @@ func (err *baseError) Error() string { return err.Err.Error() } -// MarshalJSON implements the json.Marshaler interface. -func (err *baseError) MarshalJSON() ([]byte, error) { - //nolint:errorlint - switch err := err.Err.(type) { - case Error, *withSubject: - return json.Marshal(err) - case json.Marshaler: - return err.MarshalJSON() - case interface{ MarshalText() ([]byte, error) }: - return err.MarshalText() - default: - return json.Marshal(err.Error()) - } +func (err *baseError) MarshalJSONTo(buf []byte) []byte { + return json.MarshalTo(err.Err, buf) } diff --git a/internal/gperr/builder.go b/internal/gperr/builder.go index ac85b56..6910ec6 100644 --- a/internal/gperr/builder.go +++ b/internal/gperr/builder.go @@ -24,6 +24,10 @@ type Builder struct { rwLock } +type multiline struct { + *Builder +} + // NewBuilder creates a new Builder. // // If about is not provided, the Builder will not have a subject @@ -78,12 +82,15 @@ func (b *Builder) Add(err error) *Builder { return b } - wrapped := wrap(err) - b.Lock() defer b.Unlock() - switch err := wrapped.(type) { + b.add(err) + return b +} + +func (b *Builder) add(err error) { + switch err := err.(type) { case *baseError: b.errs = append(b.errs, err.Err) case *nestedError: @@ -92,11 +99,11 @@ 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 { @@ -144,8 +151,9 @@ func (b *Builder) AddRange(errs ...error) *Builder { b.Lock() defer b.Unlock() - b.errs = append(b.errs, nonNilErrs...) - + for _, err := range nonNilErrs { + b.add(err) + } return b } diff --git a/internal/gperr/log.go b/internal/gperr/log.go index 94c2d15..dfa43b5 100644 --- a/internal/gperr/log.go +++ b/internal/gperr/log.go @@ -6,14 +6,14 @@ import ( "github.com/yusing/go-proxy/internal/logging" ) -func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger) { +func log(_ string, err error, level zerolog.Level, logger ...*zerolog.Logger) { var l *zerolog.Logger if len(logger) > 0 { l = logger[0] } else { l = logging.GetLogger() } - l.WithLevel(level).Msg(msg + ": " + err.Error()) + l.WithLevel(level).Msg(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 0000000..6fff371 --- /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 0000000..43e030c --- /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 08a5e77..12dbc19 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) } diff --git a/internal/gperr/subject.go b/internal/gperr/subject.go index 79ae475..c6640de 100644 --- a/internal/gperr/subject.go +++ b/internal/gperr/subject.go @@ -1,11 +1,12 @@ package gperr import ( - "encoding/json" "errors" "slices" "strings" + "github.com/yusing/go-proxy/pkg/json" + "github.com/yusing/go-proxy/internal/utils/strutils/ansi" ) @@ -93,8 +94,7 @@ func (err *withSubject) Error() string { return sb.String() } -// MarshalJSON implements the json.Marshaler interface. -func (err *withSubject) MarshalJSON() ([]byte, error) { +func (err *withSubject) MarshalJSONTo(buf []byte) []byte { subjects := slices.Clone(err.Subjects) slices.Reverse(subjects) @@ -102,5 +102,5 @@ func (err *withSubject) MarshalJSON() ([]byte, error) { "subjects": subjects, "err": err.Err, } - return json.Marshal(reversed) + return json.MarshalTo(reversed, buf) } diff --git a/internal/gperr/utils.go b/internal/gperr/utils.go index fb98d6e..6b749f5 100644 --- a/internal/gperr/utils.go +++ b/internal/gperr/utils.go @@ -1,9 +1,10 @@ package gperr import ( - "encoding/json" "errors" "fmt" + + "github.com/yusing/go-proxy/pkg/json" ) func newError(message string) error {