use crate::{
core::{Cmd, Model},
error::{Error, Result},
event::Event,
};
pub trait FallibleModel: Model {
fn try_update(&mut self, event: Event<Self::Message>) -> Result<Cmd<Self::Message>> {
Ok(self.update(event))
}
fn handle_error(&mut self, error: Error) -> Cmd<Self::Message> {
eprintln!("Error in model update: {}", error);
let mut current_error: &dyn std::error::Error = &error;
while let Some(source) = current_error.source() {
eprintln!(" Caused by: {}", source);
current_error = source;
}
Cmd::noop()
}
fn handle_panic(&mut self, panic_info: String) -> Cmd<Self::Message> {
let error = Error::Model(format!("Panic in update: {}", panic_info));
self.handle_error(error)
}
}
pub trait FallibleModelExt: FallibleModel {
fn update_with_error_handling(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
match self.try_update(event) {
Ok(cmd) => cmd,
Err(err) => self.handle_error(err),
}
}
fn update_with_panic_catching(&mut self, event: Event<Self::Message>) -> Cmd<Self::Message> {
use std::panic;
match panic::catch_unwind(panic::AssertUnwindSafe(|| self.try_update(event))) {
Ok(Ok(cmd)) => cmd,
Ok(Err(err)) => self.handle_error(err),
Err(panic) => {
let panic_info = if let Some(s) = panic.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
self.handle_panic(panic_info)
}
}
}
}
impl<T: FallibleModel> FallibleModelExt for T {}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
enum TestMsg {
Succeed,
Fail,
Panic,
#[allow(dead_code)]
ErrorOccurred(String),
}
struct TestModel {
success_count: usize,
error_count: usize,
panic_count: usize,
last_error: Option<String>,
errors: Arc<Mutex<Vec<String>>>,
}
impl Model for TestModel {
type Message = TestMsg;
fn update(&mut self, event: Event<TestMsg>) -> Cmd<TestMsg> {
self.update_with_error_handling(event)
}
fn view(&self) -> String {
format!(
"Success: {} | Errors: {}",
self.success_count, self.error_count
)
}
}
impl FallibleModel for TestModel {
fn try_update(&mut self, event: Event<TestMsg>) -> Result<Cmd<TestMsg>> {
match event {
Event::User(TestMsg::Succeed) => {
self.success_count += 1;
Ok(Cmd::noop())
}
Event::User(TestMsg::Fail) => Err(Error::Model("Intentional failure".to_string())),
Event::User(TestMsg::Panic) => {
panic!("Intentional panic!");
}
_ => Ok(Cmd::noop()),
}
}
fn handle_error(&mut self, error: Error) -> Cmd<TestMsg> {
self.error_count += 1;
let error_str = error.to_string();
self.last_error = Some(error_str.clone());
self.errors.lock().unwrap().push(error_str.clone());
commands::custom(move || Some(TestMsg::ErrorOccurred(error_str)))
}
fn handle_panic(&mut self, panic_info: String) -> Cmd<TestMsg> {
self.panic_count += 1;
self.last_error = Some(panic_info.clone());
self.errors.lock().unwrap().push(panic_info.clone());
commands::custom(|| Some(TestMsg::ErrorOccurred(panic_info)))
}
}
#[test]
fn test_fallible_model_success() {
let mut model = TestModel {
success_count: 0,
error_count: 0,
panic_count: 0,
last_error: None,
errors: Arc::new(Mutex::new(Vec::new())),
};
let _cmd = model.update(Event::User(TestMsg::Succeed));
assert_eq!(model.success_count, 1);
assert_eq!(model.error_count, 0);
}
#[test]
fn test_fallible_model_error() {
let mut model = TestModel {
success_count: 0,
error_count: 0,
panic_count: 0,
last_error: None,
errors: Arc::new(Mutex::new(Vec::new())),
};
let _cmd = model.update(Event::User(TestMsg::Fail));
assert_eq!(model.error_count, 1);
assert!(model.last_error.is_some());
assert!(model.last_error.unwrap().contains("Intentional failure"));
}
#[test]
fn test_fallible_model_panic_catching() {
let mut model = TestModel {
success_count: 0,
error_count: 0,
panic_count: 0,
last_error: None,
errors: Arc::new(Mutex::new(Vec::new())),
};
let _cmd = model.update_with_panic_catching(Event::User(TestMsg::Panic));
assert_eq!(model.panic_count, 1);
assert!(model.last_error.is_some());
}
#[test]
fn test_default_error_handling() {
struct DefaultModel;
impl Model for DefaultModel {
type Message = ();
fn update(&mut self, _: Event<()>) -> Cmd<()> {
Cmd::noop()
}
fn view(&self) -> String {
"DefaultModel".to_string()
}
}
impl FallibleModel for DefaultModel {}
let mut model = DefaultModel;
let _cmd = model.handle_error(Error::Model("test".to_string()));
}
}