diff --git a/Cargo.lock b/Cargo.lock index 1bcb68c..4ac8da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4294,6 +4294,7 @@ dependencies = [ "lazy_static", "mime", "mime_guess", + "result-extended", "rusqlite", "rusqlite_migration", "serde 1.0.210", diff --git a/result-extended/src/lib.rs b/result-extended/src/lib.rs index b56be5f..dc8736a 100644 --- a/result-extended/src/lib.rs +++ b/result-extended/src/lib.rs @@ -33,9 +33,9 @@ use std::{error::Error, fmt}; /// statement. pub trait FatalError: Error {} -/// Result represents a return value that might be a success, might be a fatal error, or +/// ResultExt represents a return value that might be a success, might be a fatal error, or /// might be a normal handleable error. -pub enum Result { +pub enum ResultExt { /// The operation was successful Ok(A), /// Ordinary errors. These should be handled and the application should recover gracefully. @@ -45,72 +45,72 @@ pub enum Result { Fatal(FE), } -impl Result { +impl ResultExt { /// Apply an infallible function to a successful value. - pub fn map(self, mapper: O) -> Result + pub fn map(self, mapper: O) -> ResultExt 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), + 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) -> Result + pub fn and_then(self, handler: O) -> ResultExt where - O: FnOnce(A) -> Result, + O: FnOnce(A) -> ResultExt, { match self { - Result::Ok(val) => handler(val), - Result::Err(err) => Result::Err(err), - Result::Fatal(err) => Result::Fatal(err), + 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) -> Result + pub fn map_err(self, mapper: O) -> ResultExt 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), + 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) -> Result + pub fn or_else(self, handler: O) -> ResultExt where - O: FnOnce(E) -> Result, + O: FnOnce(E) -> ResultExt, { match self { - Result::Ok(val) => Result::Ok(val), - Result::Err(err) => handler(err), - Result::Fatal(err) => Result::Fatal(err), + 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 Result { +impl From> for ResultExt { fn from(r: std::result::Result) -> Self { match r { - Ok(val) => Result::Ok(val), - Err(err) => Result::Err(err), + Ok(val) => ResultExt::Ok(val), + Err(err) => ResultExt::Err(err), } } } -impl fmt::Debug for Result +impl fmt::Debug for ResultExt where A: fmt::Debug, FE: fmt::Debug, @@ -118,14 +118,14 @@ where { 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)), + 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 Result +impl PartialEq for ResultExt where A: PartialEq, FE: PartialEq, @@ -133,27 +133,27 @@ where { 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, + (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) -> Result { - Result::Ok(val) +pub fn ok(val: A) -> ResultExt { + ResultExt::Ok(val) } /// Convenience function to create an error value. -pub fn error(err: E) -> Result { - Result::Err(err) +pub fn error(err: E) -> ResultExt { + ResultExt::Err(err) } /// Convenience function to create a fatal value. -pub fn fatal(err: FE) -> Result { - Result::Fatal(err) +pub fn fatal(err: FE) -> ResultExt { + ResultExt::Fatal(err) } /// Return early from the current function if the value is a fatal error. @@ -161,9 +161,9 @@ pub fn fatal(err: FE) -> Result { 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), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), + ResultExt::Err(err) => Err(err), + ResultExt::Ok(val) => Ok(val), } }; } @@ -173,9 +173,9 @@ macro_rules! return_fatal { 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), + ResultExt::Ok(val) => val, + ResultExt::Err(err) => return ResultExt::Err(err), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), } }; } @@ -210,19 +210,19 @@ mod test { #[test] fn it_can_map_things() { - let success: Result = ok(15); + let success: ResultExt = ok(15); assert_eq!(ok(16), success.map(|v| v + 1)); } #[test] fn it_can_chain_success() { - let success: Result = ok(15); + 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: Result = error(Error::Error); + let failure: ResultExt = error(Error::Error); assert_eq!( ok::(16), failure.or_else(|_| ok(16)) @@ -231,7 +231,7 @@ mod test { #[test] fn early_exit_on_fatal() { - fn ok_func() -> Result { + fn ok_func() -> ResultExt { let value = return_fatal!(ok::(15)); match value { Ok(_) => ok(14), @@ -239,7 +239,7 @@ mod test { } } - fn err_func() -> Result { + fn err_func() -> ResultExt { let value = return_fatal!(error::(Error::Error)); match value { Ok(_) => panic!("shouldn't have gotten here"), @@ -247,7 +247,7 @@ mod test { } } - fn fatal_func() -> Result { + fn fatal_func() -> ResultExt { let _ = return_fatal!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } @@ -259,18 +259,18 @@ mod test { #[test] fn it_can_early_exit_on_all_errors() { - fn ok_func() -> Result { + fn ok_func() -> ResultExt { let value = return_error!(ok::(15)); assert_eq!(value, 15); ok(14) } - fn err_func() -> Result { + fn err_func() -> ResultExt { return_error!(error::(Error::Error)); panic!("failed to bail"); } - fn fatal_func() -> Result { + fn fatal_func() -> ResultExt { return_error!(fatal::(FatalError::FatalError)); panic!("failed to bail"); } diff --git a/visions/server/Cargo.toml b/visions/server/Cargo.toml index 8dc0ed8..0d02396 100644 --- a/visions/server/Cargo.toml +++ b/visions/server/Cargo.toml @@ -6,26 +6,27 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -authdb = { path = "../../authdb/" } -http = { version = "1" } -serde_json = { version = "*" } -serde = { version = "1" } -tokio = { version = "1", features = [ "full" ] } -warp = { version = "0.3" } -mime_guess = "2.0.5" -mime = "0.3.17" -uuid = { version = "1.11.0", features = ["v4"] } -tokio-stream = "0.1.16" -typeshare = "1.0.4" -urlencoding = "2.1.3" -thiserror = "2.0.3" -rusqlite = "0.32.1" -rusqlite_migration = { version = "1.3.1", features = ["from-directory"] } -lazy_static = "1.5.0" -include_dir = "0.7.4" -async-trait = "0.1.83" -futures = "0.3.31" -async-std = "1.13.0" +async-std = { version = "1.13.0" } +async-trait = { version = "0.1.83" } +authdb = { path = "../../authdb/" } +futures = { version = "0.3.31" } +http = { version = "1" } +include_dir = { version = "0.7.4" } +lazy_static = { version = "1.5.0" } +mime = { version = "0.3.17" } +mime_guess = { version = "2.0.5" } +result-extended = { path = "../../result-extended" } +rusqlite = { version = "0.32.1" } +rusqlite_migration = { version = "1.3.1", features = ["from-directory"] } +serde = { version = "1" } +serde_json = { version = "*" } +thiserror = { version = "2.0.3" } +tokio = { version = "1", features = [ "full" ] } +tokio-stream = { version = "0.1.16" } +typeshare = { version = "1.0.4" } +urlencoding = { version = "2.1.3" } +uuid = { version = "1.11.0", features = ["v4"] } +warp = { version = "0.3" } [dev-dependencies] cool_asserts = "2.0.3" diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 9bb4c9f..72bc4a1 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -2,13 +2,14 @@ use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use mime::Mime; +use result_extended::{fatal, ok, ResultExt}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, - database::{CharacterId, Database}, - types::{AppError, Message, Tabletop, RGB}, + database::{CharacterId, Database, Error}, + types::{AppError, FatalError, Message, Tabletop, RGB}, }; const DEFAULT_BACKGROUND_COLOR: RGB = RGB { @@ -84,17 +85,26 @@ impl Core { self.0.read().await.tabletop.clone() } - pub async fn get_asset(&self, asset_id: AssetId) -> Result<(Mime, Vec), AppError> { - self.0 - .read() - .await - .asset_store - .get(asset_id.clone()) - .map_err(|err| match err { - asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)), - asset_db::Error::Inaccessible => AppError::Inaccessible(format!("{}", asset_id)), - asset_db::Error::UnexpectedError(err) => AppError::Inaccessible(format!("{}", err)), - }) + pub async fn get_asset( + &self, + asset_id: AssetId, + ) -> ResultExt<(Mime, Vec), AppError, FatalError> { + ResultExt::from( + self.0 + .read() + .await + .asset_store + .get(asset_id.clone()) + .map_err(|err| match err { + asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)), + asset_db::Error::Inaccessible => { + AppError::Inaccessible(format!("{}", asset_id)) + } + asset_db::Error::UnexpectedError(err) => { + AppError::Inaccessible(format!("{}", err)) + } + }), + ) } pub async fn available_images(&self) -> Vec { @@ -112,29 +122,29 @@ impl Core { .collect() } - pub async fn set_background_image(&self, asset: AssetId) -> Result<(), AppError> { + pub async fn set_background_image( + &self, + asset: AssetId, + ) -> ResultExt<(), AppError, FatalError> { let tabletop = { let mut state = self.0.write().await; state.tabletop.background_image = Some(asset.clone()); state.tabletop.clone() }; self.publish(Message::UpdateTabletop(tabletop)).await; - Ok(()) + ok(()) } pub async fn get_charsheet( &self, id: CharacterId, - ) -> Result, AppError> { - Ok(self - .0 - .write() - .await - .db - .charsheet(id) - .await - .unwrap() - .map(|cr| cr.data)) + ) -> ResultExt, AppError, FatalError> { + let mut state = self.0.write().await; + let cr = state.db.character(id).await; + cr.map(|cr| cr.map(|cr| cr.data)).or_else(|err| { + println!("Database error: {:?}", err); + ResultExt::Ok(None) + }) } pub async fn publish(&self, message: Message) { @@ -197,14 +207,14 @@ mod test { #[tokio::test] async fn it_lists_available_images() { let core = test_core(); - let image_paths = core.available_images(); + let image_paths = core.available_images().await; assert_eq!(image_paths.len(), 2); } #[tokio::test] async fn it_retrieves_an_asset() { let core = test_core(); - assert_matches!(core.get_asset(AssetId::from("asset_1")).await, Ok((mime, data)) => { + assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => { assert_eq!(mime.type_(), mime::IMAGE); assert_eq!(data, "abcdefg".as_bytes()); }); @@ -213,7 +223,7 @@ mod test { #[tokio::test] async fn it_can_retrieve_the_default_tabletop() { let core = test_core(); - assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => { + assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, None); }); @@ -222,8 +232,8 @@ mod test { #[tokio::test] async fn it_can_change_the_tabletop_background() { let core = test_core(); - assert_matches!(core.set_background_image(AssetId::from("asset_1")), Ok(())); - assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => { + assert_matches!(core.set_background_image(AssetId::from("asset_1")).await, ResultExt::Ok(())); + assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, Some(AssetId::from("asset_1"))); }); @@ -232,10 +242,13 @@ mod test { #[tokio::test] async fn it_sends_notices_to_clients_on_tabletop_change() { let core = test_core(); - let client_id = core.register_client(); - let mut receiver = core.connect_client(client_id); + let client_id = core.register_client().await; + let mut receiver = core.connect_client(client_id).await; - assert_matches!(core.set_background_image(AssetId::from("asset_1")), Ok(())); + assert_matches!( + core.set_background_image(AssetId::from("asset_1")).await, + ResultExt::Ok(()) + ); match receiver.recv().await { Some(Message::UpdateTabletop(Tabletop { background_color, diff --git a/visions/server/src/database.rs b/visions/server/src/database.rs index df077e0..406264c 100644 --- a/visions/server/src/database.rs +++ b/visions/server/src/database.rs @@ -1,18 +1,18 @@ -use std::{ - path::{Path, PathBuf}, - thread::JoinHandle, -}; +use std::path::Path; use async_std::channel::{bounded, Receiver, Sender}; use async_trait::async_trait; use include_dir::{include_dir, Dir}; use lazy_static::lazy_static; +use result_extended::{error, fatal, ok, return_error, ResultExt}; use rusqlite::Connection; use rusqlite_migration::Migrations; use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use crate::types::FatalError; + static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); lazy_static! { @@ -22,12 +22,6 @@ lazy_static! { #[derive(Debug, Error)] pub enum Error { - #[error("Duplicate item found for id {0}")] - DuplicateItem(String), - - #[error("Unexpected response for message")] - MessageMismatch, - #[error("No response to request")] NoResponse, } @@ -148,14 +142,17 @@ pub struct CharsheetRow { #[async_trait] pub trait Database: Send + Sync { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error>; + async fn character( + &mut self, + id: CharacterId, + ) -> result_extended::ResultExt, Error, FatalError>; } pub struct DiskDb { conn: Connection, } -fn setup_test_database(conn: &Connection) { +fn setup_test_database(conn: &Connection) -> Result<(), FatalError> { let mut gamecount_stmt = conn.prepare("SELECT count(*) FROM games").unwrap(); let mut count = gamecount_stmt.query([]).unwrap(); if count.next().unwrap().unwrap().get::(0) == Ok(0) { @@ -164,27 +161,43 @@ fn setup_test_database(conn: &Connection) { let game_id = format!("{}", Uuid::new_v4()); let char_id = CharacterId::new(); - let mut user_stmt = conn.prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)").unwrap(); - user_stmt.execute((admin_id.clone(), "admin", "abcdefg", true, true)).unwrap(); - user_stmt.execute((user_id.clone(), "savanni", "abcdefg", false, true)).unwrap(); + let mut user_stmt = conn + .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + user_stmt + .execute((admin_id.clone(), "admin", "abcdefg", true, true)) + .unwrap(); + user_stmt + .execute((user_id.clone(), "savanni", "abcdefg", false, true)) + .unwrap(); - let mut game_stmt = conn.prepare("INSERT INTO games VALUES (?, ?)").unwrap(); - game_stmt.execute((game_id.clone(), "Circle of Bluest Sky")).unwrap(); + let mut game_stmt = conn + .prepare("INSERT INTO games VALUES (?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + game_stmt + .execute((game_id.clone(), "Circle of Bluest Sky")) + .unwrap(); - let mut role_stmt = conn.prepare("INSERT INTO roles VALUES (?, ?, ?)").unwrap(); - role_stmt.execute((user_id.clone(), game_id.clone(), "gm")).unwrap(); + let mut role_stmt = conn + .prepare("INSERT INTO roles VALUES (?, ?, ?)") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; + role_stmt + .execute((user_id.clone(), game_id.clone(), "gm")) + .unwrap(); let mut sheet_stmt = conn .prepare("INSERT INTO characters VALUES (?, ?, ?)") - .unwrap(); + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; sheet_stmt.execute((char_id.as_str(), game_id, r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#)) .unwrap(); } + + Ok(()) } impl DiskDb { - pub fn new

(path: Option

) -> Result + pub fn new

(path: Option

) -> Result where P: AsRef, { @@ -192,38 +205,46 @@ impl DiskDb { None => Connection::open(":memory:").expect("to create a memory connection"), Some(path) => Connection::open(path).expect("to create connection"), }; - MIGRATIONS.to_latest(&mut conn).expect("to run migrations"); - setup_test_database(&conn); + MIGRATIONS + .to_latest(&mut conn) + .map_err(|err| FatalError::DatabaseMigrationFailure(format!("{}", err)))?; + + setup_test_database(&conn)?; Ok(DiskDb { conn }) } - fn user(&self, id: UserId) -> Result, Error> { + fn user(&self, id: UserId) -> Result, FatalError> { let mut stmt = self .conn .prepare("SELECT uuid, name, password, admin, enabled WHERE uuid=?") - .unwrap(); + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt - .query_map([id.as_str()], |row| Ok(UserRow { - id: row.get(0).unwrap(), - name: row.get(1).unwrap(), - password: row.get(2).unwrap(), - admin: row.get(3).unwrap(), - enabled: row.get(4).unwrap(), - })).unwrap().collect::, rusqlite::Error>>().unwrap(); + .query_map([id.as_str()], |row| { + Ok(UserRow { + id: row.get(0).unwrap(), + name: row.get(1).unwrap(), + password: row.get(2).unwrap(), + admin: row.get(3).unwrap(), + enabled: row.get(4).unwrap(), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>() + .unwrap(); match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), - _ => unimplemented!(), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } - fn charsheet(&self, id: CharacterId) -> Result, Error> { + fn character(&self, id: CharacterId) -> Result, FatalError> { let mut stmt = self .conn - .prepare("SELECT uuid, game, data FROM charsheet WHERE uuid=?") - .unwrap(); + .prepare("SELECT uuid, game, data FROM characters WHERE uuid=?") + .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt .query_map([id.as_str()], |row| { let data: String = row.get(2).unwrap(); @@ -239,24 +260,24 @@ impl DiskDb { match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), - _ => unimplemented!(), + _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } - fn save_charsheet( + fn save_character( &self, char_id: Option, game_type: String, - charsheet: serde_json::Value, - ) -> Result { + character: serde_json::Value, + ) -> std::result::Result { match char_id { None => { let char_id = CharacterId::new(); let mut stmt = self .conn - .prepare("INSERT INTO charsheet VALUES (?, ?, ?)") + .prepare("INSERT INTO characters VALUES (?, ?, ?)") .unwrap(); - stmt.execute((char_id.as_str(), game_type, charsheet.to_string())) + stmt.execute((char_id.as_str(), game_type, character.to_string())) .unwrap(); Ok(char_id) @@ -264,9 +285,9 @@ impl DiskDb { Some(char_id) => { let mut stmt = self .conn - .prepare("UPDATE charsheet SET data=? WHERE uuid=?") + .prepare("UPDATE characters SET data=? WHERE uuid=?") .unwrap(); - stmt.execute((charsheet.to_string(), char_id.as_str())) + stmt.execute((character.to_string(), char_id.as_str())) .unwrap(); Ok(char_id) @@ -281,7 +302,7 @@ async fn db_handler(db: DiskDb, requestor: Receiver) { println!("Request received: {:?}", req); match req { Request::Charsheet(id) => { - let sheet = db.charsheet(id); + let sheet = db.character(id); println!("sheet retrieved: {:?}", sheet); match sheet { Ok(sheet) => { @@ -318,27 +339,34 @@ impl DbConn { #[async_trait] impl Database for DbConn { - async fn charsheet(&mut self, id: CharacterId) -> Result, Error> { + async fn character( + &mut self, + id: CharacterId, + ) -> ResultExt, Error, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Charsheet(id), }; - self.conn.send(request).await.unwrap(); + + match self.conn.send(request).await { + Ok(()) => (), + Err(_) => return fatal(FatalError::DatabaseConnectionLost), + }; + match rx.recv().await { - Ok(DatabaseResponse::Charsheet(row)) => Ok(row), - Ok(_) => Err(Error::MessageMismatch), - Err(err) => { - println!("error: {:?}", err); - Err(Error::NoResponse) - } + Ok(DatabaseResponse::Charsheet(row)) => ok(row), + // Ok(_) => fatal(FatalError::MessageMismatch), + Err(_err) => error(Error::NoResponse), } } } #[cfg(test)] mod test { + use std::path::PathBuf; + use cool_asserts::assert_matches; use super::*; @@ -346,24 +374,24 @@ mod test { const soren: &'static str = r#"{ "type_": "Candela", "name": "Soren Jensen", "pronouns": "he/him", "circle": "Circle of the Bluest Sky", "style": "dapper gentleman", "catalyst": "a cursed book", "question": "What were the contents of that book?", "nerve": { "type_": "nerve", "drives": { "current": 1, "max": 2 }, "resistances": { "current": 0, "max": 3 }, "move": { "gilded": false, "score": 2 }, "strike": { "gilded": false, "score": 1 }, "control": { "gilded": true, "score": 0 } }, "cunning": { "type_": "cunning", "drives": { "current": 1, "max": 1 }, "resistances": { "current": 0, "max": 3 }, "sway": { "gilded": false, "score": 0 }, "read": { "gilded": false, "score": 0 }, "hide": { "gilded": false, "score": 0 } }, "intuition": { "type_": "intuition", "drives": { "current": 0, "max": 0 }, "resistances": { "current": 0, "max": 3 }, "survey": { "gilded": false, "score": 0 }, "focus": { "gilded": false, "score": 0 }, "sense": { "gilded": false, "score": 0 } }, "role": "Slink", "role_abilities": [ "Scout: If you have time to observe a location, you can spend 1 Intuition to ask a question: What do I notice here that others do not see? What in this place might be of use to us? What path should we follow?" ], "specialty": "Detective", "specialty_abilities": [ "Mind Palace: When you want to figure out how two clues might relate or what path they should point you towards, burn 1 Intution resistance. The GM will give you the information you have deduced." ] }"#; #[test] - fn it_can_retrieve_a_charsheet() { + fn it_can_retrieve_a_character() { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); - assert_matches!(db.charsheet(CharacterId::from("1")), Ok(None)); + assert_matches!(db.character(CharacterId::from("1")), Ok(None)); let js: serde_json::Value = serde_json::from_str(soren).unwrap(); let soren_id = db - .save_charsheet(None, "candela".to_owned(), js.clone()) + .save_character(None, "candela".to_owned(), js.clone()) .unwrap(); - assert_matches!(db.charsheet(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); + assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } #[tokio::test] - async fn it_can_retrieve_a_charsheet_through_conn() { + async fn it_can_retrieve_a_character_through_conn() { let memory_db: Option = None; let mut conn = DbConn::new(memory_db); - assert_matches!(conn.charsheet(CharacterId::from("1")).await, Ok(None)); + assert_matches!(conn.character(CharacterId::from("1")).await, ResultExt::Ok(None)); } } diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 1dbefec..ed8a488 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -1,10 +1,11 @@ use std::future::Future; use futures::{SinkExt, StreamExt}; +use result_extended::{ok, return_error, ResultExt}; use serde::{Deserialize, Serialize}; use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message}; -use crate::{asset_db::AssetId, core::Core, database::CharacterId, types::AppError}; +use crate::{asset_db::AssetId, core::Core, database::CharacterId, types::{AppError, FatalError}}; /* pub async fn handle_auth( @@ -32,25 +33,28 @@ pub async fn handle_auth( pub async fn handler(f: F) -> impl Reply where - F: Future>, AppError>>, + F: Future>, AppError, FatalError>>, { match f.await { - Ok(response) => response, - Err(AppError::NotFound(_)) => Response::builder() + ResultExt::Ok(response) => response, + ResultExt::Err(AppError::NotFound(_)) => Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]) .unwrap(), - Err(_) => Response::builder() + ResultExt::Err(_) => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(vec![]) .unwrap(), + ResultExt::Fatal(err) => { + panic!("Shutting down with fatal error: {:?}", err); + } } } pub async fn handle_file(core: Core, asset_id: AssetId) -> impl Reply { handler(async move { - let (mime, bytes) = core.get_asset(asset_id).await?; - Ok(Response::builder() + let (mime, bytes) = return_error!(core.get_asset(asset_id).await); + ok(Response::builder() .header("application-type", mime.to_string()) .body(bytes) .unwrap()) @@ -67,7 +71,7 @@ pub async fn handle_available_images(core: Core) -> impl Reply { .map(|path| format!("{}", path.as_str())) .collect(); - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body(serde_json::to_vec(&image_paths).unwrap()) @@ -88,7 +92,7 @@ pub async fn handle_register_client(core: Core, _request: RegisterRequest) -> im handler(async move { let client_id = core.register_client().await; - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body( @@ -106,7 +110,7 @@ pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Rep handler(async move { core.unregister_client(client_id); - Ok(Response::builder() + ok(Response::builder() .status(StatusCode::NO_CONTENT) .body(vec![]) .unwrap()) @@ -150,7 +154,7 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl handler(async move { let _ = core.set_background_image(AssetId::from(image_name)).await; - Ok(Response::builder() + ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Methods", "*") .header("Content-Type", "application/json") @@ -162,15 +166,19 @@ pub async fn handle_set_background_image(core: Core, image_name: String) -> impl pub async fn handle_get_charsheet(core: Core, charid: String) -> impl Reply { handler(async move { - let sheet = core.get_charsheet(CharacterId::from(charid)).await.unwrap(); + let sheet = match core.get_charsheet(CharacterId::from(charid)).await { + ResultExt::Ok(sheet) => sheet, + ResultExt::Err(err) => return ResultExt::Err(err), + ResultExt::Fatal(err) => return ResultExt::Fatal(err), + }; match sheet { - Some(sheet) => Ok(Response::builder() + Some(sheet) => ok(Response::builder() .header("Access-Control-Allow-Origin", "*") .header("Content-Type", "application/json") .body(serde_json::to_vec(&sheet).unwrap()) .unwrap()), - None => Ok(Response::builder() + None => ok(Response::builder() .status(StatusCode::NOT_FOUND) .body(vec![]) .unwrap()), diff --git a/visions/server/src/types.rs b/visions/server/src/types.rs index f5c5307..3372703 100644 --- a/visions/server/src/types.rs +++ b/visions/server/src/types.rs @@ -1,14 +1,42 @@ use serde::{Deserialize, Serialize}; +use thiserror::Error; use typeshare::typeshare; use crate::asset_db::AssetId; -#[derive(Debug)] +#[derive(Debug, Error)] +pub enum FatalError { + #[error("Non-unique database key {0}")] + NonUniqueDatabaseKey(String), + + #[error("Database migrations failed {0}")] + DatabaseMigrationFailure(String), + + #[error("Failed to construct a query")] + ConstructQueryFailure(String), + + #[error("Database connection lost")] + DatabaseConnectionLost, + + #[error("Unexpected response for message")] + MessageMismatch, +} + +impl result_extended::FatalError for FatalError {} + +#[derive(Debug, Error)] pub enum AppError { + #[error("something wasn't found {0}")] NotFound(String), + + #[error("object inaccessible {0}")] Inaccessible(String), + + #[error("invalid json {0}")] JsonError(serde_json::Error), + + #[error("wat {0}")] UnexpectedError(String), }