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