1use crate::{
38 robj::Rinternals, Attributes, Error, IntoRobj, Language, List, Operators, Robj, Strings,
39};
40
41#[derive(Copy, Debug, Clone, PartialEq)]
43pub enum ConditionKind {
44 Condition,
46 Message,
48 Warning,
50 Error,
52}
53
54#[derive(Debug, Clone, PartialEq)]
61pub struct Condition {
62 pub message: Vec<String>,
64 pub kind: ConditionKind,
66 pub class: Option<Vec<String>>,
69 pub call: Option<Language>,
71 pub parent: Option<RCondition>,
73 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#[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
298pub 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 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 pub fn set_kind(mut self, kind: ConditionKind) -> Self {
317 self.kind = kind;
318 self
319 }
320
321 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 pub fn set_call(mut self, call: Language) -> Self {
330 self.call = Some(call);
331 self
332 }
333
334 pub fn set_parent(mut self, parent: impl Into<RCondition>) -> Self {
337 self.parent = Some(parent.into());
338 self
339 }
340
341 pub fn set_trace(mut self, trace: List) -> Self {
343 self.trace = Some(trace);
344 self
345 }
346
347 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
373pub 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
392pub 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#[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#[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}