2024-12-27 19:29:07 +00:00
mod disk_db ;
mod types ;
2024-12-01 05:51:08 +00:00
use std ::path ::Path ;
2024-11-30 04:14:52 +00:00
2024-12-27 19:29:07 +00:00
use async_std ::channel ::{ bounded , Sender } ;
2024-11-30 16:48:35 +00:00
use async_trait ::async_trait ;
2024-12-27 19:29:07 +00:00
use disk_db ::{ db_handler , DiskDb } ;
pub use types ::{ CharacterId , CharsheetRow , GameRow , SessionId , UserId , UserRow } ;
2024-11-30 04:14:52 +00:00
2024-12-01 05:51:08 +00:00
use crate ::types ::FatalError ;
2024-11-30 16:48:35 +00:00
#[ derive(Debug) ]
enum Request {
Charsheet ( CharacterId ) ,
2024-12-27 19:02:43 +00:00
CreateSession ( UserId ) ,
2024-12-11 03:43:15 +00:00
Games ,
2024-12-27 19:02:43 +00:00
SaveUser ( Option < UserId > , String , String , bool , bool ) ,
Session ( SessionId ) ,
2024-12-16 05:27:55 +00:00
User ( UserId ) ,
2024-12-22 14:17:00 +00:00
UserByUsername ( String ) ,
2024-12-01 19:07:37 +00:00
Users ,
2024-11-30 16:48:35 +00:00
}
#[ derive(Debug) ]
struct DatabaseRequest {
2024-11-30 17:05:31 +00:00
tx : Sender < DatabaseResponse > ,
2024-11-30 16:48:35 +00:00
req : Request ,
}
#[ derive(Debug) ]
enum DatabaseResponse {
Charsheet ( Option < CharsheetRow > ) ,
2024-12-27 19:02:43 +00:00
CreateSession ( SessionId ) ,
2024-12-11 03:43:15 +00:00
Games ( Vec < GameRow > ) ,
2024-12-27 19:02:43 +00:00
SaveUser ( UserId ) ,
Session ( Option < UserRow > ) ,
2024-12-16 05:27:55 +00:00
User ( Option < UserRow > ) ,
2024-12-11 03:43:15 +00:00
Users ( Vec < UserRow > ) ,
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
#[ async_trait ]
pub trait Database : Send + Sync {
2024-12-27 19:29:07 +00:00
async fn user ( & self , _ : & UserId ) -> Result < Option < UserRow > , FatalError > ;
2024-12-18 04:43:36 +00:00
2024-12-27 19:02:43 +00:00
async fn user_by_username ( & self , _ : & str ) -> Result < Option < UserRow > , FatalError > ;
2024-12-22 14:17:00 +00:00
2024-12-18 04:43:36 +00:00
async fn save_user (
2024-12-27 19:02:43 +00:00
& self ,
2024-12-18 04:43:36 +00:00
user_id : Option < UserId > ,
name : & str ,
password : & str ,
admin : bool ,
enabled : bool ,
) -> Result < UserId , FatalError > ;
2024-12-16 05:27:55 +00:00
2024-12-18 04:43:36 +00:00
async fn users ( & mut self ) -> Result < Vec < UserRow > , FatalError > ;
2024-12-11 03:43:15 +00:00
2024-12-18 04:43:36 +00:00
async fn games ( & mut self ) -> Result < Vec < GameRow > , FatalError > ;
2024-12-01 19:07:37 +00:00
2024-12-18 04:43:36 +00:00
async fn character ( & mut self , id : CharacterId ) -> Result < Option < CharsheetRow > , FatalError > ;
2024-12-27 19:02:43 +00:00
async fn session ( & self , id : SessionId ) -> Result < Option < UserRow > , FatalError > ;
async fn create_session ( & self , id : UserId ) -> Result < SessionId , FatalError > ;
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
pub struct DbConn {
2024-11-30 17:05:31 +00:00
conn : Sender < DatabaseRequest > ,
2024-11-30 16:48:35 +00:00
handle : tokio ::task ::JoinHandle < ( ) > ,
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
impl DbConn {
pub fn new < P > ( path : Option < P > ) -> Self
where
P : AsRef < Path > ,
{
2024-11-30 17:05:31 +00:00
let ( tx , rx ) = bounded ::< DatabaseRequest > ( 5 ) ;
2024-11-30 16:48:35 +00:00
let db = DiskDb ::new ( path ) . unwrap ( ) ;
2024-11-30 04:14:52 +00:00
2024-11-30 17:20:38 +00:00
let handle = tokio ::spawn ( async move {
db_handler ( db , rx ) . await ;
} ) ;
2024-11-30 16:48:35 +00:00
Self { conn : tx , handle }
2024-11-30 04:14:52 +00:00
}
2024-12-27 19:29:07 +00:00
async fn send ( & self , req : Request ) -> Result < DatabaseResponse , FatalError > {
2024-12-16 05:27:55 +00:00
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 1 ) ;
2024-12-27 19:29:07 +00:00
let request = DatabaseRequest { tx , req } ;
2024-12-16 05:27:55 +00:00
match self . conn . send ( request ) . await {
Ok ( ( ) ) = > ( ) ,
2024-12-18 04:43:36 +00:00
Err ( _ ) = > return Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-16 05:27:55 +00:00
} ;
2024-12-27 19:29:07 +00:00
rx . recv ( )
. await
. map_err ( | _ | FatalError ::DatabaseConnectionLost )
}
}
#[ async_trait ]
impl Database for DbConn {
async fn user ( & self , uid : & UserId ) -> Result < Option < UserRow > , FatalError > {
match self
. send ( Request ::User ( uid . clone ( ) ) )
. await
{
2024-12-18 04:43:36 +00:00
Ok ( DatabaseResponse ::User ( user ) ) = > Ok ( user ) ,
Ok ( _ ) = > Err ( FatalError ::MessageMismatch ) ,
Err ( _ ) = > Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-16 05:27:55 +00:00
}
}
2024-12-27 19:02:43 +00:00
async fn user_by_username ( & self , username : & str ) -> Result < Option < UserRow > , FatalError > {
2024-12-22 14:17:00 +00:00
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 1 ) ;
let request = DatabaseRequest {
tx ,
2024-12-27 19:02:43 +00:00
req : Request ::UserByUsername ( username . to_owned ( ) ) ,
2024-12-22 14:17:00 +00:00
} ;
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 ) ,
}
}
2024-12-18 04:43:36 +00:00
async fn save_user (
2024-12-27 19:02:43 +00:00
& self ,
2024-12-18 04:43:36 +00:00
user_id : Option < UserId > ,
name : & str ,
password : & str ,
admin : bool ,
enabled : bool ,
) -> Result < UserId , FatalError > {
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 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 < Vec < UserRow > , FatalError > {
2024-12-01 19:07:37 +00:00
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 1 ) ;
let request = DatabaseRequest {
tx ,
req : Request ::Users ,
} ;
match self . conn . send ( request ) . await {
Ok ( ( ) ) = > ( ) ,
2024-12-18 04:43:36 +00:00
Err ( _ ) = > return Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-01 19:07:37 +00:00
} ;
match rx . recv ( ) . await {
2024-12-18 04:43:36 +00:00
Ok ( DatabaseResponse ::Users ( lst ) ) = > Ok ( lst ) ,
Ok ( _ ) = > Err ( FatalError ::MessageMismatch ) ,
Err ( _ ) = > Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-01 19:07:37 +00:00
}
}
2024-12-18 04:43:36 +00:00
async fn games ( & mut self ) -> Result < Vec < GameRow > , FatalError > {
2024-12-11 03:43:15 +00:00
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 1 ) ;
let request = DatabaseRequest {
tx ,
req : Request ::Games ,
} ;
match self . conn . send ( request ) . await {
Ok ( ( ) ) = > ( ) ,
2024-12-18 04:43:36 +00:00
Err ( _ ) = > return Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-11 03:43:15 +00:00
} ;
match rx . recv ( ) . await {
2024-12-18 04:43:36 +00:00
Ok ( DatabaseResponse ::Games ( lst ) ) = > Ok ( lst ) ,
Ok ( _ ) = > Err ( FatalError ::MessageMismatch ) ,
Err ( _ ) = > Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-11 03:43:15 +00:00
}
}
2024-12-18 04:43:36 +00:00
async fn character ( & mut self , id : CharacterId ) -> Result < Option < CharsheetRow > , FatalError > {
2024-11-30 17:05:31 +00:00
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 1 ) ;
2024-11-30 16:48:35 +00:00
2024-11-30 17:20:38 +00:00
let request = DatabaseRequest {
2024-11-30 16:48:35 +00:00
tx ,
req : Request ::Charsheet ( id ) ,
} ;
2024-12-01 05:51:08 +00:00
match self . conn . send ( request ) . await {
Ok ( ( ) ) = > ( ) ,
2024-12-18 04:43:36 +00:00
Err ( _ ) = > return Err ( FatalError ::DatabaseConnectionLost ) ,
2024-12-01 05:51:08 +00:00
} ;
2024-11-30 17:05:31 +00:00
match rx . recv ( ) . await {
2024-12-18 04:43:36 +00:00
Ok ( DatabaseResponse ::Charsheet ( row ) ) = > Ok ( row ) ,
Ok ( _ ) = > Err ( FatalError ::MessageMismatch ) ,
Err ( _ ) = > Err ( FatalError ::DatabaseConnectionLost ) ,
2024-11-30 04:14:52 +00:00
}
}
2024-12-27 19:02:43 +00:00
async fn session ( & self , id : SessionId ) -> Result < Option < UserRow > , FatalError > {
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 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 < SessionId , FatalError > {
let ( tx , rx ) = bounded ::< DatabaseResponse > ( 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 ) ,
}
}
2024-11-30 04:14:52 +00:00
}
#[ cfg(test) ]
mod test {
2024-12-01 05:51:08 +00:00
use std ::path ::PathBuf ;
2024-11-30 04:14:52 +00:00
use cool_asserts ::assert_matches ;
2024-12-27 19:29:07 +00:00
use disk_db ::DiskDb ;
use types ::GameId ;
2024-11-30 04:14:52 +00:00
use super ::* ;
2024-12-27 19:29:07 +00:00
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." ] }"# ;
2024-11-30 04:14:52 +00:00
2024-12-01 16:14:28 +00:00
fn setup_db ( ) -> ( DiskDb , GameId ) {
2024-11-30 16:48:35 +00:00
let no_path : Option < PathBuf > = None ;
let db = DiskDb ::new ( no_path ) . unwrap ( ) ;
2024-11-30 04:14:52 +00:00
2024-12-27 19:02:43 +00:00
db . save_user ( None , " admin " , " abcdefg " , true , true ) . unwrap ( ) ;
2024-12-01 16:14:28 +00:00
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 ( ) ;
2024-12-01 05:51:08 +00:00
assert_matches! ( db . character ( CharacterId ::from ( " 1 " ) ) , Ok ( None ) ) ;
2024-11-30 04:14:52 +00:00
2024-12-27 19:29:07 +00:00
let js : serde_json ::Value = serde_json ::from_str ( SOREN ) . unwrap ( ) ;
2024-12-16 05:27:55 +00:00
let soren_id = db . save_character ( None , game_id , js . clone ( ) ) . unwrap ( ) ;
2024-12-01 05:51:08 +00:00
assert_matches! ( db . character ( soren_id ) . unwrap ( ) , Some ( CharsheetRow { data , .. } ) = > assert_eq! ( js , data ) ) ;
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
#[ tokio::test ]
2024-12-01 05:51:08 +00:00
async fn it_can_retrieve_a_character_through_conn ( ) {
2024-11-30 16:48:35 +00:00
let memory_db : Option < PathBuf > = None ;
let mut conn = DbConn ::new ( memory_db ) ;
2024-12-27 19:02:43 +00:00
assert_matches! ( conn . character ( CharacterId ::from ( " 1 " ) ) . await , Ok ( None ) ) ;
2024-11-30 16:48:35 +00:00
}
2024-11-30 04:14:52 +00:00
}