Compare commits
5 Commits
main
...
error-hand
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | b0a467eda2 | |
Savanni D'Gerinel | ebb28c3ae6 | |
Savanni D'Gerinel | 7e14f71eaa | |
Savanni D'Gerinel | 32478d0968 | |
Savanni D'Gerinel | f9079db520 |
|
@ -756,6 +756,14 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-training"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"result-extended",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "etcetera"
|
||||
version = "0.8.0"
|
||||
|
|
|
@ -7,6 +7,7 @@ members = [
|
|||
"cyberpunk-splash",
|
||||
"dashboard",
|
||||
"emseries",
|
||||
"error-training",
|
||||
"file-service",
|
||||
"fluent-ergonomics",
|
||||
"geo-types",
|
||||
|
|
1
build.sh
1
build.sh
|
@ -10,6 +10,7 @@ RUST_ALL_TARGETS=(
|
|||
"cyberpunk-splash"
|
||||
"dashboard"
|
||||
"emseries"
|
||||
"error-training"
|
||||
"file-service"
|
||||
"fluent-ergonomics"
|
||||
"geo-types"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "error-training"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "error_training"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "error-training"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "1" }
|
||||
result-extended = { path = "../result-extended" }
|
|
@ -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<Value,
|
||||
//! FatalError, Error>.
|
||||
|
||||
use super::*;
|
||||
use ::result_extended::{error, fatal, ok, return_fatal, Result};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct DB(HashMap<String, i8>);
|
||||
|
||||
impl DB {
|
||||
pub fn new(lst: Vec<(String, i8)>) -> Self {
|
||||
Self(lst.into_iter().collect::<HashMap<String, i8>>())
|
||||
}
|
||||
|
||||
/// 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<i8, DatabaseError, FatalError> {
|
||||
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<i8, MathError> {
|
||||
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<i8, MathError, FatalError> {
|
||||
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),
|
||||
})
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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")]
|
||||
DivideByZero,
|
||||
|
||||
#[error("result exceeds maxint")]
|
||||
ExceedsMaxint,
|
||||
|
||||
#[error("result exceeds minint")]
|
||||
ExceedsMinint,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, PartialEq)]
|
||||
pub enum DatabaseError {
|
||||
#[error("value not found")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error, PartialEq)]
|
||||
pub enum OperationError {
|
||||
#[error("database error occurred: {0}")]
|
||||
DatabaseError(DatabaseError),
|
||||
|
||||
#[error("math error occurred: {0}")]
|
||||
MathError(MathError),
|
||||
}
|
||||
|
||||
impl From<DatabaseError> for OperationError {
|
||||
fn from(err: DatabaseError) -> Self {
|
||||
Self::DatabaseError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MathError> for OperationError {
|
||||
fn from(err: MathError) -> Self {
|
||||
Self::MathError(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//! Error handling practice.
|
||||
//!
|
||||
//! The purpose of this crate is to demonstrate error handling in a couple of different scenarios
|
||||
//! so that I have clear templates to refer when doing development, instead of hand-waving or
|
||||
//! putting error handling off into the unspecified future.
|
||||
//!
|
||||
//! I am going to demonstrate error handling in the style of [Error Handling in a
|
||||
//! Correctness-Critical Rust Project | sled-rs.github.io](https://sled.rs/errors.html) and in my
|
||||
//! reformulation of it using Flow.
|
||||
//!
|
||||
//! I will also test out additional libraries in the same scenarios:
|
||||
//!
|
||||
//! - anyhow
|
||||
//!
|
||||
//! A database exists with some numbers. Mathmatical calculations will be performed on those
|
||||
//! numbers. Some calculations are invalid and should fail. In some cases, those should be reported
|
||||
//! to the user, and in other cases those can be recovered. Sometimes a calculation needs to be
|
||||
//! performed on a value that doesn't exist, which is also a failure. However, sometimes, the
|
||||
//! database will detect corruption, wich is fatal and should terminate the "app".
|
||||
//!
|
||||
//! In these scenarios, the "app" is a top-level function which runs the scenario. This particular
|
||||
//! app should never crash, just show where crashes would happen.
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
//! Let's figure out a recovery mechanism in terms of a parser. For this example I'm going to
|
||||
//! assume an SGF parser in which a player's rank is specified in some non-standard, but still
|
||||
//! somewhat comprehensible, way.
|
||||
//!
|
||||
//! Correct: 5d
|
||||
//! Incorrect, but recoverable: 5 Dan
|
||||
//!
|
||||
//! In strict mode, the incorrect one would be rejected. In permissive mode, the incorrect one
|
||||
//! would be corrected. I don't know that this actually makes any sense in this context, though.
|
|
@ -0,0 +1,67 @@
|
|||
//! Sled-style error handling is based on Result<Result<Value, LocalError>, 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<String, i8>);
|
||||
|
||||
impl DB {
|
||||
pub fn new(lst: Vec<(String, i8)>) -> Self {
|
||||
Self(lst.into_iter().collect::<HashMap<String, i8>>())
|
||||
}
|
||||
|
||||
/// 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<Result<i8, DatabaseError>, FatalError> {
|
||||
if key == "fail" {
|
||||
Err(FatalError::DatabaseCorruption)
|
||||
} else {
|
||||
Ok(self.0.get(key).copied().ok_or(DatabaseError::NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn op(val: i8) -> Result<i8, MathError> {
|
||||
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<Result<i8, MathError>, 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),
|
||||
}))
|
||||
}
|
|
@ -18,8 +18,6 @@ path = "src/main.rs"
|
|||
name = "auth-cli"
|
||||
path = "src/bin/cli.rs"
|
||||
|
||||
[target.auth-cli.dependencies]
|
||||
|
||||
[dependencies]
|
||||
base64ct = { version = "1", features = [ "alloc" ] }
|
||||
build_html = { version = "2" }
|
||||
|
|
|
@ -141,6 +141,30 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<A, E: Error, FE: FatalError> From<std::result::Result<std::result::Result<A, E>, FE>>
|
||||
for Result<A, E, FE>
|
||||
{
|
||||
fn from(res: std::result::Result<std::result::Result<A, E>, FE>) -> Self {
|
||||
match res {
|
||||
Ok(Ok(v)) => ok(v),
|
||||
Ok(Err(e)) => error(e),
|
||||
Err(e) => fatal(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, E: Error, FE: FatalError> From<Result<A, E, FE>>
|
||||
for std::result::Result<std::result::Result<A, E>, FE>
|
||||
{
|
||||
fn from(res: Result<A, E, FE>) -> std::result::Result<std::result::Result<A, E>, FE> {
|
||||
match res {
|
||||
Result::Ok(v) => Ok(Ok(v)),
|
||||
Result::Err(e) => Ok(Err(e)),
|
||||
Result::Fatal(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create an ok value.
|
||||
pub fn ok<A, E: Error, FE: FatalError>(val: A) -> Result<A, E, FE> {
|
||||
Result::Ok(val)
|
||||
|
|
Loading…
Reference in New Issue