use super::cancel::CancelReason;
use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PanicPayload {
message: String,
}
impl PanicPayload {
#[inline]
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
#[inline]
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for PanicPayload {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "panic: {}", self.message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Ok = 0,
Err = 1,
Cancelled = 2,
Panicked = 3,
}
impl Severity {
#[inline]
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
#[inline]
#[must_use]
pub const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Ok),
1 => Some(Self::Err),
2 => Some(Self::Cancelled),
3 => Some(Self::Panicked),
_ => None,
}
}
}
impl fmt::Display for Severity {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ok => write!(f, "ok"),
Self::Err => write!(f, "err"),
Self::Cancelled => write!(f, "cancelled"),
Self::Panicked => write!(f, "panicked"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Outcome<T, E> {
Ok(T),
Err(E),
Cancelled(CancelReason),
Panicked(PanicPayload),
}
impl<T, E> PartialEq for Outcome<T, E>
where
T: PartialEq,
E: PartialEq,
{
#[inline]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ok(a), Self::Ok(b)) => a == b,
(Self::Err(a), Self::Err(b)) => a == b,
(Self::Cancelled(a), Self::Cancelled(b)) => a == b,
(Self::Panicked(a), Self::Panicked(b)) => a == b,
_ => false,
}
}
}
impl<T, E> Eq for Outcome<T, E>
where
T: Eq,
E: Eq,
{
}
impl<T, E> Outcome<T, E> {
#[inline]
#[must_use]
pub const fn ok(value: T) -> Self {
Self::Ok(value)
}
#[inline]
#[must_use]
pub const fn err(error: E) -> Self {
Self::Err(error)
}
#[inline]
#[must_use]
pub const fn cancelled(reason: CancelReason) -> Self {
Self::Cancelled(reason)
}
#[inline]
#[must_use]
pub const fn panicked(payload: PanicPayload) -> Self {
Self::Panicked(payload)
}
#[inline]
#[must_use]
pub const fn severity(&self) -> Severity {
match self {
Self::Ok(_) => Severity::Ok,
Self::Err(_) => Severity::Err,
Self::Cancelled(_) => Severity::Cancelled,
Self::Panicked(_) => Severity::Panicked,
}
}
#[inline]
#[must_use]
pub const fn severity_u8(&self) -> u8 {
self.severity().as_u8()
}
#[inline]
#[must_use]
pub const fn is_terminal(&self) -> bool {
true }
#[inline]
#[must_use]
pub const fn is_ok(&self) -> bool {
matches!(self, Self::Ok(_))
}
#[inline]
#[must_use]
pub const fn is_err(&self) -> bool {
matches!(self, Self::Err(_))
}
#[inline]
#[must_use]
pub const fn is_cancelled(&self) -> bool {
matches!(self, Self::Cancelled(_))
}
#[inline]
#[must_use]
pub const fn is_panicked(&self) -> bool {
matches!(self, Self::Panicked(_))
}
#[inline]
pub fn into_result(self) -> Result<T, OutcomeError<E>> {
match self {
Self::Ok(v) => Ok(v),
Self::Err(e) => Err(OutcomeError::Err(e)),
Self::Cancelled(r) => Err(OutcomeError::Cancelled(r)),
Self::Panicked(p) => Err(OutcomeError::Panicked(p)),
}
}
#[inline]
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Outcome<U, E> {
match self {
Self::Ok(v) => Outcome::Ok(f(v)),
Self::Err(e) => Outcome::Err(e),
Self::Cancelled(r) => Outcome::Cancelled(r),
Self::Panicked(p) => Outcome::Panicked(p),
}
}
#[inline]
pub fn map_err<F2, G: FnOnce(E) -> F2>(self, g: G) -> Outcome<T, F2> {
match self {
Self::Ok(v) => Outcome::Ok(v),
Self::Err(e) => Outcome::Err(g(e)),
Self::Cancelled(r) => Outcome::Cancelled(r),
Self::Panicked(p) => Outcome::Panicked(p),
}
}
#[inline]
pub fn and_then<U, F: FnOnce(T) -> Outcome<U, E>>(self, f: F) -> Outcome<U, E> {
match self {
Self::Ok(v) => f(v),
Self::Err(e) => Outcome::Err(e),
Self::Cancelled(r) => Outcome::Cancelled(r),
Self::Panicked(p) => Outcome::Panicked(p),
}
}
#[inline]
pub fn ok_or_else<F2, G: FnOnce() -> F2>(self, f: G) -> Result<T, F2> {
match self {
Self::Ok(v) => Ok(v),
_ => Err(f()),
}
}
#[inline]
#[must_use]
pub fn join(self, other: Self) -> Self {
match (self, other) {
(Self::Cancelled(mut left), Self::Cancelled(right)) => {
if right.severity() > left.severity() {
left.strengthen(&right);
}
Self::Cancelled(left)
}
(left, right) => {
if left.severity() >= right.severity() {
left
} else {
right
}
}
}
}
#[inline]
#[track_caller]
pub fn unwrap(self) -> T
where
E: fmt::Debug,
{
match self {
Self::Ok(v) => v,
Self::Err(e) => panic!("called `Outcome::unwrap()` on an `Err` value: {e:?}"),
Self::Cancelled(r) => {
panic!("called `Outcome::unwrap()` on a `Cancelled` value: {r:?}")
}
Self::Panicked(p) => panic!("called `Outcome::unwrap()` on a `Panicked` value: {p}"),
}
}
#[inline]
pub fn unwrap_or(self, default: T) -> T {
match self {
Self::Ok(v) => v,
_ => default,
}
}
#[inline]
pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
match self {
Self::Ok(v) => v,
_ => f(),
}
}
}
impl<T, E> From<Result<T, E>> for Outcome<T, E> {
#[inline]
fn from(result: Result<T, E>) -> Self {
match result {
Ok(v) => Self::Ok(v),
Err(e) => Self::Err(e),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OutcomeError<E> {
Err(E),
Cancelled(CancelReason),
Panicked(PanicPayload),
}
impl<E: fmt::Display> fmt::Display for OutcomeError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Err(e) => write!(f, "{e}"),
Self::Cancelled(r) => write!(f, "cancelled: {r}"),
Self::Panicked(p) => write!(f, "{p}"),
}
}
}
impl<E: fmt::Debug + fmt::Display> std::error::Error for OutcomeError<E> {}
#[inline]
pub fn join_outcomes<T, E>(a: Outcome<T, E>, b: Outcome<T, E>) -> Outcome<T, E> {
a.join(b)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{Value, json};
fn scrub_outcome_serde(value: Value) -> Value {
let mut scrubbed = value;
if let Some(message) = scrubbed.pointer_mut("/cancelled/Cancelled/message") {
*message = Value::String("[MESSAGE]".to_string());
}
scrubbed
}
fn scrub_outcome_json_ids(value: Value) -> Value {
let mut scrubbed = value;
if let Some(origin_region) = scrubbed.pointer_mut("/Cancelled/origin_region") {
*origin_region = json!("[REGION_ID]");
}
if let Some(origin_task) = scrubbed.pointer_mut("/Cancelled/origin_task") {
*origin_task = json!("[TASK_ID]");
}
scrubbed
}
#[test]
fn severity_ordering() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let err: Outcome<i32, &str> = Outcome::Err("error");
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
assert!(ok.severity() < err.severity());
assert!(err.severity() < cancelled.severity());
assert!(cancelled.severity() < panicked.severity());
}
#[test]
fn severity_values() {
let ok: Outcome<(), ()> = Outcome::Ok(());
let err: Outcome<(), ()> = Outcome::Err(());
let cancelled: Outcome<(), ()> = Outcome::Cancelled(CancelReason::default());
let panicked: Outcome<(), ()> = Outcome::Panicked(PanicPayload::new("test"));
assert_eq!(ok.severity(), Severity::Ok);
assert_eq!(err.severity(), Severity::Err);
assert_eq!(cancelled.severity(), Severity::Cancelled);
assert_eq!(panicked.severity(), Severity::Panicked);
}
#[test]
fn is_ok_predicate() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let err: Outcome<i32, &str> = Outcome::Err("error");
assert!(ok.is_ok());
assert!(!err.is_ok());
}
#[test]
fn is_err_predicate() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let err: Outcome<i32, &str> = Outcome::Err("error");
assert!(!ok.is_err());
assert!(err.is_err());
}
#[test]
fn is_cancelled_predicate() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
assert!(!ok.is_cancelled());
assert!(cancelled.is_cancelled());
}
#[test]
fn is_panicked_predicate() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
assert!(!ok.is_panicked());
assert!(panicked.is_panicked());
}
#[test]
fn is_terminal_always_true() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let err: Outcome<i32, &str> = Outcome::Err("error");
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
assert!(ok.is_terminal());
assert!(err.is_terminal());
assert!(cancelled.is_terminal());
assert!(panicked.is_terminal());
}
#[test]
fn join_takes_worse() {
let ok: Outcome<i32, &str> = Outcome::Ok(1);
let err: Outcome<i32, &str> = Outcome::Err("error");
let joined = join_outcomes(ok, err);
assert!(joined.is_err());
}
#[test]
fn join_ok_with_ok_returns_first() {
let a: Outcome<i32, &str> = Outcome::Ok(1);
let b: Outcome<i32, &str> = Outcome::Ok(2);
let result = join_outcomes(a, b);
assert!(matches!(result, Outcome::Ok(1)));
}
#[test]
fn join_err_with_cancelled_returns_cancelled() {
let err: Outcome<i32, &str> = Outcome::Err("error");
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let result = join_outcomes(err, cancelled);
assert!(result.is_cancelled());
}
#[test]
fn join_panicked_dominates_all() {
let ok: Outcome<i32, &str> = Outcome::Ok(1);
let err: Outcome<i32, &str> = Outcome::Err("error");
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
assert!(join_outcomes(ok, panicked.clone()).is_panicked());
assert!(join_outcomes(err, panicked.clone()).is_panicked());
assert!(join_outcomes(cancelled, panicked).is_panicked());
}
#[test]
fn join_cancelled_strengthens_to_worst_reason() {
let user: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("soft"));
let shutdown: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
let left_first = user.clone().join(shutdown.clone());
let right_first = shutdown.join(user);
match left_first {
Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
other => panic!("expected cancelled outcome, got {other:?}"),
}
match right_first {
Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
other => panic!("expected cancelled outcome, got {other:?}"),
}
}
#[test]
fn join_outcomes_cancelled_strengthens_to_worst_reason() {
let user: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("soft"));
let shutdown: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
let joined = join_outcomes(user, shutdown);
match joined {
Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
other => panic!("expected cancelled outcome, got {other:?}"),
}
}
#[test]
fn join_cancelled_equal_severity_is_left_biased() {
use crate::types::CancelKind;
let left: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("z-left"));
let right: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("a-right"));
let joined = left.join(right);
match joined {
Outcome::Cancelled(reason) => {
assert!(reason.is_kind(CancelKind::User));
assert_eq!(reason.message(), Some("z-left"));
}
other => panic!("expected cancelled outcome, got {other:?}"),
}
}
#[test]
fn join_cancelled_equal_rank_kinds_is_left_biased() {
use crate::types::CancelKind;
let left: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::timeout());
let right: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::deadline());
let joined = left.join(right);
match joined {
Outcome::Cancelled(reason) => assert!(reason.is_kind(CancelKind::Timeout)),
other => panic!("expected cancelled outcome, got {other:?}"),
}
}
#[test]
fn map_transforms_ok_value() {
let ok: Outcome<i32, &str> = Outcome::Ok(21);
let mapped = ok.map(|x| x * 2);
assert!(matches!(mapped, Outcome::Ok(42)));
}
#[test]
fn map_preserves_err() {
let err: Outcome<i32, &str> = Outcome::Err("error");
let mapped = err.map(|x| x * 2);
assert!(matches!(mapped, Outcome::Err("error")));
}
#[test]
fn map_preserves_cancelled() {
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let mapped = cancelled.map(|x| x * 2);
assert!(mapped.is_cancelled());
}
#[test]
fn map_preserves_panicked() {
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
let mapped = panicked.map(|x| x * 2);
assert!(mapped.is_panicked());
}
#[test]
fn map_err_transforms_err_value() {
let err: Outcome<i32, &str> = Outcome::Err("short");
let mapped = err.map_err(str::len);
assert!(matches!(mapped, Outcome::Err(5)));
}
#[test]
fn map_err_preserves_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let mapped = ok.map_err(str::len);
assert!(matches!(mapped, Outcome::Ok(42)));
}
#[test]
fn unwrap_returns_value_on_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
assert_eq!(ok.unwrap(), 42);
}
#[test]
#[should_panic(expected = "called `Outcome::unwrap()` on an `Err` value")]
fn unwrap_panics_on_err() {
let err: Outcome<i32, &str> = Outcome::Err("error");
let _ = err.unwrap();
}
#[test]
#[should_panic(expected = "called `Outcome::unwrap()` on a `Cancelled` value")]
fn unwrap_panics_on_cancelled() {
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let _ = cancelled.unwrap();
}
#[test]
#[should_panic(expected = "called `Outcome::unwrap()` on a `Panicked` value")]
fn unwrap_panics_on_panicked() {
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
let _ = panicked.unwrap();
}
#[test]
fn unwrap_or_returns_value_on_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
assert_eq!(ok.unwrap_or(0), 42);
}
#[test]
fn unwrap_or_returns_default_on_err() {
let err: Outcome<i32, &str> = Outcome::Err("error");
assert_eq!(err.unwrap_or(0), 0);
}
#[test]
fn unwrap_or_returns_default_on_cancelled() {
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
assert_eq!(cancelled.unwrap_or(99), 99);
}
#[test]
fn unwrap_or_else_computes_default_lazily() {
let err: Outcome<i32, &str> = Outcome::Err("error");
let mut called = false;
let result = err.unwrap_or_else(|| {
called = true;
42
});
assert!(called);
assert_eq!(result, 42);
}
#[test]
fn unwrap_or_else_doesnt_call_closure_on_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let result = ok.unwrap_or_else(|| panic!("should not be called"));
assert_eq!(result, 42);
}
#[test]
fn ok_or_else_collapses_non_ok_variants_to_fallback() {
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
assert_eq!(cancelled.ok_or_else(|| "fallback"), Err("fallback"));
assert_eq!(panicked.ok_or_else(|| "fallback"), Err("fallback"));
}
#[test]
fn ok_or_else_doesnt_call_closure_on_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let result = ok.ok_or_else(|| panic!("should not be called"));
assert_eq!(result, Ok(42));
}
#[test]
fn into_result_ok() {
let ok: Outcome<i32, &str> = Outcome::Ok(42);
let result = ok.into_result();
assert!(matches!(result, Ok(42)));
}
#[test]
fn into_result_err() {
let err: Outcome<i32, &str> = Outcome::Err("error");
let result = err.into_result();
assert!(matches!(result, Err(OutcomeError::Err("error"))));
}
#[test]
fn into_result_cancelled() {
let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
let result = cancelled.into_result();
assert!(matches!(result, Err(OutcomeError::Cancelled(_))));
}
#[test]
fn into_result_panicked() {
let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
let result = panicked.into_result();
assert!(matches!(result, Err(OutcomeError::Panicked(_))));
}
#[test]
fn from_result_ok() {
let result: Result<i32, &str> = Ok(42);
let outcome: Outcome<i32, &str> = Outcome::from(result);
assert!(matches!(outcome, Outcome::Ok(42)));
}
#[test]
fn from_result_err() {
let result: Result<i32, &str> = Err("error");
let outcome: Outcome<i32, &str> = Outcome::from(result);
assert!(matches!(outcome, Outcome::Err("error")));
}
#[test]
fn panic_payload_display() {
let payload = PanicPayload::new("something went wrong");
let display = format!("{payload}");
assert_eq!(display, "panic: something went wrong");
}
#[test]
fn panic_payload_message() {
let payload = PanicPayload::new("test message");
assert_eq!(payload.message(), "test message");
}
#[test]
fn outcome_error_display_err() {
let error: OutcomeError<&str> = OutcomeError::Err("application error");
let display = format!("{error}");
assert_eq!(display, "application error");
}
#[test]
fn outcome_error_display_cancelled() {
let error: OutcomeError<&str> = OutcomeError::Cancelled(CancelReason::default());
let display = format!("{error}");
assert!(display.contains("cancelled"));
}
#[test]
fn outcome_error_display_cancelled_uses_human_readable_reason() {
let error: OutcomeError<&str> =
OutcomeError::Cancelled(CancelReason::timeout().with_message("budget elapsed"));
let display = format!("{error}");
assert_eq!(display, "cancelled: timeout: budget elapsed");
assert!(!display.contains("CancelReason"));
}
#[test]
fn outcome_error_display_panicked() {
let error: OutcomeError<&str> = OutcomeError::Panicked(PanicPayload::new("oops"));
let display = format!("{error}");
assert!(display.contains("panic"));
assert!(display.contains("oops"));
}
#[test]
fn severity_debug_clone_copy_hash() {
use std::collections::HashSet;
let a = Severity::Cancelled;
let b = a; let c = a;
assert_eq!(a, b);
assert_eq!(a, c);
let dbg = format!("{a:?}");
assert!(dbg.contains("Cancelled"));
let mut set = HashSet::new();
set.insert(a);
assert!(set.contains(&b));
assert!(!set.contains(&Severity::Ok));
}
#[test]
fn panic_payload_debug_clone_eq() {
let a = PanicPayload::new("boom");
let b = a.clone();
assert_eq!(a, b);
assert_ne!(a, PanicPayload::new("other"));
let dbg = format!("{a:?}");
assert!(dbg.contains("PanicPayload"));
}
#[test]
fn outcome_serde_snapshot_scrubbed() {
insta::assert_json_snapshot!(
"outcome_serde_scrubbed",
scrub_outcome_serde(json!({
"ok": Outcome::<u8, &str>::ok(7),
"err": Outcome::<u8, &str>::err("denied"),
"cancelled": Outcome::<u8, &str>::cancelled(CancelReason::user("req-9f4c36b1")),
"panicked": OutcomeError::<&str>::Panicked(PanicPayload::new("boom")),
}))
);
}
#[test]
fn outcome_json_snapshot_scrubs_ids_only() {
let cancelled: Outcome<(), ()> = Outcome::cancelled(
CancelReason::linked_exit()
.with_region(crate::types::RegionId::new_for_test(42, 7))
.with_task(crate::types::TaskId::new_for_test(9, 3))
.with_timestamp(crate::types::Time::from_nanos(55))
.with_message("upstream closed"),
);
insta::assert_json_snapshot!(
"outcome_json_scrubbed_ids",
scrub_outcome_json_ids(serde_json::to_value(cancelled).expect("serialize outcome"))
);
}
}