package domain import ( "fmt" "unicode/utf8" rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" ) const MaxProtocolDetailBytes = 4 * 1024 // Error is the transport-neutral form of an RVBox structured failure. type Error struct { Code rvboxv1.ControlError_Code Message string Retryable bool IssueUUID string Cause error } func (e *Error) Error() string { if e.IssueUUID == "" { return fmt.Sprintf("%s: %s", e.Code, e.Message) } return fmt.Sprintf("%s (%s): %s", e.Code, e.IssueUUID, e.Message) } func (e *Error) Unwrap() error { return e.Cause } // NewError applies the stable retryability policy and bounds wire-visible text. func NewError(code rvboxv1.ControlError_Code, message, issueUUID string) *Error { return &Error{ Code: code, Message: boundUTF8(message, MaxProtocolDetailBytes), Retryable: DefaultRetryable(code), IssueUUID: issueUUID, } } func (e *Error) WithCause(cause error) *Error { e.Cause = cause return e } func (e *Error) ControlError() *rvboxv1.ControlError { return &rvboxv1.ControlError{ Code: e.Code, Message: boundUTF8(e.Message, MaxProtocolDetailBytes), Retryable: e.Retryable, IssueUuid: e.IssueUUID, } } func DefaultRetryable(code rvboxv1.ControlError_Code) bool { switch code { case rvboxv1.ControlError_OFFLINE, rvboxv1.ControlError_CAPACITY_EXHAUSTED, rvboxv1.ControlError_TRANSIENT, rvboxv1.ControlError_INTERNAL: return true default: return false } } func NewExecutionContextUnavailable(message, issueUUID string) *Error { return NewError(rvboxv1.ControlError_CODE_EXECUTION_CONTEXT_UNAVAILABLE, message, issueUUID) } func NewElevationUnavailable(message, issueUUID string) *Error { return NewError(rvboxv1.ControlError_CODE_ELEVATION_UNAVAILABLE, message, issueUUID) } func boundUTF8(value string, limit int) string { if len(value) <= limit { return value } value = value[:limit] for !utf8.ValidString(value) { value = value[:len(value)-1] } return value }