Skip to main content

core_crypto/error/
recursive.rs

1#![allow(missing_docs)]
2
3/// Attach context to an error which [`RecursiveError`] knows how to wrap.
4///
5/// By forcing ourselves to map errors everywhere in order for question mark operators to work, we ensure that we can
6/// take the opportunity to include a little bit of manual context. Pervasively done, this means that our errors have
7/// quite a lot of contextual information about the call stack and what precisely has gone wrong.
8///
9/// Implementing this trait is what lets [`RecursiveError::context`] select the right variant from the error's type
10/// alone, so most call sites never need to name a variant. It also lets generic code wrap an arbitrary
11/// module error without knowing which one it has; `core-crypto-ffi` relies on that.
12pub trait ToRecursiveError {
13    /// Construct a recursive error given the current context
14    fn construct_recursive(self, context: &'static str) -> RecursiveError;
15}
16
17/// Build [`RecursiveError`] from a list of `Variant: SourceType => constructor` entries.
18///
19/// Each entry generates the enum variant, a `RecursiveError::<constructor>` helper, the
20/// [`ToRecursiveError`] impl for the source type, and the `Display` and `Error::source` arms, so
21/// wrapping a new error type is a one-line change here instead of four coordinated edits.
22///
23/// We hand-roll `Error` rather than deriving `thiserror::Error`, and that is deliberate:
24/// given `#[source] source: Box<E>`, thiserror hands `&Box<E>` to the error chain, and
25/// `downcast_ref::<E>()` does not match that. Both the ffi error mapping in `core-crypto-ffi` and
26/// the `innermost_source_matches!` test helper downcast to the concrete error type, so the chain
27/// has to expose `&E` via `Box::as_ref`.
28macro_rules! recursive_error {
29    ($(
30        $( #[cfg($meta:meta)] )*
31        $variant:ident : $source:path => $constructor:ident ;
32    )+) => {
33        /// These errors wrap each of the module-specific errors in CoreCrypto.
34        ///
35        /// The goal here is to reduce the need to redeclare each of these error
36        /// types as an individual variant of a module-specific error type.
37        #[derive(Debug, derive_more::Display)]
38        pub enum RecursiveError {
39            $(
40                $( #[cfg($meta)] )*
41                #[display("{context}")]
42                $variant {
43                    context: &'static str,
44                    source: Box<$source>,
45                },
46            )+
47        }
48
49        impl RecursiveError {
50            $(
51                /// Wrap an error convertible into
52                // deliberately not an intra-doc link: some of these targets are `pub(crate)`, and a
53                // public doc comment may not link to a private item.
54                #[doc = concat!("`", stringify!($source), "`,")]
55                /// given the current context.
56                ///
57                /// Prefer [`Self::context`] unless you are relying on the `Into` conversion.
58                $( #[cfg($meta)] )*
59                pub fn $constructor<E: Into<$source>>(context: &'static str) -> impl FnOnce(E) -> Self {
60                    move |into_source| Self::$variant {
61                        context,
62                        source: Box::new(into_source.into()),
63                    }
64                }
65            )+
66        }
67
68        impl std::error::Error for RecursiveError {
69            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
70                match self {
71                    $(
72                        $( #[cfg($meta)] )*
73                        Self::$variant { source, .. } => Some(source.as_ref()),
74                    )+
75                }
76            }
77        }
78
79        $(
80            $( #[cfg($meta)] )*
81            impl ToRecursiveError for $source {
82                fn construct_recursive(self, context: &'static str) -> RecursiveError {
83                    RecursiveError::$variant {
84                        context,
85                        source: Box::new(self),
86                    }
87                }
88            }
89        )+
90    };
91}
92
93recursive_error! {
94    Root: crate::Error => root;
95    TransactionContext: crate::transaction_context::Error => transaction;
96    E2e: wire_e2e_identity::E2eIdentityError => e2e_identity;
97    MlsClient: crate::mls::session::Error => mls_client;
98    MlsConversation: crate::mls::conversation::Error => mls_conversation;
99    MlsCredential: crate::mls::credential::Error => mls_credential;
100    #[cfg(test)]
101    Test: crate::test_utils::TestError => test;
102}
103
104impl RecursiveError {
105    /// Wrap any error which [`RecursiveError`] knows how to wrap, given the current context.
106    ///
107    /// The variant is selected from the error's type, so this is usually what you want:
108    ///
109    /// ```ignore
110    /// self.session().await.map_err(RecursiveError::context("getting mls client"))?
111    /// ```
112    ///
113    /// This only accepts the module error types themselves. When you need to convert first, for
114    /// instance because you are holding a [`crate::TlsCodecError`] which several module errors
115    /// can absorb, reach for the variant-specific constructor such as
116    /// [`Self::mls_conversation`] instead.
117    pub fn context<E: ToRecursiveError>(context: &'static str) -> impl FnOnce(E) -> Self {
118        move |source| source.construct_recursive(context)
119    }
120}