diff --git a/errors/Cargo.lock b/errors/Cargo.lock deleted file mode 100644 index c9f36ca..0000000 --- a/errors/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "errors" -version = "0.1.0" diff --git a/errors/src/lib.rs b/errors/src/lib.rs deleted file mode 100644 index 1128ff5..0000000 --- a/errors/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::error::Error; -use std::result; - -pub trait FatalError {} - -pub type Result = result::Result, FE>; - -pub fn ok(val: A) -> Result { - Ok(Ok(val)) -} - -pub fn error(err: E) -> Result { - Ok(Err(err)) -} - -pub fn fatal(err: FE) -> Result { - Err(err) -} - -#[macro_export] -macro_rules! result { - ($x:expr) => { - match $x { - Ok(Ok(val)) => val, - Ok(Err(err)) => return Ok(Err(err.into())), - Err(err) => return Err(err), - } - }; -} diff --git a/flow/Cargo.lock b/flow/Cargo.lock new file mode 100644 index 0000000..2bf67a4 --- /dev/null +++ b/flow/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "flow" +version = "0.1.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" diff --git a/errors/Cargo.toml b/flow/Cargo.toml similarity index 76% rename from errors/Cargo.toml rename to flow/Cargo.toml index 926ecdd..b90bacc 100644 --- a/errors/Cargo.toml +++ b/flow/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "errors" +name = "flow" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + +[dev-dependencies] +thiserror = "1" diff --git a/flow/src/lib.rs b/flow/src/lib.rs new file mode 100644 index 0000000..10be154 --- /dev/null +++ b/flow/src/lib.rs @@ -0,0 +1,217 @@ +use std::error::Error; + +pub trait FatalError: Error {} + +pub enum Flow { + Ok(A), + Fatal(FE), + Err(E), +} + +impl Flow { + 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), + } + } + + 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), + } + } + + 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)), + } + } + + 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), + } + } +} + +impl From> for Flow { + fn from(r: Result) -> Self { + match r { + Ok(val) => Flow::Ok(val), + Err(err) => Flow::Err(err), + } + } +} + +pub fn ok(val: A) -> Flow { + Flow::Ok(val) +} + +pub fn error(err: E) -> Flow { + Flow::Err(err) +} + +pub fn fatal(err: FE) -> Flow { + Flow::Fatal(err) +} + +#[macro_export] +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] +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(); + } +}