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 rusqlite::{ types::{FromSql, FromSqlResult, ValueRef}, Connection, }; use rusqlite_migration::Migrations; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::types::FatalError; static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); lazy_static! { static ref MIGRATIONS: Migrations<'static> = Migrations::from_directory(&MIGRATIONS_DIR).unwrap(); } #[derive(Debug)] enum Request { Charsheet(CharacterId), CreateSession(UserId), Games, SaveUser(Option, String, String, bool, bool), Session(SessionId), User(UserId), UserByUsername(String), Users, } #[derive(Debug)] struct DatabaseRequest { tx: Sender, req: Request, } #[derive(Debug)] enum DatabaseResponse { Charsheet(Option), CreateSession(SessionId), Games(Vec), SaveUser(UserId), Session(Option), User(Option), Users(Vec), } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct UserId(String); impl UserId { pub fn new() -> Self { Self(format!("{}", Uuid::new_v4().hyphenated())) } pub fn as_str<'a>(&'a self) -> &'a str { &self.0 } } impl From<&str> for UserId { fn from(s: &str) -> Self { Self(s.to_owned()) } } impl From for UserId { fn from(s: String) -> Self { Self(s) } } impl FromSql for UserId { fn column_result(value: ValueRef<'_>) -> FromSqlResult { match value { ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), _ => unimplemented!(), } } } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct SessionId(String); impl SessionId { pub fn new() -> Self { Self(format!("{}", Uuid::new_v4().hyphenated())) } pub fn as_str<'a>(&'a self) -> &'a str { &self.0 } } impl From<&str> for SessionId { fn from(s: &str) -> Self { Self(s.to_owned()) } } impl From for SessionId { fn from(s: String) -> Self { Self(s) } } impl FromSql for SessionId { fn column_result(value: ValueRef<'_>) -> FromSqlResult { match value { ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), _ => unimplemented!(), } } } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct GameId(String); impl GameId { pub fn new() -> Self { Self(format!("{}", Uuid::new_v4().hyphenated())) } pub fn as_str<'a>(&'a self) -> &'a str { &self.0 } } impl From<&str> for GameId { fn from(s: &str) -> Self { Self(s.to_owned()) } } impl From for GameId { fn from(s: String) -> Self { Self(s) } } impl FromSql for GameId { fn column_result(value: ValueRef<'_>) -> FromSqlResult { match value { ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), _ => unimplemented!(), } } } #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct CharacterId(String); impl CharacterId { pub fn new() -> Self { Self(format!("{}", Uuid::new_v4().hyphenated())) } pub fn as_str<'a>(&'a self) -> &'a str { &self.0 } } impl From<&str> for CharacterId { fn from(s: &str) -> Self { Self(s.to_owned()) } } impl From for CharacterId { fn from(s: String) -> Self { Self(s) } } impl FromSql for CharacterId { fn column_result(value: ValueRef<'_>) -> FromSqlResult { match value { ValueRef::Text(text) => Ok(Self::from(String::from_utf8(text.to_vec()).unwrap())), _ => unimplemented!(), } } } #[derive(Clone, Debug)] pub struct UserRow { pub id: UserId, pub name: String, pub password: String, pub admin: bool, pub enabled: bool, } #[derive(Clone, Debug)] pub struct Role { userid: UserId, gameid: GameId, role: String, } #[derive(Clone, Debug)] pub struct GameRow { pub id: UserId, pub name: String, } #[derive(Clone, Debug)] pub struct CharsheetRow { id: String, game: GameId, pub data: serde_json::Value, } #[derive(Clone, Debug)] pub struct SessionRow { id: SessionId, user_id: SessionId, } #[async_trait] pub trait Database: Send + Sync { async fn user(&mut self, _: &UserId) -> Result, FatalError>; async fn user_by_username(&self, _: &str) -> Result, FatalError>; async fn save_user( &self, user_id: Option, name: &str, password: &str, admin: bool, enabled: bool, ) -> Result; async fn users(&mut self) -> Result, FatalError>; async fn games(&mut self) -> Result, FatalError>; async fn character(&mut self, id: CharacterId) -> Result, FatalError>; async fn session(&self, id: SessionId) -> Result, FatalError>; async fn create_session(&self, id: UserId) -> Result; } pub struct DiskDb { conn: Connection, } impl DiskDb { pub fn new

(path: Option

) -> Result where P: AsRef, { let mut conn = match path { None => Connection::open(":memory:").expect("to create a memory connection"), Some(path) => Connection::open(path).expect("to create connection"), }; 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, FatalError> { let mut stmt = self .conn .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE uuid=?") .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(); match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } fn user_by_username(&self, username: &str) -> Result, FatalError> { let mut stmt = self .conn .prepare("SELECT uuid, name, password, admin, enabled FROM users WHERE name=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt .query_map([username], |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())), _ => Err(FatalError::NonUniqueDatabaseKey(username.to_owned())), } } fn users(&self) -> Result, FatalError> { let mut stmt = self .conn .prepare("SELECT * FROM users") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items = stmt .query_map([], |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(); Ok(items) } fn save_user( &self, user_id: Option, name: &str, password: &str, admin: bool, enabled: bool, ) -> Result { match user_id { None => { let user_id = UserId::new(); let mut stmt = self .conn .prepare("INSERT INTO users VALUES (?, ?, ?, ?, ?)") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; stmt.execute((user_id.as_str(), name, password, admin, enabled)) .unwrap(); Ok(user_id) } Some(user_id) => { let mut stmt = self .conn .prepare("UPDATE users SET name=?, password=?, admin=?, enabled=? WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; stmt.execute((name, password, admin, enabled, user_id.as_str())) .unwrap(); Ok(user_id) } } } fn save_game(&self, game_id: Option, name: &str) -> Result { match game_id { None => { let game_id = GameId::new(); let mut stmt = self .conn .prepare("INSERT INTO games VALUES (?, ?)") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; stmt.execute((game_id.as_str(), name)).unwrap(); Ok(game_id) } Some(game_id) => { let mut stmt = self .conn .prepare("UPDATE games SET name=? WHERE uuid=?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; stmt.execute((name, game_id.as_str())).unwrap(); Ok(game_id) } } } fn session(&self, session_id: &SessionId) -> Result, FatalError> { let mut stmt = self.conn .prepare("SELECT u.uuid, u.name, u.password, u.admin, u.enabled FROM sessions s INNER JOIN users u ON u.uuid = s.user_id WHERE s.id = ?") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let items: Vec = stmt .query_map([session_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())), _ => Err(FatalError::NonUniqueDatabaseKey( session_id.as_str().to_owned(), )), } } fn create_session(&self, user_id: &UserId) -> Result { match self.user(user_id) { Ok(Some(_)) => { let mut stmt = self .conn .prepare("INSERT INTO sessions VALUES (?, ?)") .map_err(|err| FatalError::ConstructQueryFailure(format!("{}", err)))?; let session_id = SessionId::new(); stmt.execute((session_id.as_str(), user_id.as_str())) .unwrap(); Ok(session_id) } Ok(None) => Err(FatalError::DatabaseKeyMissing), Err(err) => Err(err), } } fn character(&self, id: CharacterId) -> Result, FatalError> { let mut stmt = self .conn .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(); Ok(CharsheetRow { id: row.get(0).unwrap(), game: row.get(1).unwrap(), data: serde_json::from_str(&data).unwrap(), }) }) .unwrap() .collect::, rusqlite::Error>>() .unwrap(); match &items[..] { [] => Ok(None), [item] => Ok(Some(item.clone())), _ => Err(FatalError::NonUniqueDatabaseKey(id.as_str().to_owned())), } } fn save_character( &self, char_id: Option, game: GameId, character: serde_json::Value, ) -> std::result::Result { match char_id { None => { let char_id = CharacterId::new(); let mut stmt = self .conn .prepare("INSERT INTO characters VALUES (?, ?, ?)") .unwrap(); stmt.execute((char_id.as_str(), game.as_str(), character.to_string())) .unwrap(); Ok(char_id) } Some(char_id) => { let mut stmt = self .conn .prepare("UPDATE characters SET data=? WHERE uuid=?") .unwrap(); stmt.execute((character.to_string(), char_id.as_str())) .unwrap(); Ok(char_id) } } } } async fn db_handler(db: DiskDb, requestor: Receiver) { while let Ok(DatabaseRequest { tx, req }) = requestor.recv().await { println!("Request received: {:?}", req); match req { Request::Charsheet(id) => { let sheet = db.character(id); match sheet { Ok(sheet) => { tx.send(DatabaseResponse::Charsheet(sheet)).await.unwrap(); } _ => unimplemented!(), } } Request::CreateSession(id) => { let session_id = db.create_session(&id).unwrap(); tx.send(DatabaseResponse::CreateSession(session_id)) .await .unwrap(); } Request::Games => { unimplemented!(); } Request::User(uid) => { let user = db.user(&uid); match user { Ok(user) => { tx.send(DatabaseResponse::User(user)).await.unwrap(); } err => panic!("{:?}", err), } } Request::UserByUsername(username) => { let user = db.user_by_username(&username); match user { Ok(user) => tx.send(DatabaseResponse::User(user)).await.unwrap(), err => panic!("{:?}", err), } } Request::SaveUser(user_id, username, password, admin, enabled) => { let user_id = db.save_user( user_id, username.as_ref(), password.as_ref(), admin, enabled, ); match user_id { Ok(user_id) => { tx.send(DatabaseResponse::SaveUser(user_id)).await.unwrap(); } err => panic!("{:?}", err), } } Request::Session(session_id) => { let user = db.session(&session_id); match user { Ok(user) => tx.send(DatabaseResponse::Session(user)).await.unwrap(), err => panic!("{:?}", err), } } Request::Users => { let users = db.users(); match users { Ok(users) => { tx.send(DatabaseResponse::Users(users)).await.unwrap(); } _ => unimplemented!(), } } } } println!("ending db_handler"); } pub struct DbConn { conn: Sender, handle: tokio::task::JoinHandle<()>, } impl DbConn { pub fn new

(path: Option

) -> Self where P: AsRef, { let (tx, rx) = bounded::(5); let db = DiskDb::new(path).unwrap(); let handle = tokio::spawn(async move { db_handler(db, rx).await; }); Self { conn: tx, handle } } } #[async_trait] impl Database for DbConn { async fn user(&mut self, uid: &UserId) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::User(uid.clone()), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::User(user)) => Ok(user), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn user_by_username(&self, username: &str) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::UserByUsername(username.to_owned()), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::User(user)) => Ok(user), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn save_user( &self, user_id: Option, name: &str, password: &str, admin: bool, enabled: bool, ) -> Result { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::SaveUser( user_id, name.to_owned(), password.to_owned(), admin, enabled, ), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::SaveUser(user_id)) => Ok(user_id), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn users(&mut self) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Users, }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::Users(lst)) => Ok(lst), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn games(&mut self) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Games, }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::Games(lst)) => Ok(lst), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn character(&mut self, id: CharacterId) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Charsheet(id), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::Charsheet(row)) => Ok(row), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn session(&self, id: SessionId) -> Result, FatalError> { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::Session(id), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::Session(row)) => Ok(row), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } async fn create_session(&self, id: UserId) -> Result { let (tx, rx) = bounded::(1); let request = DatabaseRequest { tx, req: Request::CreateSession(id), }; match self.conn.send(request).await { Ok(()) => (), Err(_) => return Err(FatalError::DatabaseConnectionLost), }; match rx.recv().await { Ok(DatabaseResponse::CreateSession(session_id)) => Ok(session_id), Ok(_) => Err(FatalError::MessageMismatch), Err(_) => Err(FatalError::DatabaseConnectionLost), } } } #[cfg(test)] mod test { use std::path::PathBuf; use cool_asserts::assert_matches; use super::*; 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." ] }"#; fn setup_db() -> (DiskDb, GameId) { let no_path: Option = None; let db = DiskDb::new(no_path).unwrap(); db.save_user(None, "admin", "abcdefg", true, true).unwrap(); let game_id = db.save_game(None, "Candela").unwrap(); (db, game_id) } #[test] fn it_can_retrieve_a_character() { let (db, game_id) = setup_db(); 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_character(None, game_id, js.clone()).unwrap(); assert_matches!(db.character(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); } #[tokio::test] async fn it_can_retrieve_a_character_through_conn() { let memory_db: Option = None; let mut conn = DbConn::new(memory_db); assert_matches!(conn.character(CharacterId::from("1")).await, Ok(None)); } }