Skip to main content

extendr_api/conditions/
mod.rs

1//! # Condition objects
2//!
3//! In R, a condition is an S3 list with:
4//! - `message`: a character vector describing the condition
5//! - `call`: the call that triggered the condition, or `NULL`
6//! - a class attribute including `"condition"` and optionally `"error"`,
7//!   `"warning"`, or `"message"`
8//!
9//! rlang conditions may include `trace` and `parent` fields, which are
10//! represented as `Option<List>` and `Option<RCondition>` respectively.
11//!
12//! ## Types
13//!
14//! This module provides four types:
15//!
16//! - [`ConditionKind`]: an enum discriminating the base R condition type
17//!   (`condition`, `message`, `warning`, `error`).
18//!
19//! - [`Condition`]: a Rust-native representation. Construct one with [`ConditionBuilder::default()`]. This is the primary type we encourage you to work with for its ergonomics due to its use of std types.
20//!
21//! - [`RCondition`]: a thin wrapper around a [`List`] that already exists in R
22//!   memory as a proper condition object. Use this when receiving or returning
23//!   condition objects at the R boundary.
24//!
25//! - [`ConditionBuilder`]: a builder for [`Condition`]. Create a builder with [`ConditionBuilder::default()`] to start, chain setters, and call `.build()` to produce a [`Condition`].
26//!
27//! ## Conversions
28//!
29//! | From \ To   | `List`  | `RCondition` | `Robj`  | `Condition` |
30//! |-------------|---------|--------------|---------|-------------|
31//! | `Condition` | `From`  | `From`       | `From`  | —           |
32//! | `List`      | —       | `TryFrom`    | blanket | `TryFrom`   |
33//! | `&List`     | —       | `TryFrom`    | —       | `TryFrom`   |
34//! | `RCondition`| `From`  | —            | `From`  | `TryFrom`   |
35//! | `Robj`      | —       | `TryFrom`    | —       | `TryFrom`   |
36//! | `&Robj`     | —       | `TryFrom`    | —       | `TryFrom`   |
37use crate::{
38    robj::Rinternals, Attributes, Error, IntoRobj, Language, List, Operators, Robj, Strings,
39};
40
41/// Discriminates the kind of R condition being constructed.
42#[derive(Copy, Debug, Clone, PartialEq)]
43pub enum ConditionKind {
44    /// A generic condition. Base class: `"condition"`.
45    Condition,
46    /// A message condition. Base classes: `"message"`, `"condition"`.
47    Message,
48    /// A warning condition. Base classes: `"warning"`, `"condition"`.
49    Warning,
50    /// An error condition. Base classes: `"error"`, `"condition"`.
51    Error,
52}
53
54/// Rust-native representation of an R condition.
55///
56/// This is the primary type to work with. Construct one via
57/// [`ConditionBuilder::default()`] and convert it to R types via the
58/// `From`/`Into` impls. To read a condition from R, use `TryFrom<Robj>` or
59/// `TryFrom<List>`.
60#[derive(Debug, Clone, PartialEq)]
61pub struct Condition {
62    /// The condition message. Corresponds to the `message` character vector in R.
63    pub message: Vec<String>,
64    /// The base kind of condition, used to set the R class hierarchy.
65    pub kind: ConditionKind,
66    /// User-defined subclasses prepended to the base classes. `None` means no
67    /// custom classes beyond those implied by `kind`.
68    pub class: Option<Vec<String>>,
69    /// The call that triggered the condition, or `None` for `NULL`.
70    pub call: Option<Language>,
71    /// An optional parent condition, used to chain conditions with rlang.
72    pub parent: Option<RCondition>,
73    /// An optional traceback. The structure is not validated.
74    pub trace: Option<List>,
75}
76
77impl From<Condition> for List {
78    fn from(value: Condition) -> Self {
79        let msg = Strings::from_values(value.message).into_robj();
80
81        let call_robj = value.call.map(|v| v.into()).unwrap_or(Robj::from(()));
82        let parent_robj = value
83            .parent
84            .map(|v| Robj::from(v.0))
85            .unwrap_or(Robj::from(()));
86        let trace_robj = value.trace.map(|v| Robj::from(v)).unwrap_or(Robj::from(()));
87
88        let mut cnd = List::from_pairs([
89            ("message", msg),
90            ("trace", trace_robj),
91            ("parent", parent_robj),
92            ("call", call_robj),
93        ]);
94
95        let base_classes: &[&str] = match value.kind {
96            ConditionKind::Condition => &["condition"],
97            ConditionKind::Message => &["message", "condition"],
98            ConditionKind::Warning => &["warning", "condition"],
99            ConditionKind::Error => &["error", "condition"],
100        };
101
102        let mut class = value.class.unwrap_or_default();
103        class.extend(base_classes.iter().map(|s| s.to_string()));
104        cnd.set_class(&class)
105            .expect("set_class on a list should never fail");
106        cnd
107    }
108}
109
110impl From<Condition> for RCondition {
111    fn from(value: Condition) -> Self {
112        Self(List::from(value))
113    }
114}
115
116impl From<Condition> for Robj {
117    fn from(value: Condition) -> Self {
118        Robj::from(List::from(value))
119    }
120}
121
122impl TryFrom<&List> for Condition {
123    type Error = Error;
124
125    fn try_from(list: &List) -> std::result::Result<Self, Self::Error> {
126        let message: Vec<String> = Strings::try_from(list.dollar("message")?)?.into();
127
128        let cls = list
129            .class()
130            .map(|inner| inner.map(|s| s.to_string()).collect::<Vec<_>>())
131            .unwrap_or_default();
132
133        if !cls.iter().any(|c| c == "condition") {
134            return Err(Error::Other(
135                "object does not inherit from `condition`".into(),
136            ));
137        }
138
139        let base_kinds = ["error", "warning", "message"];
140        let matched: Vec<&str> = base_kinds
141            .iter()
142            .copied()
143            .filter(|k| cls.iter().any(|c| c == k))
144            .collect();
145
146        if matched.len() > 1 {
147            return Err(Error::Other(format!(
148                "ambiguous condition: inherits from multiple base kinds: {}",
149                matched.join(", ")
150            )));
151        }
152
153        let kind = match matched.first().copied() {
154            Some("error") => ConditionKind::Error,
155            Some("warning") => ConditionKind::Warning,
156            Some("message") => ConditionKind::Message,
157            _ => ConditionKind::Condition,
158        };
159
160        let base_classes: &[&str] = match kind {
161            ConditionKind::Condition => &["condition"],
162            ConditionKind::Message => &["message", "condition"],
163            ConditionKind::Warning => &["warning", "condition"],
164            ConditionKind::Error => &["error", "condition"],
165        };
166        let user_class: Vec<String> = cls
167            .iter()
168            .filter(|c| !base_classes.contains(&c.as_str()))
169            .cloned()
170            .collect();
171        let class = if user_class.is_empty() {
172            None
173        } else {
174            Some(user_class)
175        };
176
177        let call = list.dollar("call").ok().and_then(|v| {
178            if v.is_null() {
179                None
180            } else {
181                Language::try_from(&v).ok()
182            }
183        });
184
185        let parent = list.dollar("parent").ok().and_then(|v| {
186            if v.is_null() {
187                None
188            } else {
189                List::try_from(&v).ok().map(RCondition)
190            }
191        });
192
193        let trace = list.dollar("trace").ok().and_then(|v| {
194            if v.is_null() {
195                None
196            } else {
197                List::try_from(&v).ok()
198            }
199        });
200
201        Ok(Condition {
202            message,
203            kind,
204            class,
205            call,
206            parent,
207            trace,
208        })
209    }
210}
211
212impl TryFrom<List> for Condition {
213    type Error = Error;
214
215    fn try_from(value: List) -> std::result::Result<Self, Self::Error> {
216        Condition::try_from(&value)
217    }
218}
219
220impl TryFrom<&Robj> for Condition {
221    type Error = Error;
222
223    fn try_from(value: &Robj) -> std::result::Result<Self, Self::Error> {
224        Condition::try_from(List::try_from(value)?)
225    }
226}
227
228impl TryFrom<Robj> for Condition {
229    type Error = Error;
230
231    fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
232        Condition::try_from(&value)
233    }
234}
235
236/// A validated R condition object held as a [`List`].
237///
238/// Use this as a function argument or return type when exchanging condition
239/// objects with R. To inspect or modify fields, convert to [`Condition`] via
240/// `TryFrom`.
241#[derive(Clone, PartialEq, Debug)]
242pub struct RCondition(pub List);
243
244impl TryFrom<List> for RCondition {
245    type Error = Error;
246
247    fn try_from(value: List) -> std::result::Result<Self, Self::Error> {
248        Condition::try_from(&value)?;
249        Ok(RCondition(value))
250    }
251}
252
253impl TryFrom<&List> for RCondition {
254    type Error = Error;
255
256    fn try_from(value: &List) -> std::result::Result<Self, Self::Error> {
257        Condition::try_from(value)?;
258        Ok(RCondition(value.clone()))
259    }
260}
261
262impl From<RCondition> for List {
263    fn from(value: RCondition) -> Self {
264        value.0
265    }
266}
267
268impl From<RCondition> for Robj {
269    fn from(value: RCondition) -> Self {
270        Robj::from(value.0)
271    }
272}
273
274impl TryFrom<RCondition> for Condition {
275    type Error = Error;
276
277    fn try_from(value: RCondition) -> std::result::Result<Self, Self::Error> {
278        Condition::try_from(value.0)
279    }
280}
281
282impl TryFrom<&Robj> for RCondition {
283    type Error = Error;
284
285    fn try_from(value: &Robj) -> std::result::Result<Self, Self::Error> {
286        RCondition::try_from(List::try_from(value)?)
287    }
288}
289
290impl TryFrom<Robj> for RCondition {
291    type Error = Error;
292
293    fn try_from(value: Robj) -> std::result::Result<Self, Self::Error> {
294        RCondition::try_from(&value)
295    }
296}
297
298/// Builder for constructing a [`Condition`].
299pub struct ConditionBuilder {
300    message: Vec<String>,
301    kind: ConditionKind,
302    class: Option<Vec<String>>,
303    call: Option<Language>,
304    parent: Option<RCondition>,
305    trace: Option<List>,
306}
307
308impl ConditionBuilder {
309    /// Set the condition message. Accepts any iterable of strings.
310    pub fn set_message(mut self, message: impl IntoIterator<Item = impl Into<String>>) -> Self {
311        self.message = message.into_iter().map(|s| s.into()).collect();
312        self
313    }
314
315    /// Set the condition kind, which determines the base R class hierarchy.
316    pub fn set_kind(mut self, kind: ConditionKind) -> Self {
317        self.kind = kind;
318        self
319    }
320
321    /// Set user-defined subclasses. These are prepended to the base classes
322    /// derived from [`ConditionKind`].
323    pub fn set_class(mut self, class: impl IntoIterator<Item = impl Into<String>>) -> Self {
324        self.class = Some(class.into_iter().map(|s| s.into()).collect());
325        self
326    }
327
328    /// Set the call associated with the condition.
329    pub fn set_call(mut self, call: Language) -> Self {
330        self.call = Some(call);
331        self
332    }
333
334    /// Set a parent condition. Accepts anything that converts into [`RCondition`],
335    /// including a [`Condition`].
336    pub fn set_parent(mut self, parent: impl Into<RCondition>) -> Self {
337        self.parent = Some(parent.into());
338        self
339    }
340
341    /// Set the traceback. The structure of the list is not validated.
342    pub fn set_trace(mut self, trace: List) -> Self {
343        self.trace = Some(trace);
344        self
345    }
346
347    /// Consume the builder and produce a [`Condition`].
348    pub fn build(self) -> Condition {
349        Condition {
350            message: self.message,
351            kind: self.kind,
352            class: self.class,
353            call: self.call,
354            parent: self.parent,
355            trace: self.trace,
356        }
357    }
358}
359
360impl Default for ConditionBuilder {
361    fn default() -> Self {
362        Self {
363            message: Vec::new(),
364            kind: ConditionKind::Condition,
365            class: None,
366            call: None,
367            parent: None,
368            trace: None,
369        }
370    }
371}
372
373/// Format an error message with rlang-style bullets.
374pub fn format_cnd_message(header: &str, body: &[&str]) -> String {
375    use std::io::IsTerminal;
376    let use_color = std::io::stderr().is_terminal();
377    let bang = if use_color { "\x1b[33m!\x1b[0m" } else { "!" };
378    let bullet = if use_color {
379        "\x1b[36m\u{2022}\x1b[0m"
380    } else {
381        "\u{2022}"
382    };
383
384    let mut msg = format!("\n{bang} {header}");
385    for line in body {
386        msg.push('\n');
387        msg.push_str(&format!("{bullet} {line}"));
388    }
389    msg
390}
391
392/// Format a warning message with rlang-style bullets.
393pub fn format_warn_message(header: &str, body: &[&str]) -> String {
394    use std::io::IsTerminal;
395    let use_color = std::io::stderr().is_terminal();
396    let bullet = if use_color {
397        "\x1b[36m\u{2022}\x1b[0m"
398    } else {
399        "\u{2022}"
400    };
401
402    let mut msg = header.to_string();
403    for line in body {
404        msg.push('\n');
405        msg.push_str(&format!("{bullet} {line}"));
406    }
407    msg
408}
409
410/// Signal a warning with an rlang-style formatted message.
411#[macro_export]
412macro_rules! warn {
413    ($msg:expr) => {{
414        let formatted = $crate::conditions::format_warn_message($msg, &[]);
415        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
416        unsafe {
417            $crate::Rf_warningcall($crate::R_NilValue, c"%s".as_ptr(), c_msg.as_ptr());
418        }
419    }};
420    ($msg:expr, $body:expr) => {{
421        let formatted = $crate::conditions::format_warn_message($msg, $body);
422        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
423        unsafe {
424            $crate::Rf_warningcall($crate::R_NilValue, c"%s".as_ptr(), c_msg.as_ptr());
425        }
426    }};
427    ($msg:expr, $body:expr, $call:expr) => {{
428        let formatted = $crate::conditions::format_warn_message($msg, $body);
429        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
430        let call_robj = $call.call();
431        unsafe {
432            let call_sexp = call_robj
433                .as_ref()
434                .map(|c| c.get())
435                .unwrap_or($crate::R_NilValue);
436            $crate::Rf_warningcall(call_sexp, c"%s".as_ptr(), c_msg.as_ptr());
437        }
438    }};
439}
440
441/// Signal an error with an rlang-style formatted message.
442#[macro_export]
443macro_rules! abort {
444    ($msg:expr) => {{
445        let formatted = $crate::conditions::format_cnd_message($msg, &[]);
446        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
447        unsafe {
448            $crate::Rf_errorcall($crate::R_NilValue, c"%s".as_ptr(), c_msg.as_ptr());
449        }
450    }};
451
452    ($msg:expr, call = $call:expr) => {{
453        let formatted = $crate::conditions::format_cnd_message($msg, &[]);
454        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
455        let call_robj = $call.call();
456        unsafe {
457            let call_sexp = call_robj
458                .as_ref()
459                .map(|c| c.get())
460                .unwrap_or($crate::R_NilValue);
461            $crate::Rf_errorcall(call_sexp, c"%s".as_ptr(), c_msg.as_ptr());
462        }
463    }};
464
465    ($msg:expr, $body:expr) => {{
466        let formatted = $crate::conditions::format_cnd_message($msg, $body);
467        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
468        unsafe {
469            $crate::Rf_errorcall($crate::R_NilValue, c"%s".as_ptr(), c_msg.as_ptr());
470        }
471    }};
472
473    ($msg:expr, $body:expr, $call:expr) => {{
474        let formatted = $crate::conditions::format_cnd_message($msg, $body);
475        let c_msg = ::std::ffi::CString::new(formatted).unwrap();
476        let call_robj = $call.call();
477        unsafe {
478            let call_sexp = call_robj
479                .as_ref()
480                .map(|c| c.get())
481                .unwrap_or($crate::R_NilValue);
482            $crate::Rf_errorcall(call_sexp, c"%s".as_ptr(), c_msg.as_ptr());
483        }
484    }};
485}
486
487#[cfg(test)]
488mod tests {
489    use extendr_engine::with_r;
490
491    use crate::{
492        conditions::{Condition, ConditionBuilder, ConditionKind, RCondition},
493        Attributes, List, Result, Robj,
494    };
495
496    #[test]
497    fn test_format_cnd_message_no_body() {
498        let msg = super::format_cnd_message("something went wrong", &[]);
499        assert!(msg.contains("something went wrong"));
500    }
501
502    #[test]
503    fn test_format_cnd_message_with_body() {
504        let msg = super::format_cnd_message("something went wrong", &["detail 1", "detail 2"]);
505        assert!(msg.contains("something went wrong"));
506        assert!(msg.contains("detail 1"));
507        assert!(msg.contains("detail 2"));
508    }
509
510    #[test]
511    fn roundtrip_with_class() -> Result<()> {
512        with_r(|| {
513            let cnd = ConditionBuilder::default()
514                .set_kind(ConditionKind::Message)
515                .set_message(["this is a custom message"])
516                .set_class(["class1", "class2"])
517                .build();
518            let c2 = Condition::try_from(RCondition::from(cnd.clone()))?;
519            assert_eq!(cnd, c2);
520            Ok(())
521        })
522    }
523
524    #[test]
525    fn roundtrip_no_class() -> Result<()> {
526        with_r(|| {
527            let cnd = ConditionBuilder::default()
528                .set_kind(ConditionKind::Warning)
529                .set_message(["watch out"])
530                .build();
531            let c2 = Condition::try_from(RCondition::from(cnd.clone()))?;
532            assert_eq!(cnd, c2);
533            Ok(())
534        })
535    }
536
537    #[test]
538    fn roundtrip_via_robj() -> Result<()> {
539        with_r(|| {
540            let cnd = ConditionBuilder::default()
541                .set_kind(ConditionKind::Error)
542                .set_message(["something failed"])
543                .set_class(["my_error"])
544                .build();
545            let robj = Robj::from(cnd.clone());
546            let c2 = Condition::try_from(robj)?;
547            assert_eq!(cnd, c2);
548            Ok(())
549        })
550    }
551
552    #[test]
553    fn roundtrip_all_kinds() -> Result<()> {
554        with_r(|| {
555            for (kind, expected_base) in [
556                (ConditionKind::Condition, "condition"),
557                (ConditionKind::Message, "message"),
558                (ConditionKind::Warning, "warning"),
559                (ConditionKind::Error, "error"),
560            ] {
561                let cnd = ConditionBuilder::default()
562                    .set_kind(kind)
563                    .set_message(["msg"])
564                    .build();
565                let list = List::from(cnd);
566                let cls: Vec<_> = list.class().unwrap().collect();
567                assert!(cls.contains(&expected_base), "missing {expected_base}");
568                assert!(cls.contains(&"condition"), "missing condition");
569            }
570            Ok(())
571        })
572    }
573
574    #[test]
575    fn err_not_a_condition() -> Result<()> {
576        with_r(|| {
577            let list = List::from_pairs([("message", Robj::from("oops"))]);
578            let result = Condition::try_from(&list);
579            assert!(result.is_err());
580            Ok(())
581        })
582    }
583
584    #[test]
585    fn err_not_a_list() -> Result<()> {
586        with_r(|| {
587            let robj = Robj::from("just a string");
588            let result = Condition::try_from(&robj);
589            assert!(result.is_err());
590            Ok(())
591        })
592    }
593
594    #[test]
595    fn err_ambiguous_kind() -> Result<()> {
596        with_r(|| {
597            let mut list =
598                List::from_pairs([("message", Robj::from("oops")), ("call", Robj::from(()))]);
599            list.set_class(&["error", "warning", "condition"]).unwrap();
600            let result = Condition::try_from(&list);
601            assert!(result.is_err());
602            Ok(())
603        })
604    }
605
606    #[test]
607    fn roundtrip_rcondition_to_condition() -> Result<()> {
608        with_r(|| {
609            let cnd = ConditionBuilder::default()
610                .set_kind(ConditionKind::Error)
611                .set_message(["bad thing"])
612                .set_class(["my_err"])
613                .build();
614            let rcnd = RCondition::from(cnd.clone());
615            let c2 = Condition::try_from(rcnd)?;
616            assert_eq!(cnd, c2);
617            Ok(())
618        })
619    }
620}