Organize backend into product executable and shared Go modules

This commit is contained in:
Stephan D committed 2026-09-12 14:14:08 +02:00
1 parent 981e84a4b4
commit f0720ec66b
382 files changed
+1944 -6989

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
package merrors
import "strings"
type classifiedError struct {
base error
code error
msg string
reason string
publicMessage bool
argumentNames []string
}
func (e classifiedError) Error() string {
if e.publicMessage {
msg := strings.TrimSpace(e.msg)
if msg != "" {
return msg
}
if e.code != nil {
return e.code.Error()
}
if e.base != nil {
return e.base.Error()
}
return ""
}
return Wrap(e.base, e.msg).Error()
}
func (e classifiedError) Unwrap() []error {
causes := make([]error, 0, 2)
if e.base != nil {
causes = append(causes, e.base)
}
if e.code != nil {
causes = append(causes, e.code)
}
return causes
}
func (e classifiedError) Reason() string {
return strings.TrimSpace(e.reason)
}
func (e classifiedError) InvalidArgumentFields() []string {
return append([]string(nil), e.argumentNames...)
}
+262
View File
@@ -0,0 +1,262 @@
package merrors
import (
"errors"
"fmt"
"strings"
)
type definedError string
func (e definedError) Error() string {
return string(e)
}
type wrappedError struct {
base error
msg string
}
// opaqueWrappedError preserves error classification and causal inspection
// without including the cause text in Error. Use it at boundaries where a
// storage or provider error can contain secrets or sensitive identifiers.
type opaqueWrappedError struct {
base error
cause error
msg string
}
func (e wrappedError) Error() string {
baseText := ""
if e.base != nil {
baseText = strings.TrimSpace(e.base.Error())
}
msg := strings.TrimSpace(e.msg)
switch {
case baseText == "":
return msg
case msg == "":
return baseText
default:
return baseText + ": " + msg
}
}
func (e wrappedError) Unwrap() error {
return e.base
}
func (e opaqueWrappedError) Error() string {
return Wrap(e.base, e.msg).Error()
}
func (e opaqueWrappedError) Unwrap() []error {
causes := make([]error, 0, 2)
if e.base != nil {
causes = append(causes, e.base)
}
if e.cause != nil {
causes = append(causes, e.cause)
}
return causes
}
func Define(msg string) error {
return definedError(msg)
}
func Message(msg string) error {
return definedError(msg)
}
func Messagef(format string, args ...any) error {
return Message(fmt.Sprintf(format, args...))
}
func Wrap(err error, msg string) error {
return wrappedError{
base: err,
msg: msg,
}
}
func Wrapf(err error, format string, args ...any) error {
return Wrap(err, fmt.Sprintf(format, args...))
}
// OpaqueWrap adds safe context while retaining the cause for errors.Is and
// errors.As. Unlike Wrap, it deliberately excludes the cause text from Error.
func OpaqueWrap(base, cause error, msg string) error {
return opaqueWrappedError{base: base, cause: cause, msg: msg}
}
var ErrNotImplemented = Define("notImplemented")
func NotImplemented(msg string) error {
return Wrap(ErrNotImplemented, msg)
}
var ErrNoData = Define("noData")
func NoData(msg string) error {
return Wrap(ErrNoData, msg)
}
func NoDataf(format string, args ...any) error {
return NoData(fmt.Sprintf(format, args...))
}
var ErrInternal = Define("internal")
func Internal(msg string) error {
return Wrap(ErrInternal, msg)
}
func Internalf(format string, args ...any) error {
return Internal(fmt.Sprintf(format, args...))
}
var ErrInvalidArg = Define("invalidArgError")
func InvalidArgument(msg string, argumentNames ...string) error {
return Wrap(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
}
func InvalidArgumentf(format string, args ...any) error {
return InvalidArgument(fmt.Sprintf(format, args...))
}
func InvalidArgumentWithNamesf(argumentNames []string, format string, args ...any) error {
return InvalidArgument(fmt.Sprintf(format, args...), argumentNames...)
}
var ErrDataConflict = Define("dataConflict")
func DataConflict(msg string) error {
return Wrap(ErrDataConflict, msg)
}
func DataConflictf(format string, args ...any) error {
return DataConflict(fmt.Sprintf(format, args...))
}
func DataConflictWrap(err error, msg string) error {
return wrapError(ErrDataConflict, msg, err)
}
func DataConflictWrapf(err error, format string, args ...any) error {
return DataConflictWrap(err, fmt.Sprintf(format, args...))
}
var ErrFailedPrecondition = Define("failedPrecondition")
func FailedPrecondition(msg string) error {
return Wrap(ErrFailedPrecondition, msg)
}
func FailedPreconditionf(format string, args ...any) error {
return FailedPrecondition(fmt.Sprintf(format, args...))
}
func FailedPreconditionWrap(err error, msg string) error {
return wrapError(ErrFailedPrecondition, msg, err)
}
func FailedPreconditionWrapf(err error, format string, args ...any) error {
return FailedPreconditionWrap(err, fmt.Sprintf(format, args...))
}
var ErrServiceUnavailable = Define("serviceUnavailable")
func ServiceUnavailable(msg string) error {
return Wrap(ErrServiceUnavailable, msg)
}
func ServiceUnavailablef(format string, args ...any) error {
return ServiceUnavailable(fmt.Sprintf(format, args...))
}
func ServiceUnavailableWrap(err error, msg string) error {
return wrapError(ErrServiceUnavailable, msg, err)
}
func ServiceUnavailableWrapf(err error, format string, args ...any) error {
return ServiceUnavailableWrap(err, fmt.Sprintf(format, args...))
}
type objectRef interface {
IsZero() bool
Hex() string
}
var ErrAccessDenied = Define("accessDenied")
func PermissionDenied(msg string) error {
return Wrap(ErrAccessDenied, msg)
}
func AccessDenied(object, action string, ref objectRef) error {
if ref != nil && !ref.IsZero() {
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s' with ID '%s'", action, object, ref.Hex()))
}
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s'", action, object))
}
var ErrInvalidDataType = Define("invalidDataType")
func InvalidDataType(msg string) error {
return Wrap(ErrInvalidDataType, msg)
}
func InvalidDataTypef(format string, args ...any) error {
return InvalidDataType(fmt.Sprintf(format, args...))
}
var ErrUnauthorized = Define("unathorized")
func Unauthorized(msg string) error {
return Wrap(ErrUnauthorized, msg)
}
func Unauthorizedf(format string, args ...any) error {
return Unauthorized(fmt.Sprintf(format, args...))
}
func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error {
return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err)
}
func InvalidArgumentWrapf(err error, format string, args ...any) error {
return InvalidArgumentWrap(err, fmt.Sprintf(format, args...))
}
func InternalWrap(err error, msg string) error {
return wrapError(ErrInternal, msg, err)
}
func InternalWrapf(err error, format string, args ...any) error {
return InternalWrap(err, fmt.Sprintf(format, args...))
}
func InternalWrapAll(msg string, errs ...error) error {
return wrapErrors(ErrInternal, msg, errs...)
}
func wrapError(base error, msg string, err error) error {
return wrapErrors(base, msg, err)
}
func wrapErrors(base error, msg string, errs ...error) error {
baseErr := Wrap(base, msg)
causes := make([]error, 0, len(errs))
for _, err := range errs {
if err != nil {
causes = append(causes, err)
}
}
if len(causes) == 0 {
return baseErr
}
return errors.Join(baseErr, errors.Join(causes...))
}
+125
View File
@@ -0,0 +1,125 @@
package merrors
import (
"errors"
"strings"
"testing"
)
type testReasonedError struct {
error
reason string
}
func (e testReasonedError) Reason() string {
return e.reason
}
func TestReasonUsesReasonedError(t *testing.T) {
err := testReasonedError{
error: InvalidArgument("value is invalid"),
reason: "amount_below_minimum",
}
reason, ok := Reason(err)
if !ok {
t.Fatalf("expected reason")
}
if reason != "amount_below_minimum" {
t.Fatalf("reason mismatch: got=%q", reason)
}
}
func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) {
t.Run("without argument name keeps old behavior", func(t *testing.T) {
err := InvalidArgument("value is missing")
expected := "invalidArgError: value is missing"
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
if !errors.Is(err, ErrInvalidArg) {
t.Fatalf("error should wrap ErrInvalidArg")
}
})
t.Run("single argument name", func(t *testing.T) {
err := InvalidArgument("value is missing", "bot_token_env")
expected := `invalidArgError: broken argument "bot_token_env": value is missing`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
t.Run("multiple argument names", func(t *testing.T) {
err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ")
expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
})
}
func TestInvalidArgumentWithNamesfSupportsBrokenArgumentName(t *testing.T) {
err := InvalidArgumentWithNamesf([]string{"gateway.supported_assets.currency"}, "asset[%d] %s", 2, "is invalid")
expected := `invalidArgError: broken argument "gateway.supported_assets.currency": asset[2] is invalid`
if err.Error() != expected {
t.Fatalf("unexpected error message: %s", err)
}
if !errors.Is(err, ErrInvalidArg) {
t.Fatalf("error should wrap ErrInvalidArg")
}
}
func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) {
base := Define("root cause")
err := InvalidArgumentWrap(base, "value is missing", "bot_token_env")
if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) {
t.Fatalf("wrapped error should include broken argument name: %s", err)
}
if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) {
t.Fatalf("wrapped error should preserve all error layers")
}
}
func TestInternalWrapAllPreservesAllCauses(t *testing.T) {
first := Define("first cause")
second := Define("second cause")
err := InternalWrapAll("combined failure", first, nil, second)
if !strings.Contains(err.Error(), "internal: combined failure") {
t.Fatalf("wrapped error should include context: %s", err)
}
if !errors.Is(err, ErrInternal) || !errors.Is(err, first) || !errors.Is(err, second) {
t.Fatalf("wrapped error should preserve all error layers")
}
}
func TestOpaqueWrapPreservesCauseWithoutExposingItsText(t *testing.T) {
cause := DataConflict("restricted storage key")
err := OpaqueWrap(ErrInternal, cause, "safe persistence failure")
if got, want := err.Error(), "internal: safe persistence failure"; got != want {
t.Fatalf("opaque error mismatch: got=%q want=%q", got, want)
}
if !errors.Is(err, ErrInternal) || !errors.Is(err, ErrDataConflict) {
t.Fatalf("opaque error did not preserve classifications: %v", err)
}
}
func TestOperationalWrapsPreserveClassificationAndCause(t *testing.T) {
cause := Define("dependency failure")
tests := []struct {
name string
err error
kind error
}{
{name: "failed precondition", err: FailedPreconditionWrap(cause, "AA capability unavailable"), kind: ErrFailedPrecondition},
{name: "service unavailable", err: ServiceUnavailableWrap(cause, "Arbitrum RPC unavailable"), kind: ErrServiceUnavailable},
{name: "data conflict", err: DataConflictWrap(cause, "managed wallet replay differs"), kind: ErrDataConflict},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !errors.Is(tt.err, tt.kind) || !errors.Is(tt.err, cause) {
t.Fatalf("wrapped error should preserve classification and cause: %s", tt.err)
}
})
}
}
+44
View File
@@ -0,0 +1,44 @@
package merrors
import (
"fmt"
"strings"
)
// InvalidArgumentClassified returns an invalid-argument error with a stable
// machine reason and structured field names for transport adapters.
func InvalidArgumentClassified(reason, msg string, argumentNames ...string) error {
return classifiedError{
base: ErrInvalidArg,
msg: invalidArgumentMessage(msg, argumentNames...),
reason: strings.TrimSpace(reason),
argumentNames: normalizeArgumentNames(argumentNames),
}
}
func invalidArgumentMessage(msg string, argumentNames ...string) string {
normalizedNames := normalizeArgumentNames(argumentNames)
names := make([]string, 0, len(normalizedNames))
for _, name := range normalizedNames {
names = append(names, fmt.Sprintf("%q", name))
}
if len(names) == 0 {
return msg
}
prefix := "broken argument"
if len(names) > 1 {
prefix = "broken arguments"
}
return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg)
}
func normalizeArgumentNames(argumentNames []string) []string {
names := make([]string, 0, len(argumentNames))
for _, name := range argumentNames {
if name = strings.TrimSpace(name); name != "" {
names = append(names, name)
}
}
return names
}
+19
View File
@@ -0,0 +1,19 @@
package merrors
import (
"errors"
"strings"
)
func Reason(err error) (string, bool) {
var reasoned interface {
Reason() string
}
if errors.As(err, &reasoned) {
reason := strings.TrimSpace(reasoned.Reason())
if reason != "" {
return reason, true
}
}
return "", false
}
+50
View File
@@ -0,0 +1,50 @@
package merrors
import (
"errors"
"strings"
"time"
)
// ThrottleDetails describes a temporary request limit without coupling callers
// to a transport-specific response format.
type ThrottleDetails struct {
ScopeKind string
RetryAfter time.Duration
}
type throttleDetailsError struct {
base error
msg string
details ThrottleDetails
}
func (e throttleDetailsError) Error() string {
return Wrap(e.base, e.msg).Error()
}
func (e throttleDetailsError) Unwrap() error {
return e.base
}
// WithThrottle annotates an error with retry information while preserving its
// classification for errors.Is and errors.As.
func WithThrottle(base error, scopeKind string, retryAfter time.Duration, msg string) error {
return throttleDetailsError{
base: base,
msg: msg,
details: ThrottleDetails{
ScopeKind: strings.TrimSpace(scopeKind),
RetryAfter: retryAfter,
},
}
}
// ThrottleDetailsFrom returns retry information attached by WithThrottle.
func ThrottleDetailsFrom(err error) (ThrottleDetails, bool) {
var throttled throttleDetailsError
if !errors.As(err, &throttled) {
return ThrottleDetails{}, false
}
return throttled.details, true
}
+35
View File
@@ -0,0 +1,35 @@
package merrors
import (
"errors"
"testing"
"time"
)
func TestWithThrottlePreservesClassificationAndDetails(t *testing.T) {
base := Define("verificationLimited")
err := Wrap(
WithThrottle(base, " destination ", 45*time.Second, "request is temporarily limited"),
"verification failed",
)
if !errors.Is(err, base) {
t.Fatalf("throttle error did not preserve classification: %v", err)
}
details, ok := ThrottleDetailsFrom(err)
if !ok {
t.Fatal("throttle details were not available through wrapping")
}
if details.ScopeKind != "destination" || details.RetryAfter != 45*time.Second {
t.Fatalf("unexpected throttle details: %+v", details)
}
if got, want := err.Error(), "verificationLimited: request is temporarily limited: verification failed"; got != want {
t.Fatalf("unexpected throttle error text: got=%q want=%q", got, want)
}
}
func TestThrottleDetailsFromRejectsUnannotatedError(t *testing.T) {
if details, ok := ThrottleDetailsFrom(ServiceUnavailable("dependency unavailable")); ok {
t.Fatalf("unexpected throttle details: %+v", details)
}
}