mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-22 16:28:30 +02:00
refactor: improve error handling and response formatting in API
This commit is contained in:
@@ -37,11 +37,11 @@ func (err *baseError) Subjectf(format string, args ...any) Error {
|
||||
}
|
||||
|
||||
func (err baseError) With(extra error) Error {
|
||||
return &nestedError{&err, []error{extra}}
|
||||
return &nestedError{err.Err, []error{extra}}
|
||||
}
|
||||
|
||||
func (err baseError) Withf(format string, args ...any) Error {
|
||||
return &nestedError{&err, []error{fmt.Errorf(format, args...)}}
|
||||
return &nestedError{err.Err, []error{fmt.Errorf(format, args...)}}
|
||||
}
|
||||
|
||||
func (err *baseError) Error() string {
|
||||
@@ -62,3 +62,11 @@ func (err *baseError) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (err *baseError) Plain() []byte {
|
||||
return Plain(err.Err)
|
||||
}
|
||||
|
||||
func (err *baseError) Markdown() []byte {
|
||||
return Markdown(err.Err)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ func TestBuilderNested(t *testing.T) {
|
||||
• Inner: 1
|
||||
• Inner: 2
|
||||
• Action 2
|
||||
• Inner: 3`
|
||||
• Inner: 3
|
||||
`
|
||||
ExpectEqual(t, got, expected)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,16 @@ type Error interface {
|
||||
Subject(subject string) Error
|
||||
// Subjectf is a wrapper for Subject(fmt.Sprintf(format, args...)).
|
||||
Subjectf(format string, args ...any) Error
|
||||
PlainError
|
||||
MarkdownError
|
||||
}
|
||||
|
||||
type PlainError interface {
|
||||
Plain() []byte
|
||||
}
|
||||
|
||||
type MarkdownError interface {
|
||||
Markdown() []byte
|
||||
}
|
||||
|
||||
// this makes JSON marshaling work,
|
||||
|
||||
@@ -153,6 +153,7 @@ func TestErrorStringNested(t *testing.T) {
|
||||
• 2
|
||||
• action 3 > inner3: generic failure
|
||||
• 3
|
||||
• 3`
|
||||
• 3
|
||||
`
|
||||
expect.Equal(t, ansi.StripANSI(ne.Error()), want)
|
||||
}
|
||||
|
||||
43
internal/gperr/hint.go
Normal file
43
internal/gperr/hint.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package gperr
|
||||
|
||||
import "github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
|
||||
type Hint struct {
|
||||
Prefix string
|
||||
Message string
|
||||
Suffix string
|
||||
}
|
||||
|
||||
var _ PlainError = (*Hint)(nil)
|
||||
var _ MarkdownError = (*Hint)(nil)
|
||||
|
||||
func (h *Hint) Error() string {
|
||||
return h.Prefix + ansi.Info(h.Message) + h.Suffix
|
||||
}
|
||||
|
||||
func (h *Hint) Plain() []byte {
|
||||
return []byte(h.Prefix + h.Message + h.Suffix)
|
||||
}
|
||||
|
||||
func (h *Hint) Markdown() []byte {
|
||||
return []byte(h.Prefix + "**" + h.Message + "**" + h.Suffix)
|
||||
}
|
||||
|
||||
func (h *Hint) MarshalText() ([]byte, error) {
|
||||
return h.Plain(), nil
|
||||
}
|
||||
|
||||
func (h *Hint) String() string {
|
||||
return h.Error()
|
||||
}
|
||||
|
||||
func DoYouMean(s string) *Hint {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &Hint{
|
||||
Prefix: "Do you mean ",
|
||||
Message: s,
|
||||
Suffix: "?",
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func log(msg string, err error, level zerolog.Level, logger ...*zerolog.Logger)
|
||||
} else {
|
||||
l = logging.GetLogger()
|
||||
}
|
||||
l.WithLevel(level).Msg(New(highlight(msg)).With(err).Error())
|
||||
l.WithLevel(level).Msg(New(highlightANSI(msg)).With(err).Error())
|
||||
switch level {
|
||||
case zerolog.FatalLevel:
|
||||
os.Exit(1)
|
||||
|
||||
@@ -3,8 +3,6 @@ package gperr
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
//nolint:recvcheck
|
||||
@@ -67,48 +65,98 @@ func (err *nestedError) Is(other error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var nilError = newError("<nil>")
|
||||
var bulletPrefix = []byte("• ")
|
||||
var markdownBulletPrefix = []byte("- ")
|
||||
var spaces = []byte(" ")
|
||||
|
||||
type appendLineFunc func(buf []byte, err error, level int) []byte
|
||||
|
||||
func (err *nestedError) Error() string {
|
||||
if err == nil {
|
||||
return makeLine("<nil>", 0)
|
||||
return nilError.Error()
|
||||
}
|
||||
|
||||
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)...)
|
||||
return strutils.JoinLines(lines)
|
||||
buf := appendLineNormal(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLineNormal)
|
||||
}
|
||||
return strutils.JoinLines(makeLines(err.Extras, 0))
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func makeLine(err string, level int) string {
|
||||
const bulletPrefix = "• "
|
||||
const spaces = " "
|
||||
func (err *nestedError) Plain() []byte {
|
||||
if err == nil {
|
||||
return appendLinePlain(nil, nilError, 0)
|
||||
}
|
||||
buf := appendLinePlain(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLinePlain)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (err *nestedError) Markdown() []byte {
|
||||
if err == nil {
|
||||
return appendLineMd(nil, nilError, 0)
|
||||
}
|
||||
|
||||
buf := appendLineMd(nil, err.Err, 0)
|
||||
if len(err.Extras) > 0 {
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, 1, appendLineMd)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLineNormal(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return err
|
||||
return append(buf, err.Error()...)
|
||||
}
|
||||
return spaces[:2*level] + bulletPrefix + err
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, bulletPrefix...)
|
||||
buf = append(buf, err.Error()...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func makeLines(errs []error, level int) []string {
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
func appendLinePlain(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return append(buf, Plain(err)...)
|
||||
}
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, bulletPrefix...)
|
||||
buf = append(buf, Plain(err)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLineMd(buf []byte, err error, level int) []byte {
|
||||
if level == 0 {
|
||||
return append(buf, Markdown(err)...)
|
||||
}
|
||||
buf = append(buf, spaces[:2*level]...)
|
||||
buf = append(buf, markdownBulletPrefix...)
|
||||
buf = append(buf, Markdown(err)...)
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendLines(buf []byte, errs []error, level int, appendLine appendLineFunc) []byte {
|
||||
if len(errs) == 0 {
|
||||
return buf
|
||||
}
|
||||
lines := make([]string, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
switch err := wrap(err).(type) {
|
||||
case *nestedError:
|
||||
if err.Err != nil {
|
||||
lines = append(lines, makeLine(err.Err.Error(), level))
|
||||
lines = append(lines, makeLines(err.Extras, level+1)...)
|
||||
buf = appendLine(buf, err.Err, level)
|
||||
buf = append(buf, '\n')
|
||||
buf = appendLines(buf, err.Extras, level+1, appendLine)
|
||||
} else {
|
||||
lines = append(lines, makeLines(err.Extras, level)...)
|
||||
buf = appendLines(buf, err.Extras, level, appendLine)
|
||||
}
|
||||
default:
|
||||
lines = append(lines, makeLine(err.Error(), level))
|
||||
buf = appendLine(buf, err, level)
|
||||
buf = append(buf, '\n')
|
||||
}
|
||||
}
|
||||
return lines
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils/ansi"
|
||||
)
|
||||
@@ -19,10 +19,23 @@ type withSubject struct {
|
||||
|
||||
const subjectSep = " > "
|
||||
|
||||
func highlight(subject string) string {
|
||||
type highlightFunc func(subject string) string
|
||||
|
||||
var _ PlainError = (*withSubject)(nil)
|
||||
var _ MarkdownError = (*withSubject)(nil)
|
||||
|
||||
func highlightANSI(subject string) string {
|
||||
return ansi.HighlightRed + subject + ansi.Reset
|
||||
}
|
||||
|
||||
func highlightMarkdown(subject string) string {
|
||||
return "**" + subject + "**"
|
||||
}
|
||||
|
||||
func noHighlight(subject string) string {
|
||||
return subject
|
||||
}
|
||||
|
||||
func PrependSubject(subject string, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -69,24 +82,38 @@ func (err *withSubject) Unwrap() error {
|
||||
}
|
||||
|
||||
func (err *withSubject) Error() string {
|
||||
return string(err.fmtError(highlightANSI))
|
||||
}
|
||||
|
||||
func (err *withSubject) Plain() []byte {
|
||||
return err.fmtError(noHighlight)
|
||||
}
|
||||
|
||||
func (err *withSubject) Markdown() []byte {
|
||||
return err.fmtError(highlightMarkdown)
|
||||
}
|
||||
|
||||
func (err *withSubject) fmtError(highlight highlightFunc) []byte {
|
||||
// subject is in reversed order
|
||||
n := len(err.Subjects)
|
||||
size := 0
|
||||
errStr := err.Err.Error()
|
||||
var sb strings.Builder
|
||||
var buf bytes.Buffer
|
||||
for _, s := range err.Subjects {
|
||||
size += len(s)
|
||||
}
|
||||
sb.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||
buf.Grow(size + 2 + n*len(subjectSep) + len(errStr) + len(highlight("")))
|
||||
|
||||
for i := n - 1; i > 0; i-- {
|
||||
sb.WriteString(err.Subjects[i])
|
||||
sb.WriteString(subjectSep)
|
||||
buf.WriteString(err.Subjects[i])
|
||||
buf.WriteString(subjectSep)
|
||||
}
|
||||
sb.WriteString(highlight(err.Subjects[0]))
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(errStr)
|
||||
return sb.String()
|
||||
buf.WriteString(highlight(err.Subjects[0]))
|
||||
if errStr != "" {
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(errStr)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@@ -29,16 +27,17 @@ func Wrap(err error, message ...string) Error {
|
||||
if len(message) == 0 || message[0] == "" {
|
||||
return wrap(err)
|
||||
}
|
||||
wrapped := &wrappedError{err, message[0]}
|
||||
//nolint:errorlint
|
||||
switch err := err.(type) {
|
||||
case *baseError:
|
||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
||||
err.Err = wrapped
|
||||
return err
|
||||
case *nestedError:
|
||||
err.Err = fmt.Errorf("%s: %w", message[0], err.Err)
|
||||
err.Err = wrapped
|
||||
return err
|
||||
}
|
||||
return &baseError{fmt.Errorf("%s: %w", message[0], err)}
|
||||
return &baseError{wrapped}
|
||||
}
|
||||
|
||||
func Unwrap(err error) Error {
|
||||
@@ -65,18 +64,6 @@ func wrap(err error) Error {
|
||||
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 {
|
||||
@@ -103,3 +90,27 @@ func Collect[T any, Err error, Arg any, Func func(Arg) (T, Err)](eb *Builder, fn
|
||||
eb.Add(err)
|
||||
return result
|
||||
}
|
||||
|
||||
func Plain(err error) []byte {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if p, ok := err.(PlainError); ok {
|
||||
return p.Plain()
|
||||
}
|
||||
return []byte(err.Error())
|
||||
}
|
||||
|
||||
func Markdown(err error) []byte {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch err := err.(type) {
|
||||
case MarkdownError:
|
||||
return err.Markdown()
|
||||
case interface{ Unwrap() []error }:
|
||||
return appendLines(nil, err.Unwrap(), 0, appendLineMd)
|
||||
default:
|
||||
return []byte(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,55 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testErr struct{}
|
||||
|
||||
func (e *testErr) Error() string {
|
||||
func (e testErr) Error() string {
|
||||
return "test error"
|
||||
}
|
||||
|
||||
func (e *testErr) MarshalJSON() ([]byte, error) {
|
||||
return nil, nil
|
||||
func (e testErr) Plain() []byte {
|
||||
return []byte("test error")
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
func (e testErr) Markdown() []byte {
|
||||
return []byte("**test error**")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
type testMultiErr struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
func (e testMultiErr) Error() string {
|
||||
return Join(e.errors...).Error()
|
||||
}
|
||||
|
||||
func (e testMultiErr) Unwrap() []error {
|
||||
return e.errors
|
||||
}
|
||||
|
||||
func TestFormatting(t *testing.T) {
|
||||
err := testErr{}
|
||||
plain := Plain(err)
|
||||
if string(plain) != "test error" {
|
||||
t.Errorf("expected test error, got %s", string(plain))
|
||||
}
|
||||
md := Markdown(err)
|
||||
if string(md) != "**test error**" {
|
||||
t.Errorf("expected test error, got %s", string(md))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiError(t *testing.T) {
|
||||
err := testMultiErr{[]error{testErr{}, testErr{}}}
|
||||
plain := Plain(err)
|
||||
if string(plain) != "test error\ntest error" {
|
||||
t.Errorf("expected test error, got %s", string(plain))
|
||||
}
|
||||
md := Markdown(err)
|
||||
if string(md) != "**test error**\n**test error**" {
|
||||
t.Errorf("expected test error, got %s", string(md))
|
||||
}
|
||||
}
|
||||
|
||||
34
internal/gperr/wrapped.go
Normal file
34
internal/gperr/wrapped.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package gperr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type wrappedError struct {
|
||||
Err error
|
||||
Message string
|
||||
}
|
||||
|
||||
var _ PlainError = (*wrappedError)(nil)
|
||||
var _ MarkdownError = (*wrappedError)(nil)
|
||||
|
||||
func (e *wrappedError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Plain() []byte {
|
||||
return fmt.Appendf(nil, "%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Markdown() []byte {
|
||||
return fmt.Appendf(nil, "**%s**: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e *wrappedError) Is(target error) bool {
|
||||
return errors.Is(e.Err, target)
|
||||
}
|
||||
Reference in New Issue
Block a user