From fe6097ac5eaa0e445dc90bfe2ace38a679762ac2 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 11 Oct 2023 11:28:04 -0400 Subject: [PATCH] Develop a flow-style implementation of the sled example --- Cargo.lock | 1 + error-training/Cargo.toml | 1 + error-training/src/flow.rs | 76 ++++++++++++++++++++++++++++++++++++ error-training/src/lib.rs | 79 +++----------------------------------- error-training/src/sled.rs | 67 ++++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 74 deletions(-) create mode 100644 error-training/src/flow.rs create mode 100644 error-training/src/sled.rs diff --git a/Cargo.lock b/Cargo.lock index efbd9b9..5e61665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,6 +760,7 @@ dependencies = [ name = "error-training" version = "0.1.0" dependencies = [ + "result-extended", "thiserror", ] diff --git a/error-training/Cargo.toml b/error-training/Cargo.toml index 505c026..2b266cc 100644 --- a/error-training/Cargo.toml +++ b/error-training/Cargo.toml @@ -15,3 +15,4 @@ path = "src/main.rs" [dependencies] thiserror = { version = "1" } +result-extended = { path = "../result-extended" } diff --git a/error-training/src/flow.rs b/error-training/src/flow.rs new file mode 100644 index 0000000..12f07ba --- /dev/null +++ b/error-training/src/flow.rs @@ -0,0 +1,76 @@ +//! Flow-style error handling is conceptually the same as sled-style, but with macros to help +//! out with the data structures. The result of a Flow-able operation is Flow. + +use super::*; +use ::result_extended::{error, fatal, ok, return_error, return_fatal, Result}; +use std::collections::HashMap; + +pub struct DB(HashMap); + +impl DB { + pub fn new(lst: Vec<(String, i8)>) -> Self { + Self(lst.into_iter().collect::>()) + } + + /// Retrieve a value from the database. Throw a fatal error with the "fail" key, but + /// otherwise return either the value or DatabaseError::NotFound. + /// + /// ```rust + /// use error_training::{*, flow::*}; + /// use ::result_extended::Result; + /// + /// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); + /// assert_eq!(db.get("fail"), Result::Fatal(FatalError::DatabaseCorruption)); + /// assert_eq!(db.get("a"), Result::Ok(15)); + /// assert_eq!(db.get("c"), Result::Err(DatabaseError::NotFound)); + /// ``` + pub fn get(&self, key: &str) -> Result { + if key == "fail" { + fatal(FatalError::DatabaseCorruption) + } else { + // Result::from(self.0.get(key).copied().ok_or(DatabaseError::NotFound)) + self.0 + .get(key) + .copied() + .ok_or(DatabaseError::NotFound) + .into() + } + } +} + +fn op(val: i8) -> std::result::Result { + if val as i32 + 120_i32 > (i8::MAX as i32) { + Err(MathError::ExceedsMaxint) + } else { + Ok(val + 120) + } +} + +/// This function exists to test several of the major cases. This is where I will figure +/// out how to handle everything and also have clean code. +/// ```rust +/// use error_training::{*, flow::*}; +/// use ::result_extended::Result; +/// +/// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); +/// assert_eq!(run_op(&db, "a"), Result::Ok(i8::MAX)); +/// assert_eq!(run_op(&db, "b"), Result::Ok(120)); +/// assert_eq!(run_op(&db, "c"), Result::Ok(0)); +/// assert_eq!(run_op(&db, "fail"), Result::Fatal(FatalError::DatabaseCorruption)); +/// ``` +/// +/// I have defined this function such that a database miss becomes a 0 and no operation +/// will be performed on it. Since that is the only database error that can occur, this +/// function can only return a `MathError` or a `FatalError`. +pub fn run_op(db: &DB, key: &str) -> Result { + let res = match return_fatal!(db.get(key)) { + Err(DatabaseError::NotFound) => Ok(0), + Ok(val) => op(val), + }; + + Result::from(res).or_else(|err| match err { + MathError::ExceedsMaxint => ok(i8::MAX), + _ => error(err), + }) +} diff --git a/error-training/src/lib.rs b/error-training/src/lib.rs index 6ddd52e..ac0c9d5 100644 --- a/error-training/src/lib.rs +++ b/error-training/src/lib.rs @@ -1,11 +1,16 @@ use thiserror::Error; +pub mod flow; +pub mod sled; + #[derive(Clone, Debug, Error, PartialEq)] pub enum FatalError { #[error("Database corruption detected")] DatabaseCorruption, } +impl ::result_extended::FatalError for FatalError {} + #[derive(Clone, Debug, Error, PartialEq)] pub enum MathError { #[error("divide by zero is not defined")] @@ -44,77 +49,3 @@ impl From for OperationError { Self::MathError(err) } } - -pub mod sled { - //! Sled-style error handling is based on Result, FatalError>. - //! FatalErrors do not get resolved. LocalErrors get bubbled up until they can be handled. - - use super::*; - use std::collections::HashMap; - - pub struct DB(HashMap); - - impl DB { - pub fn new(lst: Vec<(String, i8)>) -> Self { - Self(lst.into_iter().collect::>()) - } - - /// Retrieve a value from the database. Throw a fatal error with the "fail" key, but - /// otherwise return either the value or DatabaseError::NotFound. - /// - /// ```rust - /// use error_training::{*, sled::*}; - /// - /// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); - /// assert_eq!(db.get("fail"), Err(FatalError::DatabaseCorruption)); - /// assert_eq!(db.get("a"), Ok(Ok(15))); - /// assert_eq!(db.get("c"), Ok(Err(DatabaseError::NotFound))); - /// ``` - pub fn get(&self, key: &str) -> Result, FatalError> { - if key == "fail" { - Err(FatalError::DatabaseCorruption) - } else { - Ok(self.0.get(key).copied().ok_or(DatabaseError::NotFound)) - } - } - } - - fn op(val: i8) -> Result { - if val as i32 + 120_i32 > (i8::MAX as i32) { - Err(MathError::ExceedsMaxint) - } else { - Ok(val + 120) - } - } - - /// This function exists to test several of the major cases. This is where I will figure out - /// how to handle everything and also have clean code. - /// ```rust - /// use error_training::{*, sled::*}; - /// - /// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); - /// assert_eq!(run_op(&db, "a"), Ok(Ok(i8::MAX))); - /// assert_eq!(run_op(&db, "b"), Ok(Ok(120))); - /// assert_eq!(run_op(&db, "c"), Ok(Ok(0))); - /// assert_eq!(run_op(&db, "fail"), Err(FatalError::DatabaseCorruption)); - /// ``` - /// - /// I have defined this function such that a database miss becomes a 0 and no operation will be - /// performed on it. Since that is the only database error that can occur, this function can - /// only return a `MathError` or a `FatalError`. - pub fn run_op(db: &DB, key: &str) -> Result, FatalError> { - let val = db.get(key)?; - - let res = match val { - Err(DatabaseError::NotFound) => Ok(0), - Ok(val) => op(val), - }; - - Ok(match res { - Ok(val) => Ok(val), - Err(MathError::ExceedsMaxint) => Ok(127), - Err(err) => Err(err), - }) - } -} - diff --git a/error-training/src/sled.rs b/error-training/src/sled.rs new file mode 100644 index 0000000..78ab304 --- /dev/null +++ b/error-training/src/sled.rs @@ -0,0 +1,67 @@ +//! Sled-style error handling is based on Result, FatalError>. +//! FatalErrors do not get resolved. LocalErrors get bubbled up until they can be handled. + +use super::*; +use std::collections::HashMap; + +pub struct DB(HashMap); + +impl DB { + pub fn new(lst: Vec<(String, i8)>) -> Self { + Self(lst.into_iter().collect::>()) + } + + /// Retrieve a value from the database. Throw a fatal error with the "fail" key, but + /// otherwise return either the value or DatabaseError::NotFound. + /// + /// ```rust + /// use error_training::{*, sled::*}; + /// + /// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); + /// assert_eq!(db.get("fail"), Err(FatalError::DatabaseCorruption)); + /// assert_eq!(db.get("a"), Ok(Ok(15))); + /// assert_eq!(db.get("c"), Ok(Err(DatabaseError::NotFound))); + /// ``` + pub fn get(&self, key: &str) -> Result, FatalError> { + if key == "fail" { + Err(FatalError::DatabaseCorruption) + } else { + Ok(self.0.get(key).copied().ok_or(DatabaseError::NotFound)) + } + } +} + +fn op(val: i8) -> Result { + if val as i32 + 120_i32 > (i8::MAX as i32) { + Err(MathError::ExceedsMaxint) + } else { + Ok(val + 120) + } +} + +/// This function exists to test several of the major cases. This is where I will figure out +/// how to handle everything and also have clean code. +/// ```rust +/// use error_training::{*, sled::*}; +/// +/// let db = DB::new(vec![("a".to_owned(), 15), ("b".to_owned(), 0)]); +/// assert_eq!(run_op(&db, "a"), Ok(Ok(i8::MAX))); +/// assert_eq!(run_op(&db, "b"), Ok(Ok(120))); +/// assert_eq!(run_op(&db, "c"), Ok(Ok(0))); +/// assert_eq!(run_op(&db, "fail"), Err(FatalError::DatabaseCorruption)); +/// ``` +/// +/// I have defined this function such that a database miss becomes a 0 and no operation will be +/// performed on it. Since that is the only database error that can occur, this function can +/// only return a `MathError` or a `FatalError`. +pub fn run_op(db: &DB, key: &str) -> Result, FatalError> { + let res = match db.get(key)? { + Err(DatabaseError::NotFound) => Ok(0), + Ok(val) => op(val), + }; + + Ok(res.or_else(|err| match err { + MathError::ExceedsMaxint => Ok(127), + err => Err(err), + })) +}