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}