Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -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...)
|
||||
}
|
||||
@@ -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...))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user