use crate::UITree;
use std::panic::AssertUnwindSafe;
pub enum BoundaryResult<Msg> {
Ok(UITree<Msg>),
Recovered {
fallback: UITree<Msg>,
panic: Option<String>,
},
}
pub fn error_boundary<Msg: Clone + 'static>(
primary: impl FnOnce() -> UITree<Msg>,
fallback: impl FnOnce() -> UITree<Msg>,
) -> BoundaryResult<Msg> {
match std::panic::catch_unwind(AssertUnwindSafe(primary)) {
Ok(tree) => BoundaryResult::Ok(tree),
Err(payload) => {
let panic = payload_as_string(&payload);
let fallback = fallback();
BoundaryResult::Recovered { fallback, panic }
}
}
}
pub fn recover_or<Msg: Clone + 'static>(
primary: impl FnOnce() -> UITree<Msg>,
fallback: impl FnOnce() -> UITree<Msg>,
) -> UITree<Msg> {
match error_boundary(primary, fallback) {
BoundaryResult::Ok(tree) | BoundaryResult::Recovered { fallback: tree, .. } => tree,
}
}
fn payload_as_string(payload: &Box<dyn std::any::Any + Send>) -> Option<String> {
payload
.downcast_ref::<&str>()
.map(|s| (*s).to_string())
.or_else(|| payload.downcast_ref::<String>().cloned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ContainerBuilder, NodeKind};
fn button_ok<Msg>() -> UITree<Msg> {
UITree::container(|c| {
c.button("ok");
})
}
fn button_boom<Msg>() -> UITree<Msg> {
UITree::container(|c| {
c.button("ok");
panic!("boom");
})
}
fn fallback_tree<Msg>() -> UITree<Msg> {
UITree::container(|c| {
c.text("recovered");
})
}
#[test]
fn primary_ok_returns_primary() {
let r = error_boundary(button_ok::<()>, fallback_tree::<()>);
assert!(matches!(r, BoundaryResult::Ok(_)));
}
#[test]
fn panic_recovers_with_fallback() {
let r = error_boundary(button_boom::<()>, fallback_tree::<()>);
match r {
BoundaryResult::Recovered { fallback, panic } => {
let NodeKind::Container { children } = &fallback.kind else {
panic!("expected container");
};
assert!(matches!(children[0].kind, NodeKind::Text { .. }));
assert_eq!(panic.as_deref(), Some("boom"));
}
BoundaryResult::Ok(_) => panic!("expected recovery"),
}
}
#[test]
fn recover_or_returns_tree_either_way() {
let ok = recover_or(button_ok::<()>, fallback_tree::<()>);
let recovered = recover_or(button_boom::<()>, fallback_tree::<()>);
assert!(matches!(ok.kind, NodeKind::Container { .. }));
assert!(matches!(recovered.kind, NodeKind::Container { .. }));
}
#[test]
fn recovers_with_empty_children_primary() {
let _ = ContainerBuilder::<()>::new();
}
}