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)
+ }
+ })
+ }
+}