Skip to main content

miniextendr_api/
condition.rs

1//! Structured error adapter for `std::error::Error`.
2//!
3//! [`RCondition`] wraps any `E: std::error::Error` and preserves the full
4//! error chain (cause/source) when converting to an R error message.
5//!
6//! # Usage
7//!
8//! Use as the `Err` type in `Result` returns from `#[miniextendr]` functions:
9//!
10//! ```ignore
11//! use miniextendr_api::condition::RCondition;
12//!
13//! #[miniextendr]
14//! fn parse_config(path: &str) -> Result<i32, RCondition<std::io::Error>> {
15//!     let content = std::fs::read_to_string(path).map_err(RCondition)?;
16//!     Ok(content.len() as i32)
17//! }
18//! ```
19//!
20//! The R error message includes the full cause chain:
21//! ```r
22//! tryCatch(parse_config("/nonexistent"), error = function(e) e$message)
23//! # "No such file or directory (os error 2)\n  caused by: ..."
24//! ```
25
26/// Structured error wrapper that preserves the `std::error::Error` cause chain.
27///
28/// When displayed, formats the error message with its full source chain:
29/// ```text
30/// top-level message
31///   caused by: middle error
32///   caused by: root cause
33/// ```
34///
35/// Implements `From<E>` so it works with `?` and `.map_err(RCondition)`.
36///
37/// # Example
38///
39/// ```ignore
40/// use miniextendr_api::condition::RCondition;
41/// use std::num::ParseIntError;
42///
43/// #[miniextendr]
44/// fn parse_number(s: &str) -> Result<i32, RCondition<ParseIntError>> {
45///     s.parse::<i32>().map_err(RCondition)
46/// }
47/// ```
48pub struct RCondition<E: std::error::Error>(pub E);
49
50impl<E: std::error::Error> From<E> for RCondition<E> {
51    #[inline]
52    fn from(err: E) -> Self {
53        RCondition(err)
54    }
55}
56
57impl<E: std::error::Error> std::fmt::Display for RCondition<E> {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        // Write the top-level message
60        write!(f, "{}", self.0)?;
61
62        // Walk the cause chain
63        let mut current: &dyn std::error::Error = &self.0;
64        while let Some(source) = current.source() {
65            write!(f, "\n  caused by: {source}")?;
66            current = source;
67        }
68
69        Ok(())
70    }
71}
72
73impl<E: std::error::Error> std::fmt::Debug for RCondition<E> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        // Debug shows the type name + Display output
76        write!(f, "RCondition<{}>({})", std::any::type_name::<E>(), self)
77    }
78}
79
80impl<E: std::error::Error> RCondition<E> {
81    /// Get the inner error.
82    #[inline]
83    pub fn into_inner(self) -> E {
84        self.0
85    }
86
87    /// Get the Rust type name of the wrapped error (for programmatic matching).
88    #[inline]
89    pub fn rust_type_name(&self) -> &'static str {
90        std::any::type_name::<E>()
91    }
92
93    /// Collect the full cause chain as a `Vec<String>`.
94    pub fn cause_chain(&self) -> Vec<String> {
95        let mut chain = vec![self.0.to_string()];
96        let mut current: &dyn std::error::Error = &self.0;
97        while let Some(source) = current.source() {
98            chain.push(source.to_string());
99            current = source;
100        }
101        chain
102    }
103}