Skip to main content

asupersync/types/
outcome.rs

1//! Four-valued outcome type with severity lattice.
2//!
3//! # Overview
4//!
5//! The [`Outcome`] type represents the result of a concurrent operation with four
6//! possible states arranged in a severity lattice:
7//!
8//! ```text
9//!           Panicked
10//!              ↑
11//!          Cancelled
12//!              ↑
13//!             Err
14//!              ↑
15//!             Ok
16//! ```
17//!
18//! - [`Outcome::Ok`] - Success with value
19//! - [`Outcome::Err`] - Application error (recoverable business logic failure)
20//! - [`Outcome::Cancelled`] - Operation was cancelled (external interruption)
21//! - [`Outcome::Panicked`] - Task panicked (unrecoverable failure)
22//!
23//! # Severity Lattice
24//!
25//! The severity ordering `Ok < Err < Cancelled < Panicked` enables:
26//!
27//! - **Monotone aggregation**: When joining concurrent tasks, the worst outcome wins
28//! - **Clear semantics**: Cancellation is worse than error, panic is worst
29//! - **Idempotent composition**: `join(a, a) = a`
30//!
31//! # HTTP Status Code Mapping
32//!
33//! When using Outcome for HTTP handlers, the recommended status code mapping is:
34//!
35//! | Outcome Variant | HTTP Status | Description |
36//! |-----------------|-------------|-------------|
37//! | `Ok(T)` | 200 OK (or custom) | Success, response body in T |
38//! | `Err(E)` | 4xx/5xx | Based on error kind |
39//! | `Cancelled(_)` | 499 Client Closed Request | Request was cancelled |
40//! | `Panicked(_)` | 500 Internal Server Error | Server panic caught |
41//!
42//! ```rust,ignore
43//! async fn handler(ctx: RequestContext<'_>) -> Outcome<Response, ApiError> {
44//!     let user = get_user(ctx.user_id()).await?;  // Err -> 4xx/5xx
45//!     Outcome::ok(Response::json(user))           // Ok -> 200
46//! }
47//! // If cancelled: 499 Client Closed Request
48//! // If panicked: 500 Internal Server Error
49//! ```
50//!
51//! # Examples
52//!
53//! ## Basic Usage
54//!
55//! ```
56//! use asupersync::{Outcome, CancelReason};
57//!
58//! // Construction using static methods
59//! let success: Outcome<i32, &str> = Outcome::ok(42);
60//! let failure: Outcome<i32, &str> = Outcome::err("not found");
61//!
62//! // Inspection
63//! assert!(success.is_ok());
64//! assert!(failure.is_err());
65//!
66//! // Transformation
67//! let doubled = success.map(|x| x * 2);
68//! assert_eq!(doubled.unwrap(), 84);
69//! ```
70//!
71//! ## Aggregation (Join Semantics)
72//!
73//! ```
74//! use asupersync::{Outcome, join_outcomes, CancelReason};
75//!
76//! // When joining outcomes, the worst wins
77//! let ok: Outcome<(), ()> = Outcome::ok(());
78//! let err: Outcome<(), ()> = Outcome::err(());
79//! let cancelled: Outcome<(), ()> = Outcome::cancelled(CancelReason::timeout());
80//!
81//! // Err is worse than Ok
82//! let joined = join_outcomes(ok.clone(), err.clone());
83//! assert!(joined.is_err());
84//!
85//! // Cancelled is worse than Err
86//! let joined = join_outcomes(err, cancelled);
87//! assert!(joined.is_cancelled());
88//! ```
89//!
90//! ## Conversion to Result
91//!
92//! ```
93//! use asupersync::{Outcome, OutcomeError};
94//!
95//! let outcome: Outcome<i32, &str> = Outcome::ok(42);
96//! let result: Result<i32, OutcomeError<&str>> = outcome.into_result();
97//! assert!(result.is_ok());
98//! ```
99
100use super::cancel::CancelReason;
101use core::fmt;
102use serde::{Deserialize, Serialize};
103
104/// Payload from a caught panic.
105///
106/// This wraps the panic value for safe transport across task boundaries.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct PanicPayload {
109    message: String,
110}
111
112impl PanicPayload {
113    /// Creates a new panic payload with the given message.
114    #[inline]
115    #[must_use]
116    pub fn new(message: impl Into<String>) -> Self {
117        Self {
118            message: message.into(),
119        }
120    }
121
122    /// Returns the panic message.
123    #[inline]
124    #[must_use]
125    pub fn message(&self) -> &str {
126        &self.message
127    }
128}
129
130impl fmt::Display for PanicPayload {
131    #[inline]
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "panic: {}", self.message)
134    }
135}
136
137/// Severity level of an outcome.
138///
139/// The severity levels form a total order:
140/// `Ok < Err < Cancelled < Panicked`
141///
142/// When aggregating outcomes (e.g., joining parallel tasks), the outcome
143/// with higher severity takes precedence.
144///
145/// # Examples
146///
147/// ```
148/// use asupersync::{Outcome, Severity, CancelReason};
149///
150/// let ok: Outcome<(), ()> = Outcome::ok(());
151/// let err: Outcome<(), ()> = Outcome::err(());
152///
153/// assert_eq!(ok.severity(), Severity::Ok);
154/// assert_eq!(err.severity(), Severity::Err);
155/// assert!(Severity::Ok < Severity::Err);
156/// assert!(Severity::Err < Severity::Cancelled);
157/// assert!(Severity::Cancelled < Severity::Panicked);
158/// ```
159#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
160pub enum Severity {
161    /// Success - the operation completed normally.
162    Ok = 0,
163    /// Error - the operation failed with an application error.
164    Err = 1,
165    /// Cancelled - the operation was cancelled before completion.
166    Cancelled = 2,
167    /// Panicked - the operation panicked.
168    Panicked = 3,
169}
170
171impl Severity {
172    /// Returns the numeric severity value (0-3).
173    ///
174    /// This is useful for serialization or comparison.
175    #[inline]
176    #[must_use]
177    pub const fn as_u8(self) -> u8 {
178        self as u8
179    }
180
181    /// Creates a Severity from a numeric value.
182    ///
183    /// Returns `None` if the value is out of range (> 3).
184    #[inline]
185    #[must_use]
186    pub const fn from_u8(value: u8) -> Option<Self> {
187        match value {
188            0 => Some(Self::Ok),
189            1 => Some(Self::Err),
190            2 => Some(Self::Cancelled),
191            3 => Some(Self::Panicked),
192            _ => None,
193        }
194    }
195}
196
197impl fmt::Display for Severity {
198    #[inline]
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        match self {
201            Self::Ok => write!(f, "ok"),
202            Self::Err => write!(f, "err"),
203            Self::Cancelled => write!(f, "cancelled"),
204            Self::Panicked => write!(f, "panicked"),
205        }
206    }
207}
208
209/// The four-valued outcome of a concurrent operation.
210///
211/// Forms a severity lattice where worse outcomes dominate:
212/// `Ok < Err < Cancelled < Panicked`
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub enum Outcome<T, E> {
215    /// Success with a value.
216    Ok(T),
217    /// Application-level error.
218    Err(E),
219    /// The operation was cancelled.
220    Cancelled(CancelReason),
221    /// The operation panicked.
222    Panicked(PanicPayload),
223}
224
225impl<T, E> PartialEq for Outcome<T, E>
226where
227    T: PartialEq,
228    E: PartialEq,
229{
230    #[inline]
231    fn eq(&self, other: &Self) -> bool {
232        match (self, other) {
233            (Self::Ok(a), Self::Ok(b)) => a == b,
234            (Self::Err(a), Self::Err(b)) => a == b,
235            (Self::Cancelled(a), Self::Cancelled(b)) => a == b,
236            (Self::Panicked(a), Self::Panicked(b)) => a == b,
237            _ => false,
238        }
239    }
240}
241
242impl<T, E> Eq for Outcome<T, E>
243where
244    T: Eq,
245    E: Eq,
246{
247}
248
249impl<T, E> Outcome<T, E> {
250    // =========================================================================
251    // Construction
252    // =========================================================================
253
254    /// Creates a successful outcome with the given value.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use asupersync::Outcome;
260    ///
261    /// let outcome: Outcome<i32, &str> = Outcome::ok(42);
262    /// assert!(outcome.is_ok());
263    /// assert_eq!(outcome.unwrap(), 42);
264    /// ```
265    #[inline]
266    #[must_use]
267    pub const fn ok(value: T) -> Self {
268        Self::Ok(value)
269    }
270
271    /// Creates an error outcome with the given error.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use asupersync::Outcome;
277    ///
278    /// let outcome: Outcome<i32, &str> = Outcome::err("not found");
279    /// assert!(outcome.is_err());
280    /// ```
281    #[inline]
282    #[must_use]
283    pub const fn err(error: E) -> Self {
284        Self::Err(error)
285    }
286
287    /// Creates a cancelled outcome with the given reason.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use asupersync::{Outcome, CancelReason};
293    ///
294    /// let outcome: Outcome<i32, &str> = Outcome::cancelled(CancelReason::timeout());
295    /// assert!(outcome.is_cancelled());
296    /// ```
297    #[inline]
298    #[must_use]
299    pub const fn cancelled(reason: CancelReason) -> Self {
300        Self::Cancelled(reason)
301    }
302
303    /// Creates a panicked outcome with the given payload.
304    ///
305    /// # Examples
306    ///
307    /// ```
308    /// use asupersync::{Outcome, PanicPayload};
309    ///
310    /// let outcome: Outcome<i32, &str> = Outcome::panicked(PanicPayload::new("oops"));
311    /// assert!(outcome.is_panicked());
312    /// ```
313    #[inline]
314    #[must_use]
315    pub const fn panicked(payload: PanicPayload) -> Self {
316        Self::Panicked(payload)
317    }
318
319    // =========================================================================
320    // Inspection
321    // =========================================================================
322
323    /// Returns the severity level of this outcome.
324    ///
325    /// The severity levels are ordered: `Ok < Err < Cancelled < Panicked`.
326    /// This is useful for aggregation where the worst outcome should win.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use asupersync::{Outcome, Severity, CancelReason};
332    ///
333    /// let ok: Outcome<i32, &str> = Outcome::ok(42);
334    /// let err: Outcome<i32, &str> = Outcome::err("oops");
335    /// let cancelled: Outcome<i32, &str> = Outcome::cancelled(CancelReason::timeout());
336    ///
337    /// assert_eq!(ok.severity(), Severity::Ok);
338    /// assert_eq!(err.severity(), Severity::Err);
339    /// assert_eq!(cancelled.severity(), Severity::Cancelled);
340    /// assert!(ok.severity() < err.severity());
341    /// ```
342    #[inline]
343    #[must_use]
344    pub const fn severity(&self) -> Severity {
345        match self {
346            Self::Ok(_) => Severity::Ok,
347            Self::Err(_) => Severity::Err,
348            Self::Cancelled(_) => Severity::Cancelled,
349            Self::Panicked(_) => Severity::Panicked,
350        }
351    }
352
353    /// Returns the numeric severity level (0 = Ok, 3 = Panicked).
354    ///
355    /// Prefer [`severity()`][Self::severity] for type-safe comparisons.
356    #[inline]
357    #[must_use]
358    pub const fn severity_u8(&self) -> u8 {
359        self.severity().as_u8()
360    }
361
362    /// Returns true if this is a terminal outcome (any non-pending state).
363    ///
364    /// All `Outcome` variants are terminal states.
365    #[inline]
366    #[must_use]
367    pub const fn is_terminal(&self) -> bool {
368        true // All variants are terminal
369    }
370
371    /// Returns true if this outcome is `Ok`.
372    ///
373    /// # Examples
374    ///
375    /// ```
376    /// use asupersync::Outcome;
377    ///
378    /// let ok: Outcome<i32, &str> = Outcome::ok(42);
379    /// let err: Outcome<i32, &str> = Outcome::err("oops");
380    ///
381    /// assert!(ok.is_ok());
382    /// assert!(!err.is_ok());
383    /// ```
384    #[inline]
385    #[must_use]
386    pub const fn is_ok(&self) -> bool {
387        matches!(self, Self::Ok(_))
388    }
389
390    /// Returns true if this outcome is `Err`.
391    ///
392    /// # Examples
393    ///
394    /// ```
395    /// use asupersync::Outcome;
396    ///
397    /// let err: Outcome<i32, &str> = Outcome::err("oops");
398    /// assert!(err.is_err());
399    /// ```
400    #[inline]
401    #[must_use]
402    pub const fn is_err(&self) -> bool {
403        matches!(self, Self::Err(_))
404    }
405
406    /// Returns true if this outcome is `Cancelled`.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use asupersync::{Outcome, CancelReason};
412    ///
413    /// let cancelled: Outcome<i32, &str> = Outcome::cancelled(CancelReason::timeout());
414    /// assert!(cancelled.is_cancelled());
415    /// ```
416    #[inline]
417    #[must_use]
418    pub const fn is_cancelled(&self) -> bool {
419        matches!(self, Self::Cancelled(_))
420    }
421
422    /// Returns true if this outcome is `Panicked`.
423    ///
424    /// # Examples
425    ///
426    /// ```
427    /// use asupersync::{Outcome, PanicPayload};
428    ///
429    /// let panicked: Outcome<i32, &str> = Outcome::panicked(PanicPayload::new("oops"));
430    /// assert!(panicked.is_panicked());
431    /// ```
432    #[inline]
433    #[must_use]
434    pub const fn is_panicked(&self) -> bool {
435        matches!(self, Self::Panicked(_))
436    }
437
438    /// Converts this outcome to a standard Result, with cancellation and panic as errors.
439    ///
440    /// This is useful when interfacing with code that expects `Result`.
441    #[inline]
442    pub fn into_result(self) -> Result<T, OutcomeError<E>> {
443        match self {
444            Self::Ok(v) => Ok(v),
445            Self::Err(e) => Err(OutcomeError::Err(e)),
446            Self::Cancelled(r) => Err(OutcomeError::Cancelled(r)),
447            Self::Panicked(p) => Err(OutcomeError::Panicked(p)),
448        }
449    }
450
451    /// Maps the success value using the provided function.
452    #[inline]
453    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Outcome<U, E> {
454        match self {
455            Self::Ok(v) => Outcome::Ok(f(v)),
456            Self::Err(e) => Outcome::Err(e),
457            Self::Cancelled(r) => Outcome::Cancelled(r),
458            Self::Panicked(p) => Outcome::Panicked(p),
459        }
460    }
461
462    /// Maps the error value using the provided function.
463    ///
464    /// # Examples
465    ///
466    /// ```
467    /// use asupersync::Outcome;
468    ///
469    /// let err: Outcome<i32, &str> = Outcome::err("short");
470    /// let mapped = err.map_err(str::len);
471    /// assert!(matches!(mapped, Outcome::Err(5)));
472    /// ```
473    #[inline]
474    pub fn map_err<F2, G: FnOnce(E) -> F2>(self, g: G) -> Outcome<T, F2> {
475        match self {
476            Self::Ok(v) => Outcome::Ok(v),
477            Self::Err(e) => Outcome::Err(g(e)),
478            Self::Cancelled(r) => Outcome::Cancelled(r),
479            Self::Panicked(p) => Outcome::Panicked(p),
480        }
481    }
482
483    /// Applies a function to the success value, flattening the result.
484    ///
485    /// This is the monadic bind operation, useful for chaining operations
486    /// that might fail.
487    ///
488    /// # Examples
489    ///
490    /// ```
491    /// use asupersync::Outcome;
492    ///
493    /// fn parse_int(s: &str) -> Outcome<i32, &'static str> {
494    ///     s.parse::<i32>().map_err(|_| "parse error").into()
495    /// }
496    ///
497    /// fn double(x: i32) -> Outcome<i32, &'static str> {
498    ///     Outcome::ok(x * 2)
499    /// }
500    ///
501    /// let result = parse_int("21").and_then(double);
502    /// assert_eq!(result.unwrap(), 42);
503    ///
504    /// let result = parse_int("abc").and_then(double);
505    /// assert!(result.is_err());
506    /// ```
507    #[inline]
508    pub fn and_then<U, F: FnOnce(T) -> Outcome<U, E>>(self, f: F) -> Outcome<U, E> {
509        match self {
510            Self::Ok(v) => f(v),
511            Self::Err(e) => Outcome::Err(e),
512            Self::Cancelled(r) => Outcome::Cancelled(r),
513            Self::Panicked(p) => Outcome::Panicked(p),
514        }
515    }
516
517    /// Returns the success value, or computes a fallback from a closure.
518    ///
519    /// Unlike [`unwrap_or_else`][Self::unwrap_or_else], this returns a `Result`
520    /// instead of another value of `T`.
521    ///
522    /// This intentionally collapses every non-`Ok` outcome (`Err`,
523    /// `Cancelled`, and `Panicked`) into the same lazily computed fallback
524    /// error. Use [`into_result`][Self::into_result] if you need to preserve
525    /// which terminal outcome occurred.
526    ///
527    /// # Examples
528    ///
529    /// ```
530    /// use asupersync::{Outcome, CancelReason};
531    ///
532    /// let ok: Outcome<i32, &str> = Outcome::ok(42);
533    /// let result: Result<i32, &str> = ok.ok_or_else(|| "default error");
534    /// assert_eq!(result, Ok(42));
535    ///
536    /// let cancelled: Outcome<i32, &str> = Outcome::cancelled(CancelReason::timeout());
537    /// let result: Result<i32, &str> = cancelled.ok_or_else(|| "was cancelled");
538    /// assert_eq!(result, Err("was cancelled"));
539    /// ```
540    #[inline]
541    pub fn ok_or_else<F2, G: FnOnce() -> F2>(self, f: G) -> Result<T, F2> {
542        match self {
543            Self::Ok(v) => Ok(v),
544            _ => Err(f()),
545        }
546    }
547
548    /// Joins this outcome with another, returning the outcome with higher severity.
549    ///
550    /// This implements the lattice join operation for aggregating outcomes
551    /// from parallel tasks. The outcome with the worst (highest) severity wins.
552    ///
553    /// # Note on Value Handling
554    ///
555    /// When both outcomes are `Ok`, this method returns `self`. When both are
556    /// `Cancelled`, a strictly stronger [`CancelReason`] is retained. Equal-
557    /// severity cancellation ties remain left-biased and return `self`.
558    ///
559    /// # Examples
560    ///
561    /// ```
562    /// use asupersync::{Outcome, CancelReason};
563    ///
564    /// let ok1: Outcome<i32, &str> = Outcome::ok(1);
565    /// let ok2: Outcome<i32, &str> = Outcome::ok(2);
566    /// let err: Outcome<i32, &str> = Outcome::err("error");
567    /// let cancelled: Outcome<i32, &str> = Outcome::cancelled(CancelReason::timeout());
568    ///
569    /// // Ok + Ok = first Ok (both same severity)
570    /// assert!(ok1.clone().join(ok2).is_ok());
571    ///
572    /// // Ok + Err = Err (Err is worse)
573    /// assert!(ok1.clone().join(err.clone()).is_err());
574    ///
575    /// // Err + Cancelled = Cancelled (Cancelled is worse)
576    /// assert!(err.join(cancelled).is_cancelled());
577    /// ```
578    /// Implements `def.outcome.join_semantics` (#31).
579    /// Left-bias: on equal severity, `self` (left argument) wins. The only
580    /// `Cancelled + Cancelled` special case is when the right-hand cancellation
581    /// reason has strictly higher severity and therefore strengthens the result.
582    /// This is intentional: join is associative on severity, but not fully
583    /// value-commutative. See `law.join.assoc` (#42).
584    #[inline]
585    #[must_use]
586    pub fn join(self, other: Self) -> Self {
587        match (self, other) {
588            (Self::Cancelled(mut left), Self::Cancelled(right)) => {
589                if right.severity() > left.severity() {
590                    left.strengthen(&right);
591                }
592                Self::Cancelled(left)
593            }
594            (left, right) => {
595                if left.severity() >= right.severity() {
596                    left
597                } else {
598                    right
599                }
600            }
601        }
602    }
603
604    // =========================================================================
605    // Unwrap Operations
606    // =========================================================================
607
608    /// Returns the success value or panics.
609    ///
610    /// # Panics
611    ///
612    /// Panics if the outcome is not `Ok`.
613    #[inline]
614    #[track_caller]
615    pub fn unwrap(self) -> T
616    where
617        E: fmt::Debug,
618    {
619        match self {
620            Self::Ok(v) => v,
621            Self::Err(e) => panic!("called `Outcome::unwrap()` on an `Err` value: {e:?}"),
622            Self::Cancelled(r) => {
623                panic!("called `Outcome::unwrap()` on a `Cancelled` value: {r:?}")
624            }
625            Self::Panicked(p) => panic!("called `Outcome::unwrap()` on a `Panicked` value: {p}"),
626        }
627    }
628
629    /// Returns the success value or a default.
630    #[inline]
631    pub fn unwrap_or(self, default: T) -> T {
632        match self {
633            Self::Ok(v) => v,
634            _ => default,
635        }
636    }
637
638    /// Returns the success value or computes it from a closure.
639    #[inline]
640    pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
641        match self {
642            Self::Ok(v) => v,
643            _ => f(),
644        }
645    }
646}
647
648impl<T, E> From<Result<T, E>> for Outcome<T, E> {
649    #[inline]
650    fn from(result: Result<T, E>) -> Self {
651        match result {
652            Ok(v) => Self::Ok(v),
653            Err(e) => Self::Err(e),
654        }
655    }
656}
657
658/// Error type for converting Outcome to Result.
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub enum OutcomeError<E> {
661    /// Application error.
662    Err(E),
663    /// Cancellation.
664    Cancelled(CancelReason),
665    /// Panic.
666    Panicked(PanicPayload),
667}
668
669impl<E: fmt::Display> fmt::Display for OutcomeError<E> {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        match self {
672            Self::Err(e) => write!(f, "{e}"),
673            Self::Cancelled(r) => write!(f, "cancelled: {r}"),
674            Self::Panicked(p) => write!(f, "{p}"),
675        }
676    }
677}
678
679impl<E: fmt::Debug + fmt::Display> std::error::Error for OutcomeError<E> {}
680
681/// Compares two outcomes by severity and returns the worse one.
682///
683/// This implements the lattice join operation.
684///
685/// When both outcomes are `Cancelled`, a strictly stronger [`CancelReason`] is
686/// kept. Equal-severity cancellation ties remain left-biased.
687#[inline]
688pub fn join_outcomes<T, E>(a: Outcome<T, E>, b: Outcome<T, E>) -> Outcome<T, E> {
689    a.join(b)
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695    use serde_json::{Value, json};
696
697    fn scrub_outcome_serde(value: Value) -> Value {
698        let mut scrubbed = value;
699
700        if let Some(message) = scrubbed.pointer_mut("/cancelled/Cancelled/message") {
701            *message = Value::String("[MESSAGE]".to_string());
702        }
703
704        scrubbed
705    }
706
707    fn scrub_outcome_json_ids(value: Value) -> Value {
708        let mut scrubbed = value;
709
710        if let Some(origin_region) = scrubbed.pointer_mut("/Cancelled/origin_region") {
711            *origin_region = json!("[REGION_ID]");
712        }
713
714        if let Some(origin_task) = scrubbed.pointer_mut("/Cancelled/origin_task") {
715            *origin_task = json!("[TASK_ID]");
716        }
717
718        scrubbed
719    }
720
721    // =========================================================================
722    // Severity Ordering Tests
723    // =========================================================================
724
725    #[test]
726    fn severity_ordering() {
727        let ok: Outcome<i32, &str> = Outcome::Ok(42);
728        let err: Outcome<i32, &str> = Outcome::Err("error");
729        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
730        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
731
732        assert!(ok.severity() < err.severity());
733        assert!(err.severity() < cancelled.severity());
734        assert!(cancelled.severity() < panicked.severity());
735    }
736
737    #[test]
738    fn severity_values() {
739        let ok: Outcome<(), ()> = Outcome::Ok(());
740        let err: Outcome<(), ()> = Outcome::Err(());
741        let cancelled: Outcome<(), ()> = Outcome::Cancelled(CancelReason::default());
742        let panicked: Outcome<(), ()> = Outcome::Panicked(PanicPayload::new("test"));
743
744        assert_eq!(ok.severity(), Severity::Ok);
745        assert_eq!(err.severity(), Severity::Err);
746        assert_eq!(cancelled.severity(), Severity::Cancelled);
747        assert_eq!(panicked.severity(), Severity::Panicked);
748    }
749
750    // =========================================================================
751    // Predicate Tests (is_ok, is_err, is_cancelled, is_panicked, is_terminal)
752    // =========================================================================
753
754    #[test]
755    fn is_ok_predicate() {
756        let ok: Outcome<i32, &str> = Outcome::Ok(42);
757        let err: Outcome<i32, &str> = Outcome::Err("error");
758
759        assert!(ok.is_ok());
760        assert!(!err.is_ok());
761    }
762
763    #[test]
764    fn is_err_predicate() {
765        let ok: Outcome<i32, &str> = Outcome::Ok(42);
766        let err: Outcome<i32, &str> = Outcome::Err("error");
767
768        assert!(!ok.is_err());
769        assert!(err.is_err());
770    }
771
772    #[test]
773    fn is_cancelled_predicate() {
774        let ok: Outcome<i32, &str> = Outcome::Ok(42);
775        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
776
777        assert!(!ok.is_cancelled());
778        assert!(cancelled.is_cancelled());
779    }
780
781    #[test]
782    fn is_panicked_predicate() {
783        let ok: Outcome<i32, &str> = Outcome::Ok(42);
784        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
785
786        assert!(!ok.is_panicked());
787        assert!(panicked.is_panicked());
788    }
789
790    #[test]
791    fn is_terminal_always_true() {
792        // All Outcome variants are terminal states
793        let ok: Outcome<i32, &str> = Outcome::Ok(42);
794        let err: Outcome<i32, &str> = Outcome::Err("error");
795        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
796        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
797
798        assert!(ok.is_terminal());
799        assert!(err.is_terminal());
800        assert!(cancelled.is_terminal());
801        assert!(panicked.is_terminal());
802    }
803
804    // =========================================================================
805    // Join Operation Tests (Lattice Laws)
806    // =========================================================================
807
808    #[test]
809    fn join_takes_worse() {
810        let ok: Outcome<i32, &str> = Outcome::Ok(1);
811        let err: Outcome<i32, &str> = Outcome::Err("error");
812
813        let joined = join_outcomes(ok, err);
814        assert!(joined.is_err());
815    }
816
817    #[test]
818    fn join_ok_with_ok_returns_first() {
819        let a: Outcome<i32, &str> = Outcome::Ok(1);
820        let b: Outcome<i32, &str> = Outcome::Ok(2);
821
822        // When equal severity, first argument wins
823        let result = join_outcomes(a, b);
824        assert!(matches!(result, Outcome::Ok(1)));
825    }
826
827    #[test]
828    fn join_err_with_cancelled_returns_cancelled() {
829        let err: Outcome<i32, &str> = Outcome::Err("error");
830        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
831
832        let result = join_outcomes(err, cancelled);
833        assert!(result.is_cancelled());
834    }
835
836    #[test]
837    fn join_panicked_dominates_all() {
838        let ok: Outcome<i32, &str> = Outcome::Ok(1);
839        let err: Outcome<i32, &str> = Outcome::Err("error");
840        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
841        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("panic"));
842
843        assert!(join_outcomes(ok, panicked.clone()).is_panicked());
844        assert!(join_outcomes(err, panicked.clone()).is_panicked());
845        assert!(join_outcomes(cancelled, panicked).is_panicked());
846    }
847
848    #[test]
849    fn join_cancelled_strengthens_to_worst_reason() {
850        let user: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("soft"));
851        let shutdown: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
852
853        let left_first = user.clone().join(shutdown.clone());
854        let right_first = shutdown.join(user);
855
856        match left_first {
857            Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
858            other => panic!("expected cancelled outcome, got {other:?}"),
859        }
860
861        match right_first {
862            Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
863            other => panic!("expected cancelled outcome, got {other:?}"),
864        }
865    }
866
867    #[test]
868    fn join_outcomes_cancelled_strengthens_to_worst_reason() {
869        let user: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("soft"));
870        let shutdown: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::shutdown());
871
872        let joined = join_outcomes(user, shutdown);
873
874        match joined {
875            Outcome::Cancelled(reason) => assert!(reason.is_shutdown()),
876            other => panic!("expected cancelled outcome, got {other:?}"),
877        }
878    }
879
880    #[test]
881    fn join_cancelled_equal_severity_is_left_biased() {
882        use crate::types::CancelKind;
883        let left: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("z-left"));
884        let right: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::user("a-right"));
885
886        let joined = left.join(right);
887
888        match joined {
889            Outcome::Cancelled(reason) => {
890                assert!(reason.is_kind(CancelKind::User));
891                assert_eq!(reason.message(), Some("z-left"));
892            }
893            other => panic!("expected cancelled outcome, got {other:?}"),
894        }
895    }
896
897    #[test]
898    fn join_cancelled_equal_rank_kinds_is_left_biased() {
899        use crate::types::CancelKind;
900        let left: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::timeout());
901        let right: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::deadline());
902
903        let joined = left.join(right);
904
905        match joined {
906            Outcome::Cancelled(reason) => assert!(reason.is_kind(CancelKind::Timeout)),
907            other => panic!("expected cancelled outcome, got {other:?}"),
908        }
909    }
910
911    // =========================================================================
912    // Map Operations Tests
913    // =========================================================================
914
915    #[test]
916    fn map_transforms_ok_value() {
917        let ok: Outcome<i32, &str> = Outcome::Ok(21);
918        let mapped = ok.map(|x| x * 2);
919        assert!(matches!(mapped, Outcome::Ok(42)));
920    }
921
922    #[test]
923    fn map_preserves_err() {
924        let err: Outcome<i32, &str> = Outcome::Err("error");
925        let mapped = err.map(|x| x * 2);
926        assert!(matches!(mapped, Outcome::Err("error")));
927    }
928
929    #[test]
930    fn map_preserves_cancelled() {
931        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
932        let mapped = cancelled.map(|x| x * 2);
933        assert!(mapped.is_cancelled());
934    }
935
936    #[test]
937    fn map_preserves_panicked() {
938        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
939        let mapped = panicked.map(|x| x * 2);
940        assert!(mapped.is_panicked());
941    }
942
943    #[test]
944    fn map_err_transforms_err_value() {
945        let err: Outcome<i32, &str> = Outcome::Err("short");
946        let mapped = err.map_err(str::len);
947        assert!(matches!(mapped, Outcome::Err(5)));
948    }
949
950    #[test]
951    fn map_err_preserves_ok() {
952        let ok: Outcome<i32, &str> = Outcome::Ok(42);
953        let mapped = ok.map_err(str::len);
954        assert!(matches!(mapped, Outcome::Ok(42)));
955    }
956
957    // =========================================================================
958    // Unwrap Operations Tests
959    // =========================================================================
960
961    #[test]
962    fn unwrap_returns_value_on_ok() {
963        let ok: Outcome<i32, &str> = Outcome::Ok(42);
964        assert_eq!(ok.unwrap(), 42);
965    }
966
967    #[test]
968    #[should_panic(expected = "called `Outcome::unwrap()` on an `Err` value")]
969    fn unwrap_panics_on_err() {
970        let err: Outcome<i32, &str> = Outcome::Err("error");
971        let _ = err.unwrap();
972    }
973
974    #[test]
975    #[should_panic(expected = "called `Outcome::unwrap()` on a `Cancelled` value")]
976    fn unwrap_panics_on_cancelled() {
977        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
978        let _ = cancelled.unwrap();
979    }
980
981    #[test]
982    #[should_panic(expected = "called `Outcome::unwrap()` on a `Panicked` value")]
983    fn unwrap_panics_on_panicked() {
984        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
985        let _ = panicked.unwrap();
986    }
987
988    #[test]
989    fn unwrap_or_returns_value_on_ok() {
990        let ok: Outcome<i32, &str> = Outcome::Ok(42);
991        assert_eq!(ok.unwrap_or(0), 42);
992    }
993
994    #[test]
995    fn unwrap_or_returns_default_on_err() {
996        let err: Outcome<i32, &str> = Outcome::Err("error");
997        assert_eq!(err.unwrap_or(0), 0);
998    }
999
1000    #[test]
1001    fn unwrap_or_returns_default_on_cancelled() {
1002        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
1003        assert_eq!(cancelled.unwrap_or(99), 99);
1004    }
1005
1006    #[test]
1007    fn unwrap_or_else_computes_default_lazily() {
1008        let err: Outcome<i32, &str> = Outcome::Err("error");
1009        let mut called = false;
1010        let result = err.unwrap_or_else(|| {
1011            called = true;
1012            42
1013        });
1014        assert!(called);
1015        assert_eq!(result, 42);
1016    }
1017
1018    #[test]
1019    fn unwrap_or_else_doesnt_call_closure_on_ok() {
1020        let ok: Outcome<i32, &str> = Outcome::Ok(42);
1021        let result = ok.unwrap_or_else(|| panic!("should not be called"));
1022        assert_eq!(result, 42);
1023    }
1024
1025    #[test]
1026    fn ok_or_else_collapses_non_ok_variants_to_fallback() {
1027        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
1028        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
1029
1030        assert_eq!(cancelled.ok_or_else(|| "fallback"), Err("fallback"));
1031        assert_eq!(panicked.ok_or_else(|| "fallback"), Err("fallback"));
1032    }
1033
1034    #[test]
1035    fn ok_or_else_doesnt_call_closure_on_ok() {
1036        let ok: Outcome<i32, &str> = Outcome::Ok(42);
1037        let result = ok.ok_or_else(|| panic!("should not be called"));
1038        assert_eq!(result, Ok(42));
1039    }
1040
1041    // =========================================================================
1042    // into_result Conversion Tests
1043    // =========================================================================
1044
1045    #[test]
1046    fn into_result_ok() {
1047        let ok: Outcome<i32, &str> = Outcome::Ok(42);
1048        let result = ok.into_result();
1049        assert!(matches!(result, Ok(42)));
1050    }
1051
1052    #[test]
1053    fn into_result_err() {
1054        let err: Outcome<i32, &str> = Outcome::Err("error");
1055        let result = err.into_result();
1056        assert!(matches!(result, Err(OutcomeError::Err("error"))));
1057    }
1058
1059    #[test]
1060    fn into_result_cancelled() {
1061        let cancelled: Outcome<i32, &str> = Outcome::Cancelled(CancelReason::default());
1062        let result = cancelled.into_result();
1063        assert!(matches!(result, Err(OutcomeError::Cancelled(_))));
1064    }
1065
1066    #[test]
1067    fn into_result_panicked() {
1068        let panicked: Outcome<i32, &str> = Outcome::Panicked(PanicPayload::new("oops"));
1069        let result = panicked.into_result();
1070        assert!(matches!(result, Err(OutcomeError::Panicked(_))));
1071    }
1072
1073    // =========================================================================
1074    // From<Result> Conversion Tests
1075    // =========================================================================
1076
1077    #[test]
1078    fn from_result_ok() {
1079        let result: Result<i32, &str> = Ok(42);
1080        let outcome: Outcome<i32, &str> = Outcome::from(result);
1081        assert!(matches!(outcome, Outcome::Ok(42)));
1082    }
1083
1084    #[test]
1085    fn from_result_err() {
1086        let result: Result<i32, &str> = Err("error");
1087        let outcome: Outcome<i32, &str> = Outcome::from(result);
1088        assert!(matches!(outcome, Outcome::Err("error")));
1089    }
1090
1091    // =========================================================================
1092    // Display Implementations Tests
1093    // =========================================================================
1094
1095    #[test]
1096    fn panic_payload_display() {
1097        let payload = PanicPayload::new("something went wrong");
1098        let display = format!("{payload}");
1099        assert_eq!(display, "panic: something went wrong");
1100    }
1101
1102    #[test]
1103    fn panic_payload_message() {
1104        let payload = PanicPayload::new("test message");
1105        assert_eq!(payload.message(), "test message");
1106    }
1107
1108    #[test]
1109    fn outcome_error_display_err() {
1110        let error: OutcomeError<&str> = OutcomeError::Err("application error");
1111        let display = format!("{error}");
1112        assert_eq!(display, "application error");
1113    }
1114
1115    #[test]
1116    fn outcome_error_display_cancelled() {
1117        let error: OutcomeError<&str> = OutcomeError::Cancelled(CancelReason::default());
1118        let display = format!("{error}");
1119        assert!(display.contains("cancelled"));
1120    }
1121
1122    #[test]
1123    fn outcome_error_display_cancelled_uses_human_readable_reason() {
1124        let error: OutcomeError<&str> =
1125            OutcomeError::Cancelled(CancelReason::timeout().with_message("budget elapsed"));
1126        let display = format!("{error}");
1127        assert_eq!(display, "cancelled: timeout: budget elapsed");
1128        assert!(!display.contains("CancelReason"));
1129    }
1130
1131    #[test]
1132    fn outcome_error_display_panicked() {
1133        let error: OutcomeError<&str> = OutcomeError::Panicked(PanicPayload::new("oops"));
1134        let display = format!("{error}");
1135        assert!(display.contains("panic"));
1136        assert!(display.contains("oops"));
1137    }
1138
1139    #[test]
1140    fn severity_debug_clone_copy_hash() {
1141        use std::collections::HashSet;
1142        let a = Severity::Cancelled;
1143        let b = a; // Copy
1144        let c = a;
1145        assert_eq!(a, b);
1146        assert_eq!(a, c);
1147        let dbg = format!("{a:?}");
1148        assert!(dbg.contains("Cancelled"));
1149        let mut set = HashSet::new();
1150        set.insert(a);
1151        assert!(set.contains(&b));
1152        assert!(!set.contains(&Severity::Ok));
1153    }
1154
1155    #[test]
1156    fn panic_payload_debug_clone_eq() {
1157        let a = PanicPayload::new("boom");
1158        let b = a.clone();
1159        assert_eq!(a, b);
1160        assert_ne!(a, PanicPayload::new("other"));
1161        let dbg = format!("{a:?}");
1162        assert!(dbg.contains("PanicPayload"));
1163    }
1164
1165    #[test]
1166    fn outcome_serde_snapshot_scrubbed() {
1167        insta::assert_json_snapshot!(
1168            "outcome_serde_scrubbed",
1169            scrub_outcome_serde(json!({
1170                "ok": Outcome::<u8, &str>::ok(7),
1171                "err": Outcome::<u8, &str>::err("denied"),
1172                "cancelled": Outcome::<u8, &str>::cancelled(CancelReason::user("req-9f4c36b1")),
1173                "panicked": OutcomeError::<&str>::Panicked(PanicPayload::new("boom")),
1174            }))
1175        );
1176    }
1177
1178    #[test]
1179    fn outcome_json_snapshot_scrubs_ids_only() {
1180        let cancelled: Outcome<(), ()> = Outcome::cancelled(
1181            CancelReason::linked_exit()
1182                .with_region(crate::types::RegionId::new_for_test(42, 7))
1183                .with_task(crate::types::TaskId::new_for_test(9, 3))
1184                .with_timestamp(crate::types::Time::from_nanos(55))
1185                .with_message("upstream closed"),
1186        );
1187
1188        insta::assert_json_snapshot!(
1189            "outcome_json_scrubbed_ids",
1190            scrub_outcome_json_ids(serde_json::to_value(cancelled).expect("serialize outcome"))
1191        );
1192    }
1193}