qubit_fs/error/open_failure.rs
1// =============================================================================
2// Copyright (c) 2026 Haixing Hu.
3//
4// SPDX-License-Identifier: Apache-2.0
5// =============================================================================
6//! Opening failures that preserve isolated recovery ownership.
7
8use std::error::Error;
9use std::fmt::Debug;
10use std::fmt::Display;
11use std::fmt::Formatter;
12use std::fmt::Result as FmtResult;
13
14use super::FsError;
15use super::OpenFailureStage;
16
17/// A failed open with its causal error and optional isolated recovery session.
18///
19/// `R` owns only explicit cleanup authority. Keep this error or take its
20/// recovery session before reporting failure. Dropping the error does not
21/// confirm cleanup. There is deliberately no conversion into `FsError` that
22/// discards recovery.
23///
24/// # Examples
25///
26/// ```rust
27/// use qubit_fs::error::{FsError, FsErrorKind, FsOperation, OpenFailure, OpenFailureStage};
28///
29/// assert!(std::any::type_name::<OpenFailure<()>>().contains("OpenFailure"));
30/// let error = FsError::new(FsErrorKind::NotFound, FsOperation::OpenReader, "missing");
31/// assert_eq!(FsOperation::OpenReader, error.operation());
32/// assert_eq!(OpenFailureStage::Preflight, OpenFailureStage::Preflight);
33/// ```
34#[must_use = "inspect and retain recovery ownership before abandoning an open failure"]
35pub struct OpenFailure<R> {
36 /// Original contextual failure, unchanged by later recovery.
37 error: Box<FsError>,
38 /// Stage where opening stopped.
39 stage: OpenFailureStage,
40 /// Isolated session, present when a successfully opened envelope was
41 /// rejected.
42 recovery: Option<R>,
43}
44
45impl<R> OpenFailure<R> {
46 /// Creates a failure after classifying the stage and transferring
47 /// ownership.
48 pub(crate) fn new(error: FsError, stage: OpenFailureStage, recovery: Option<R>) -> Self {
49 Self {
50 error: Box::new(error),
51 stage,
52 recovery,
53 }
54 }
55 /// Returns the original failure; cleanup does not rewrite it.
56 pub const fn error(&self) -> &FsError {
57 &self.error
58 }
59 /// Returns the stage where opening stopped.
60 pub const fn stage(&self) -> OpenFailureStage {
61 self.stage
62 }
63 /// Returns the retained recovery session, or None before an envelope
64 /// arrived.
65 pub const fn recovery(&self) -> Option<&R> {
66 self.recovery.as_ref()
67 }
68 /// Borrows the retained session for explicit cleanup, when available.
69 pub fn recovery_mut(&mut self) -> Option<&mut R> {
70 self.recovery.as_mut()
71 }
72 /// Transfers recovery ownership without changing the historical failure.
73 pub fn take_recovery(&mut self) -> Option<R> {
74 self.recovery.take()
75 }
76 /// Returns all failure facts and transfers any recovery ownership.
77 ///
78 /// # Returns
79 /// The original error, opening stage, and optional retained recovery
80 /// session.
81 pub fn into_parts(self) -> (FsError, OpenFailureStage, Option<R>) {
82 (*self.error, self.stage, self.recovery)
83 }
84}
85impl<R> Debug for OpenFailure<R> {
86 /// Formats failure facts without formatting the session or its claimed
87 /// identity.
88 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
89 f.debug_struct("OpenFailure")
90 .field("error", &self.error)
91 .field("stage", &self.stage)
92 .field("has_recovery", &self.recovery.is_some())
93 .finish()
94 }
95}
96impl<R> Display for OpenFailure<R> {
97 /// Formats only the original contextual failure.
98 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
99 Display::fmt(&self.error, f)
100 }
101}
102impl<R: 'static> Error for OpenFailure<R> {
103 /// Exposes the original error chain without surrendering recovery
104 /// ownership.
105 fn source(&self) -> Option<&(dyn Error + 'static)> {
106 Some(self.error.as_ref())
107 }
108}