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