283 lines
9.0 KiB
Rust
283 lines
9.0 KiB
Rust
/*
|
|
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
|
|
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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
//! Control Flow for Correctness-Critical applications
|
|
//!
|
|
//! https://sled.rs/errors.html
|
|
//!
|
|
//! Where the sled.rs library uses `Result<Result<A, Error>, 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 {}
|
|
|
|
/// Result<A, FE, E> represents a return value that might be a success, might be a fatal error, or
|
|
/// might be a normal handleable error.
|
|
pub enum Result<A, E, FE> {
|
|
/// 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<A, E, FE> Result<A, E, FE> {
|
|
/// Apply an infallible function to a successful value.
|
|
pub fn map<B, O>(self, mapper: O) -> Result<B, E, FE>
|
|
where
|
|
O: FnOnce(A) -> B,
|
|
{
|
|
match self {
|
|
Result::Ok(val) => Result::Ok(mapper(val)),
|
|
Result::Err(err) => Result::Err(err),
|
|
Result::Fatal(err) => Result::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<B, O>(self, handler: O) -> Result<B, E, FE>
|
|
where
|
|
O: FnOnce(A) -> Result<B, E, FE>,
|
|
{
|
|
match self {
|
|
Result::Ok(val) => handler(val),
|
|
Result::Err(err) => Result::Err(err),
|
|
Result::Fatal(err) => Result::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<F, O>(self, mapper: O) -> Result<A, F, FE>
|
|
where
|
|
O: FnOnce(E) -> F,
|
|
{
|
|
match self {
|
|
Result::Ok(val) => Result::Ok(val),
|
|
Result::Err(err) => Result::Err(mapper(err)),
|
|
Result::Fatal(err) => Result::Fatal(err),
|
|
}
|
|
}
|
|
|
|
/// Provide a function to use to recover from (or simply re-throw) an error.
|
|
pub fn or_else<O, F>(self, handler: O) -> Result<A, F, FE>
|
|
where
|
|
O: FnOnce(E) -> Result<A, F, FE>,
|
|
{
|
|
match self {
|
|
Result::Ok(val) => Result::Ok(val),
|
|
Result::Err(err) => handler(err),
|
|
Result::Fatal(err) => Result::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<A, E, FE> From<std::result::Result<A, E>> for Result<A, E, FE> {
|
|
fn from(r: std::result::Result<A, E>) -> Self {
|
|
match r {
|
|
Ok(val) => Result::Ok(val),
|
|
Err(err) => Result::Err(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<A, E, FE> fmt::Debug for Result<A, E, FE>
|
|
where
|
|
A: fmt::Debug,
|
|
FE: fmt::Debug,
|
|
E: fmt::Debug,
|
|
{
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Result::Ok(val) => f.write_fmt(format_args!("Result::Ok {:?}", val)),
|
|
Result::Err(err) => f.write_fmt(format_args!("Result::Err {:?}", err)),
|
|
Result::Fatal(err) => f.write_fmt(format_args!("Result::Fatal {:?}", err)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<A, E, FE> PartialEq for Result<A, E, FE>
|
|
where
|
|
A: PartialEq,
|
|
FE: PartialEq,
|
|
E: PartialEq,
|
|
{
|
|
fn eq(&self, rhs: &Self) -> bool {
|
|
match (self, rhs) {
|
|
(Result::Ok(val), Result::Ok(rhs)) => val == rhs,
|
|
(Result::Err(_), Result::Err(_)) => true,
|
|
(Result::Fatal(_), Result::Fatal(_)) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convenience function to create an ok value.
|
|
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> Result<A, E, FE> {
|
|
Result::Ok(val)
|
|
}
|
|
|
|
/// Convenience function to create an error value.
|
|
pub fn error<A, E: Error, FE: FatalError>(err: E) -> Result<A, E, FE> {
|
|
Result::Err(err)
|
|
}
|
|
|
|
/// Convenience function to create a fatal value.
|
|
pub fn fatal<A, E: Error, FE: FatalError>(err: FE) -> Result<A, E, FE> {
|
|
Result::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 {
|
|
Result::Fatal(err) => return Result::Fatal(err),
|
|
Result::Err(err) => Err(err),
|
|
Result::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 {
|
|
Result::Ok(val) => val,
|
|
Result::Err(err) => return Result::Err(err),
|
|
Result::Fatal(err) => return Result::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: Result<i32, Error, FatalError> = ok(15);
|
|
assert_eq!(ok(16), success.map(|v| v + 1));
|
|
}
|
|
|
|
#[test]
|
|
fn it_can_chain_success() {
|
|
let success: Result<i32, Error, FatalError> = ok(15);
|
|
assert_eq!(ok(16), success.and_then(|v| ok(v + 1)));
|
|
}
|
|
|
|
#[test]
|
|
fn it_can_handle_an_error() {
|
|
let failure: Result<i32, Error, FatalError> = error(Error::Error);
|
|
assert_eq!(
|
|
ok::<i32, Error, FatalError>(16),
|
|
failure.or_else(|_| ok(16))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn early_exit_on_fatal() {
|
|
fn ok_func() -> Result<i32, Error, FatalError> {
|
|
let value = return_fatal!(ok::<i32, Error, FatalError>(15));
|
|
match value {
|
|
Ok(_) => ok(14),
|
|
Err(err) => error(err),
|
|
}
|
|
}
|
|
|
|
fn err_func() -> Result<i32, Error, FatalError> {
|
|
let value = return_fatal!(error::<i32, Error, FatalError>(Error::Error));
|
|
match value {
|
|
Ok(_) => panic!("shouldn't have gotten here"),
|
|
Err(_) => ok(0),
|
|
}
|
|
}
|
|
|
|
fn fatal_func() -> Result<i32, Error, FatalError> {
|
|
let _ = return_fatal!(fatal::<i32, Error, FatalError>(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() -> Result<i32, Error, FatalError> {
|
|
let value = return_error!(ok::<i32, Error, FatalError>(15));
|
|
assert_eq!(value, 15);
|
|
ok(14)
|
|
}
|
|
|
|
fn err_func() -> Result<i32, Error, FatalError> {
|
|
return_error!(error::<i32, Error, FatalError>(Error::Error));
|
|
panic!("failed to bail");
|
|
}
|
|
|
|
fn fatal_func() -> Result<i32, Error, FatalError> {
|
|
return_error!(fatal::<i32, Error, FatalError>(FatalError::FatalError));
|
|
panic!("failed to bail");
|
|
}
|
|
|
|
fatal_func();
|
|
assert_eq!(ok_func(), ok(14));
|
|
err_func();
|
|
}
|
|
}
|