Bluemage/go/pkg/errs/errs.go

240 lines
4.9 KiB
Go

package errs
import (
"context"
"errors"
"fmt"
"log/slog"
"reflect"
"strings"
"connectrpc.com/connect"
"github.com/tigorlazuardi/bluemage/go/pkg/caller"
)
type Error interface {
error
Message(msg string, args ...any) Error
GetMessage() string
Code(status connect.Code) Error
GetCode() connect.Code
Caller(pc caller.Caller) Error
GetCaller() caller.Caller
Details(...any) Error
GetDetails() []any
Log(ctx context.Context) Error
}
var (
_ Error = (*Err)(nil)
_ slog.LogValuer = (*Err)(nil)
)
type Err struct {
message string
code connect.Code
caller caller.Caller
details []any
origin error
}
// Unwrap implements the implementation of errors.Unwrap.
func (er *Err) Unwrap() error {
return er.origin
}
// LogValue implements slog.LogValuer.
func (er *Err) LogValue() slog.Value {
values := make([]slog.Attr, 0, 5)
if er.message != "" {
values = append(values, slog.String("message", er.message))
}
if er.code != 0 && er.code != connect.CodeInternal {
values = append(values, slog.String("code", er.code.String()))
}
if er.caller.PC != 0 {
values = append(values, slog.Attr{Key: "caller", Value: er.caller.LogValue()})
}
if len(er.details) > 0 {
values = append(values, slog.Group("details", er.details...))
}
if er.origin == nil {
values = append(values, slog.Any("error", er.origin))
} else if lv, ok := er.origin.(slog.LogValuer); ok {
values = append(values, slog.Attr{Key: "error", Value: lv.LogValue()})
} else {
values = append(values, slog.Attr{Key: "error", Value: slog.GroupValue(
slog.String("type", reflect.TypeOf(er.origin).String()),
slog.String("message", er.origin.Error()),
slog.Any("data", er.origin),
)})
}
return slog.GroupValue(values...)
}
// Caller implements Error.
func (e *Err) Caller(pc caller.Caller) Error {
e.caller = pc
return e
}
// Code implements Error.
func (e *Err) Code(status connect.Code) Error {
e.code = status
return e
}
// Details implements Error.
func (e *Err) Details(keysAndValues ...any) Error {
e.details = keysAndValues
return e
}
// Error implements Error.
//
// The error message is constructed by concatenating the message of the error,
// discarding any empty and duplicate messages in the chain.
//
// The error messages are concatenated with a colon and a space.
func (e *Err) Error() string {
s := strings.Builder{}
if e.message != "" {
s.WriteString(e.message)
if e.origin != nil {
s.WriteString(": ")
}
}
unwrap := errors.Unwrap(e)
for unwrap != nil {
var current string
if e, ok := unwrap.(Error); ok && e.GetMessage() != "" {
current = e.GetMessage()
} else {
current = unwrap.Error()
}
next := errors.Unwrap(unwrap)
if next != nil {
current, _ = strings.CutSuffix(current, next.Error())
current, _ = strings.CutSuffix(current, ": ")
}
if current != "" {
s.WriteString(current)
if next != nil {
s.WriteString(": ")
}
}
unwrap = next
}
return s.String()
}
// GetCaller implements Error.
func (e *Err) GetCaller() caller.Caller {
return e.caller
}
// GetCode implements Error.
func (e *Err) GetCode() connect.Code {
return e.code
}
// GetDetails implements Error.
func (e *Err) GetDetails() []any {
return e.details
}
// GetMessage implements Error.
func (e *Err) GetMessage() string {
return e.message
}
// Log implements Error.
func (e *Err) Log(ctx context.Context) Error {
panic("unimplemented")
}
// Message implements Error.
func (e *Err) Message(msg string, args ...any) Error {
e.message = fmt.Sprintf(msg, args...)
return e
}
func Wrap(err error, message string) Error {
return &Err{
origin: err,
caller: caller.New(3),
message: message,
code: connect.CodeInternal,
}
}
func Wrapw(err error, message string, details ...any) Error {
return &Err{
origin: err,
details: details,
message: message,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Wrapf(err error, message string, args ...any) Error {
message = fmt.Sprintf(message, args...)
return &Err{
origin: err,
message: message,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Fail(message string, details ...any) Error {
return &Err{
origin: errors.New(message),
details: details,
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func Failf(message string, args ...any) Error {
return &Err{
origin: fmt.Errorf(message, args...),
caller: caller.New(3),
code: connect.CodeInternal,
}
}
func IntoConnectError(err error) error {
if err == nil {
return nil
}
if e := FindError(err); e != nil {
return connect.NewError(e.GetCode(), e)
}
return connect.NewError(connect.CodeUnknown, err)
}
func FindError(err error) Error {
for {
if e, ok := err.(Error); ok {
return e
}
if e, ok := err.(interface{ Unwrap() error }); ok {
err = e.Unwrap()
continue
}
return nil
}
}
func DrillToError(err error) error {
e := FindError(err)
if e != nil {
return e
}
return err
}