//! Control Flow for Correctness-Critical applications //! //! https://sled.rs/errors.html //! //! Where the sled.rs library uses `Result, FatalError>`, these are a little hard to //! work with. This library works out a set of utility functions that allow us to work with the //! nested errors in the same way as a regular Result. use std::error::Error; /// Implement this trait for the application's fatal errors. /// /// Fatal errors should be things that should trigger an application shutdown. Applications should /// not try to recover from fatal errors, but simply bring the app to the safest shutdown point and /// report the best possible information to the user. /// /// Examples: database corruption, or the database is unavailable in an application that cannot /// function without it. Graphics environment cannot be initialized in a GUI app. /// /// Applications should generally have only one FatalError type. There are no handling utilities /// for Fatal conditions, so Fatal conditions must be handled through an ordinary `match` /// statement. pub trait FatalError: Error {} /// Flow represents a return value that might be a success, might be a fatal error, or /// might be a normal handleable error. pub enum Flow { /// The operation was successful Ok(A), /// The operation encountered a fatal error. These should be bubbled up to a level that can /// safely shut the application down. Fatal(FE), /// Ordinary errors. These should be handled and the application should recover gracefully. Err(E), } impl Flow { /// Apply an infallible function to a successful value. pub fn map(self, mapper: O) -> Flow where O: FnOnce(A) -> B, { match self { Flow::Ok(val) => Flow::Ok(mapper(val)), Flow::Fatal(err) => Flow::Fatal(err), Flow::Err(err) => Flow::Err(err), } } /// Apply a potentially fallible function to a successful value. /// /// Like `Result.and_then`, the mapping function can itself fail. pub fn and_then(self, handler: O) -> Flow where O: FnOnce(A) -> Flow, { match self { Flow::Ok(val) => handler(val), Flow::Fatal(err) => Flow::Fatal(err), Flow::Err(err) => Flow::Err(err), } } /// Map a normal error from one type to another. This is useful for converting an error from /// one type to another, especially in re-throwing an underlying error. `?` syntax does not /// work with `Flow`, so you will likely need to use this a lot. pub fn map_err(self, mapper: O) -> Flow where O: FnOnce(E) -> F, { match self { Flow::Ok(val) => Flow::Ok(val), Flow::Fatal(err) => Flow::Fatal(err), Flow::Err(err) => Flow::Err(mapper(err)), } } /// Provide a function to use to recover from (or simply re-throw) an error. pub fn or_else(self, handler: O) -> Flow where O: FnOnce(E) -> Flow, { match self { Flow::Ok(val) => Flow::Ok(val), Flow::Fatal(err) => Flow::Fatal(err), Flow::Err(err) => handler(err), } } } /// Convert from a normal `Result` type to a `Flow` type. The error condition for a `Result` will /// be treated as `Flow::Err`, never `Flow::Fatal`. impl From> for Flow { fn from(r: Result) -> Self { match r { Ok(val) => Flow::Ok(val), Err(err) => Flow::Err(err), } } } /// Convenience function to create an ok value. pub fn ok(val: A) -> Flow { Flow::Ok(val) } /// Convenience function to create an error value. pub fn error(err: E) -> Flow { Flow::Err(err) } /// Convenience function to create a fatal value. pub fn fatal(err: FE) -> Flow { Flow::Fatal(err) } #[macro_export] /// Return early from the current function if the value is a fatal error. macro_rules! return_fatal { ($x:expr) => { match $x { Flow::Fatal(err) => return Flow::Fatal(err), Flow::Err(err) => Err(err), Flow::Ok(val) => Ok(val), } }; } #[macro_export] /// Return early from the current function is the value is an error. macro_rules! return_error { ($x:expr) => { match $x { Flow::Ok(val) => val, Flow::Err(err) => return Flow::Err(err), Flow::Fatal(err) => return Flow::Fatal(err), } }; } #[cfg(test)] mod test { use super::*; use thiserror::Error; #[derive(Debug, Error)] enum FatalError { #[error("A fatal error occurred")] FatalError, } impl super::FatalError for FatalError {} impl PartialEq for FatalError { fn eq(&self, rhs: &Self) -> bool { true } } #[derive(Debug, Error)] enum Error { #[error("an error occurred")] Error, } impl PartialEq for Error { fn eq(&self, rhs: &Self) -> bool { true } } impl PartialEq for Flow { fn eq(&self, rhs: &Self) -> bool { match (self, rhs) { (Flow::Ok(val), Flow::Ok(rhs)) => val == rhs, (Flow::Err(_), Flow::Err(_)) => true, (Flow::Fatal(_), Flow::Fatal(_)) => true, _ => false, } } } impl std::fmt::Debug for Flow { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Flow::Ok(val) => f.write_fmt(format_args!("Flow::Ok {}", val)), Flow::Err(err) => f.write_fmt(format_args!("Flow::Err {:?}", err)), Flow::Fatal(err) => f.write_fmt(format_args!("Flow::Fatal {:?}", err)), } } } #[test] fn it_can_map_things() { let success = ok(15); assert_eq!(ok(16), success.map(|v| v + 1)); } #[test] fn it_can_chain_success() { let success = ok(15); assert_eq!(ok(16), success.and_then(|v| ok(v + 1))); } #[test] fn it_can_handle_an_error() { let failure = error(Error::Error); assert_eq!(ok(16), failure.or_else(|_| ok(16))); } #[test] fn early_exit_on_fatal() { fn ok_func() -> Flow { let value = return_fatal!(ok::(15)); match value { Ok(_) => ok(14), Err(err) => error(err), } } fn err_func() -> Flow { let value = return_fatal!(error::(Error::Error)); match value { Ok(_) => panic!("shouldn't have gotten here"), Err(err) => ok(0), } } fn fatal_func() -> Flow { return_fatal!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } fatal_func(); assert_eq!(ok_func(), ok(14)); assert_eq!(err_func(), ok(0)); } #[test] fn it_can_early_exit_on_all_errors() { fn ok_func() -> Flow { let value = return_error!(ok::(15)); assert_eq!(value, 15); ok(14) } fn err_func() -> Flow { return_error!(error::(Error::Error)); panic!("failed to bail"); } fn fatal_func() -> Flow { return_error!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } fatal_func(); assert_eq!(ok_func(), ok(14)); err_func(); } }