diff --git a/internal/error/log.go b/internal/error/log.go deleted file mode 100644 index 7e79d1ee..00000000 --- a/internal/error/log.go +++ /dev/null @@ -1,43 +0,0 @@ -package err - -import ( - "github.com/rs/zerolog" - "github.com/yusing/go-proxy/internal/logging" -) - -func getLogger(logger ...*zerolog.Logger) *zerolog.Logger { - if len(logger) > 0 { - return logger[0] - } - return logging.GetLogger() -} - -//go:inline -func LogFatal(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Fatal().Msg(err.Error()) -} - -//go:inline -func LogError(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Error().Msg(err.Error()) -} - -//go:inline -func LogWarn(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Warn().Msg(err.Error()) -} - -//go:inline -func LogPanic(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Panic().Msg(err.Error()) -} - -//go:inline -func LogInfo(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Info().Msg(err.Error()) -} - -//go:inline -func LogDebug(msg string, err error, logger ...*zerolog.Logger) { - getLogger(logger...).Debug().Msg(err.Error()) -} diff --git a/internal/gperr/README.md b/internal/gperr/README.md new file mode 100644 index 00000000..1ee390e4 --- /dev/null +++ b/internal/gperr/README.md @@ -0,0 +1,106 @@ +# gperr + +gperr is an error interface that supports nested structure and subject highlighting. + +## Usage + +### gperr.Error + +The error interface. + +### gperr.New + +Like `errors.New`, but returns a `gperr.Error`. + +### gperr.Wrap + +Like `fmt.Errorf("%s: %w", message, err)`, but returns a `gperr.Error`. + +### gperr.Error.Subject + +Returns a new error with the subject prepended to the error message. The main subject is highlighted. + +```go +err := gperr.New("error message") +err = err.Subject("bar") +err = err.Subject("foo") +``` + +Output: + +foo > bar: error message + +### gperr.Error.Subjectf + +Like `gperr.Error.Subject`, but formats the subject with `fmt.Sprintf`. + +### gperr.PrependSubject + +Prepends the subject to the error message like `gperr.Error.Subject`. + +```go +err := gperr.New("error message") +err = gperr.PrependSubject(err, "foo") +err = gperr.PrependSubject(err, "bar") +``` + +Output: + +bar > foo: error message + +### gperr.Error.With + +Adds a new error to the error chain. + +```go +err := gperr.New("error message") +err = err.With(gperr.New("inner error")) +err = err.With(gperr.New("inner error 2").With(gperr.New("inner inner error"))) +``` + +Output: + +``` +error message: + • inner error + • inner error 2 + • inner inner error +``` + +### gperr.Error.Withf + +Like `gperr.Error.With`, but formats the error with `fmt.Errorf`. + +### gperr.Error.Is + +Returns true if the error is equal to the given error. + +### gperr.Builder + +A builder for `gperr.Error`. + +```go +builder := gperr.NewBuilder("foo") +builder.Add(gperr.New("error message")) +builder.Addf("error message: %s", "foo") +builder.AddRange(gperr.New("error message 1"), gperr.New("error message 2")) +``` + +Output: + +``` +foo: + • error message + • error message: foo + • error message 1 + • error message 2 +``` + +### gperr.Builder.Build + +Builds a `gperr.Error` from the builder. + +## When to return gperr.Error + +- When you want to return multiple errors +- When the error has a subject diff --git a/internal/error/base.go b/internal/gperr/base.go similarity index 98% rename from internal/error/base.go rename to internal/gperr/base.go index 4668a118..96a36b31 100644 --- a/internal/error/base.go +++ b/internal/gperr/base.go @@ -1,4 +1,4 @@ -package err +package gperr import ( "encoding/json" diff --git a/internal/error/builder.go b/internal/gperr/builder.go similarity index 91% rename from internal/error/builder.go rename to internal/gperr/builder.go index 96fb8868..4eeee60c 100644 --- a/internal/error/builder.go +++ b/internal/gperr/builder.go @@ -1,4 +1,4 @@ -package err +package gperr import ( "fmt" @@ -36,7 +36,7 @@ func (b *Builder) error() Error { func (b *Builder) Error() Error { if len(b.errs) == 1 { - return From(b.errs[0]) + return wrap(b.errs[0]) } return b.error() } @@ -60,7 +60,7 @@ func (b *Builder) Add(err error) *Builder { b.Lock() defer b.Unlock() - switch err := From(err).(type) { + switch err := wrap(err).(type) { case *baseError: b.errs = append(b.errs, err.Err) case *nestedError: @@ -122,3 +122,9 @@ func (b *Builder) AddRange(errs ...error) *Builder { return b } + +func (b *Builder) ForEach(fn func(error)) { + for _, err := range b.errs { + fn(err) + } +} diff --git a/internal/error/builder_test.go b/internal/gperr/builder_test.go similarity index 94% rename from internal/error/builder_test.go rename to internal/gperr/builder_test.go index 2975e4ae..04aa3261 100644 --- a/internal/error/builder_test.go +++ b/internal/gperr/builder_test.go @@ -1,4 +1,4 @@ -package err_test +package gperr_test import ( "context" @@ -6,7 +6,7 @@ import ( "io" "testing" - . "github.com/yusing/go-proxy/internal/error" + . "github.com/yusing/go-proxy/internal/gperr" . "github.com/yusing/go-proxy/internal/utils/testing" ) diff --git a/internal/error/error.go b/internal/gperr/error.go similarity index 98% rename from internal/error/error.go rename to internal/gperr/error.go index 03e0079c..6d3e70c8 100644 --- a/internal/error/error.go +++ b/internal/gperr/error.go @@ -1,4 +1,4 @@ -package err +package gperr type Error interface { error diff --git a/internal/error/error_test.go b/internal/gperr/error_test.go similarity index 96% rename from internal/error/error_test.go rename to internal/gperr/error_test.go index ad3aca79..81d01392 100644 --- a/internal/error/error_test.go +++ b/internal/gperr/error_test.go @@ -1,4 +1,4 @@ -package err +package gperr import ( "errors" @@ -44,7 +44,7 @@ func TestBaseWithExtra(t *testing.T) { func TestBaseUnwrap(t *testing.T) { err := errors.New("err") - wrapped := From(err) + wrapped := Wrap(err) ExpectError(t, err, errors.Unwrap(wrapped)) } @@ -52,7 +52,7 @@ func TestBaseUnwrap(t *testing.T) { func TestNestedUnwrap(t *testing.T) { err := errors.New("err") err2 := New("err2") - wrapped := From(err).Subject("foo").With(err2.Subject("bar")) + wrapped := Wrap(err).Subject("foo").With(err2.Subject("bar")) unwrapper, ok := wrapped.(interface{ Unwrap() []error }) ExpectTrue(t, ok) @@ -64,7 +64,7 @@ func TestNestedUnwrap(t *testing.T) { func TestErrorIs(t *testing.T) { from := errors.New("error") - err := From(from) + err := Wrap(from) ExpectError(t, from, err) ExpectTrue(t, err.Is(from)) diff --git a/internal/gperr/log.go b/internal/gperr/log.go new file mode 100644 index 00000000..94c2d15b --- /dev/null +++ b/internal/gperr/log.go @@ -0,0 +1,40 @@ +package gperr + +import ( + "github.com/rs/zerolog" + "github.com/yusing/go-proxy/internal/common" + "github.com/yusing/go-proxy/internal/logging" +) + +func log(msg 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()) +} + +func LogFatal(msg string, err error, logger ...*zerolog.Logger) { + if common.IsDebug { + LogPanic(msg, err, logger...) + } + log(msg, err, zerolog.FatalLevel, logger...) +} + +func LogError(msg string, err error, logger ...*zerolog.Logger) { + log(msg, err, zerolog.ErrorLevel, logger...) +} + +func LogWarn(msg string, err error, logger ...*zerolog.Logger) { + log(msg, err, zerolog.WarnLevel, logger...) +} + +func LogPanic(msg string, err error, logger ...*zerolog.Logger) { + log(msg, err, zerolog.PanicLevel, logger...) +} + +func LogDebug(msg string, err error, logger ...*zerolog.Logger) { + log(msg, err, zerolog.DebugLevel, logger...) +} diff --git a/internal/error/nested_error.go b/internal/gperr/nested_error.go similarity index 97% rename from internal/error/nested_error.go rename to internal/gperr/nested_error.go index e666a4f1..dc97d87e 100644 --- a/internal/error/nested_error.go +++ b/internal/gperr/nested_error.go @@ -1,4 +1,4 @@ -package err +package gperr import ( "errors" @@ -99,7 +99,7 @@ func makeLines(errs []error, level int) []string { } lines := make([]string, 0, len(errs)) for _, err := range errs { - switch err := From(err).(type) { + switch err := wrap(err).(type) { case *nestedError: if err.Err != nil { lines = append(lines, makeLine(err.Err.Error(), level)) diff --git a/internal/error/subject.go b/internal/gperr/subject.go similarity index 99% rename from internal/error/subject.go rename to internal/gperr/subject.go index eac74995..293b648d 100644 --- a/internal/error/subject.go +++ b/internal/gperr/subject.go @@ -1,4 +1,4 @@ -package err +package gperr import ( "encoding/json" diff --git a/internal/error/utils.go b/internal/gperr/utils.go similarity index 60% rename from internal/error/utils.go rename to internal/gperr/utils.go index e4440c21..4ac1d640 100644 --- a/internal/error/utils.go +++ b/internal/gperr/utils.go @@ -1,6 +1,8 @@ -package err +package gperr import ( + "encoding/json" + "errors" "fmt" ) @@ -19,27 +21,50 @@ func Errorf(format string, args ...any) Error { return &baseError{fmt.Errorf(format, args...)} } +// Wrap wraps message in front of the error message. func Wrap(err error, message ...string) Error { - if len(message) == 0 || message[0] == "" { - return From(err) + if err == nil { + return nil } - return Errorf("%w: %s", err, message[0]) + if len(message) == 0 || message[0] == "" { + return wrap(err) + } + //nolint:errorlint + switch err := err.(type) { + case *baseError: + err.Err = fmt.Errorf("%s: %w", message[0], err.Err) + return err + case *nestedError: + err.Err = fmt.Errorf("%s: %w", message[0], err.Err) + return err + } + return &baseError{fmt.Errorf("%s: %w", message[0], err)} } -func From(err error) Error { +func wrap(err error) Error { if err == nil { return nil } //nolint:errorlint switch err := err.(type) { - case *baseError: - return err - case *nestedError: + case Error: return err } return &baseError{err} } +func IsJSONMarshallable(err error) bool { + switch err := err.(type) { + case *nestedError, *withSubject: + return true + case *baseError: + return IsJSONMarshallable(err.Err) + default: + var v json.Marshaler + return errors.As(err, &v) + } +} + func Join(errors ...error) Error { n := 0 for _, err := range errors { @@ -66,9 +91,3 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn eb.Add(err) return result } - -func Collect2[T any, Err error, Arg1 any, Arg2 any, Func func(Arg1, Arg2) (T, Err)](eb *Builder, fn Func, arg1 Arg1, arg2 Arg2) T { - result, err := fn(arg1, arg2) - eb.Add(err) - return result -} diff --git a/internal/gperr/utils_test.go b/internal/gperr/utils_test.go new file mode 100644 index 00000000..4fe44225 --- /dev/null +++ b/internal/gperr/utils_test.go @@ -0,0 +1,63 @@ +package gperr + +import ( + "errors" + "testing" +) + +type testErr struct{} + +func (e *testErr) Error() string { + return "test error" +} + +func (e *testErr) MarshalJSON() ([]byte, error) { + return nil, nil +} + +func TestIsJSONMarshallable(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "testErr", + err: &testErr{}, + want: true, + }, + { + name: "baseError", + err: &baseError{}, + want: false, + }, + { + name: "baseError with json marshallable error", + err: &baseError{&testErr{}}, + want: true, + }, + { + name: "nestedError", + err: &nestedError{}, + want: true, + }, + { + name: "withSubject", + err: &withSubject{}, + want: true, + }, + { + name: "standard error", + err: errors.New("test error"), + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := IsJSONMarshallable(test.err); got != test.want { + t.Errorf("IsJSONMarshallable(%v) = %v, want %v", test.err, got, test.want) + } + }) + } +}