/* Copyright 2023, Savanni D'Gerinel This file is part of the Luminescent Dreams Tools. Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Lumeto. If not, see . */ //! 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, fmt}; /// 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 {} /// ResultExt represents a return value that might be a success, might be a fatal error, or /// might be a normal handleable error. pub enum ResultExt { /// The operation was successful Ok(A), /// Ordinary errors. These should be handled and the application should recover gracefully. Err(E), /// The operation encountered a fatal error. These should be bubbled up to a level that can /// safely shut the application down. Fatal(FE), } impl ResultExt { /// Apply an infallible function to a successful value. pub fn map(self, mapper: O) -> ResultExt where O: FnOnce(A) -> B, { match self { ResultExt::Ok(val) => ResultExt::Ok(mapper(val)), ResultExt::Err(err) => ResultExt::Err(err), ResultExt::Fatal(err) => ResultExt::Fatal(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) -> ResultExt where O: FnOnce(A) -> ResultExt, { match self { ResultExt::Ok(val) => handler(val), ResultExt::Err(err) => ResultExt::Err(err), ResultExt::Fatal(err) => ResultExt::Fatal(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 `Result`, so you will likely need to use this a lot. pub fn map_err(self, mapper: O) -> ResultExt where O: FnOnce(E) -> F, { match self { ResultExt::Ok(val) => ResultExt::Ok(val), ResultExt::Err(err) => ResultExt::Err(mapper(err)), ResultExt::Fatal(err) => ResultExt::Fatal(err), } } /// Provide a function to use to recover from (or simply re-throw) an error. pub fn or_else(self, handler: O) -> ResultExt where O: FnOnce(E) -> ResultExt, { match self { ResultExt::Ok(val) => ResultExt::Ok(val), ResultExt::Err(err) => handler(err), ResultExt::Fatal(err) => ResultExt::Fatal(err), } } } /// Convert from a normal `Result` type to a `Result` type. The error condition for a `Result` will /// be treated as `Result::Err`, never `Result::Fatal`. impl From> for ResultExt { fn from(r: std::result::Result) -> Self { match r { Ok(val) => ResultExt::Ok(val), Err(err) => ResultExt::Err(err), } } } impl fmt::Debug for ResultExt where A: fmt::Debug, FE: fmt::Debug, E: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ResultExt::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)), ResultExt::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)), ResultExt::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)), } } } impl PartialEq for ResultExt where A: PartialEq, FE: PartialEq, E: PartialEq, { fn eq(&self, rhs: &Self) -> bool { match (self, rhs) { (ResultExt::Ok(val), ResultExt::Ok(rhs)) => val == rhs, (ResultExt::Err(_), ResultExt::Err(_)) => true, (ResultExt::Fatal(_), ResultExt::Fatal(_)) => true, _ => false, } } } /// Convenience function to create an ok value. pub fn ok(val: A) -> ResultExt { ResultExt::Ok(val) } /// Convenience function to create an error value. pub fn error(err: E) -> ResultExt { ResultExt::Err(err) } /// Convenience function to create a fatal value. pub fn fatal(err: FE) -> ResultExt { ResultExt::Fatal(err) } /// Return early from the current function if the value is a fatal error. #[macro_export] macro_rules! return_fatal { ($x:expr) => { match $x { ResultExt::Fatal(err) => return ResultExt::Fatal(err), ResultExt::Err(err) => Err(err), ResultExt::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 { ResultExt::Ok(val) => val, ResultExt::Err(err) => return ResultExt::Err(err), ResultExt::Fatal(err) => return ResultExt::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 } } #[test] fn it_can_map_things() { let success: ResultExt = ok(15); assert_eq!(ok(16), success.map(|v| v + 1)); } #[test] fn it_can_chain_success() { let success: ResultExt = ok(15); assert_eq!(ok(16), success.and_then(|v| ok(v + 1))); } #[test] fn it_can_handle_an_error() { let failure: ResultExt = error(Error::Error); assert_eq!( ok::(16), failure.or_else(|_| ok(16)) ); } #[test] fn early_exit_on_fatal() { fn ok_func() -> ResultExt { let value = return_fatal!(ok::(15)); match value { Ok(_) => ok(14), Err(err) => error(err), } } fn err_func() -> ResultExt { let value = return_fatal!(error::(Error::Error)); match value { Ok(_) => panic!("shouldn't have gotten here"), Err(_) => ok(0), } } fn fatal_func() -> ResultExt { let _ = 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() -> ResultExt { let value = return_error!(ok::(15)); assert_eq!(value, 15); ok(14) } fn err_func() -> ResultExt { return_error!(error::(Error::Error)); panic!("failed to bail"); } fn fatal_func() -> ResultExt { return_error!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } fatal_func(); assert_eq!(ok_func(), ok(14)); err_func(); } }