2024-11-30 04:14:52 +00:00
use std ::{
2024-11-30 16:48:35 +00:00
path ::{ Path , PathBuf } ,
2024-11-30 04:14:52 +00:00
thread ::JoinHandle ,
} ;
2024-11-30 16:48:35 +00:00
use async_trait ::async_trait ;
2024-11-30 04:14:52 +00:00
use include_dir ::{ include_dir , Dir } ;
use lazy_static ::lazy_static ;
use rusqlite ::Connection ;
use rusqlite_migration ::Migrations ;
use serde ::{ Deserialize , Serialize } ;
use thiserror ::Error ;
2024-11-30 16:48:35 +00:00
use tokio ::sync ::{ mpsc , oneshot } ;
2024-11-30 04:14:52 +00:00
use uuid ::Uuid ;
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, Error) ]
pub enum Error {
#[ error( " Duplicate item found for id {0} " ) ]
DuplicateItem ( String ) ,
#[ error( " Unexpected response for message " ) ]
MessageMismatch ,
2024-11-30 16:48:35 +00:00
#[ error( " No response to request " ) ]
NoResponse
}
#[ derive(Debug) ]
enum Request {
Charsheet ( CharacterId ) ,
}
#[ derive(Debug) ]
struct DatabaseRequest {
tx : oneshot ::Sender < DatabaseResponse > ,
req : Request ,
}
#[ derive(Debug) ]
enum DatabaseResponse {
Charsheet ( Option < CharsheetRow > ) ,
2024-11-30 04:14:52 +00:00
}
#[ derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize) ]
pub struct CharacterId ( String ) ;
impl CharacterId {
pub fn new ( ) -> Self {
CharacterId ( 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 {
CharacterId ( s . to_owned ( ) )
}
}
impl From < String > for CharacterId {
fn from ( s : String ) -> Self {
CharacterId ( s )
}
}
#[ derive(Clone, Debug) ]
pub struct CharsheetRow {
id : String ,
gametype : String ,
data : serde_json ::Value ,
}
2024-11-30 16:48:35 +00:00
#[ async_trait ]
pub trait Database : Send + Sync {
async fn charsheet (
& mut self ,
id : CharacterId ,
) -> Result < Option < CharsheetRow > , Error > ;
2024-11-30 04:14:52 +00:00
}
pub struct DiskDb {
conn : Connection ,
}
impl DiskDb {
2024-11-30 16:48:35 +00:00
pub fn new < P > ( path : Option < P > ) -> Result < Self , Error >
where
P : AsRef < Path > ,
{
2024-11-30 04:14:52 +00:00
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 ) . expect ( " to run migrations " ) ;
Ok ( DiskDb { conn } )
}
fn charsheet ( & self , id : CharacterId ) -> Result < Option < CharsheetRow > , Error > {
let mut stmt = self
. conn
. prepare ( " SELECT uuid, gametype, data FROM charsheet WHERE uuid=? " )
. unwrap ( ) ;
let items : Vec < CharsheetRow > = stmt
. query_map ( [ id . as_str ( ) ] , | row | {
let data : String = row . get ( 2 ) . unwrap ( ) ;
Ok ( CharsheetRow {
id : row . get ( 0 ) . unwrap ( ) ,
gametype : row . get ( 1 ) . unwrap ( ) ,
data : serde_json ::from_str ( & data ) . unwrap ( ) ,
} )
} )
. unwrap ( )
. collect ::< Result < Vec < CharsheetRow > , rusqlite ::Error > > ( )
. unwrap ( ) ;
match & items [ .. ] {
[ ] = > Ok ( None ) ,
[ item ] = > Ok ( Some ( item . clone ( ) ) ) ,
_ = > unimplemented! ( ) ,
}
}
fn save_charsheet (
& self ,
char_id : Option < CharacterId > ,
game_type : String ,
charsheet : serde_json ::Value ,
) -> Result < CharacterId , Error > {
match char_id {
None = > {
let char_id = CharacterId ::new ( ) ;
let mut stmt = self
. conn
. prepare ( " INSERT INTO charsheet VALUES (?, ?, ?) " )
. unwrap ( ) ;
stmt . execute ( ( char_id . as_str ( ) , game_type , charsheet . to_string ( ) ) )
. unwrap ( ) ;
Ok ( char_id )
}
Some ( char_id ) = > {
let mut stmt = self
. conn
. prepare ( " UPDATE charsheet SET data=? WHERE uuid=? " )
. unwrap ( ) ;
stmt . execute ( ( charsheet . to_string ( ) , char_id . as_str ( ) ) )
. unwrap ( ) ;
Ok ( char_id )
}
}
}
}
2024-11-30 16:48:35 +00:00
async fn db_handler ( db : DiskDb , mut requestor : mpsc ::Receiver < DatabaseRequest > ) {
println! ( " Starting db_handler " ) ;
while let Some ( DatabaseRequest { tx , req } ) = requestor . recv ( ) . await {
println! ( " Request received: {:?} " , req ) ;
match req {
Request ::Charsheet ( id ) = > {
let sheet = db . charsheet ( id ) ;
println! ( " sheet retrieved: {:?} " , sheet ) ;
match sheet {
Ok ( sheet ) = > tx . send ( DatabaseResponse ::Charsheet ( sheet ) ) . unwrap ( ) ,
_ = > unimplemented! ( ) ,
}
}
}
}
println! ( " ending db_handler " ) ;
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
pub struct DbConn {
conn : mpsc ::Sender < DatabaseRequest > ,
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 > ,
{
let ( tx , rx ) = mpsc ::channel ( 5 ) ;
let db = DiskDb ::new ( path ) . unwrap ( ) ;
2024-11-30 04:14:52 +00:00
2024-11-30 16:48:35 +00:00
let handle = tokio ::spawn ( async move { db_handler ( db , rx ) . await ; } ) ;
Self { conn : tx , handle }
2024-11-30 04:14:52 +00:00
}
}
2024-11-30 16:48:35 +00:00
#[ async_trait ]
impl Database for DbConn {
async fn charsheet (
& mut self ,
id : CharacterId ,
) -> Result < Option < CharsheetRow > , Error > {
let ( tx , rx ) = oneshot ::channel ::< DatabaseResponse > ( ) ;
let request = DatabaseRequest {
tx ,
req : Request ::Charsheet ( id ) ,
} ;
self . conn . send ( request ) . await . unwrap ( ) ;
match rx . await {
Ok ( DatabaseResponse ::Charsheet ( row ) ) = > Ok ( row ) ,
2024-11-30 04:14:52 +00:00
Ok ( _ ) = > Err ( Error ::MessageMismatch ) ,
2024-11-30 16:48:35 +00:00
Err ( err ) = > {
println! ( " error: {:?} " , err ) ;
Err ( Error ::NoResponse )
}
2024-11-30 04:14:52 +00:00
}
}
}
#[ cfg(test) ]
mod test {
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." ] }"# ;
#[ test ]
fn it_can_retrieve_a_charsheet ( ) {
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
assert_matches! ( db . charsheet ( CharacterId ::from ( " 1 " ) ) , Ok ( None ) ) ;
let js : serde_json ::Value = serde_json ::from_str ( soren ) . unwrap ( ) ;
2024-11-30 16:48:35 +00:00
let soren_id = db
. save_charsheet ( None , " candela " . to_owned ( ) , js . clone ( ) )
. unwrap ( ) ;
2024-11-30 04:14:52 +00:00
assert_matches! ( db . charsheet ( soren_id ) . unwrap ( ) , Some ( CharsheetRow { data , .. } ) = > assert_eq! ( js , data ) ) ;
}
2024-11-30 16:48:35 +00:00
#[ tokio::test ]
async fn it_can_retrieve_a_charsheet_through_conn ( ) {
let memory_db : Option < PathBuf > = None ;
let mut conn = DbConn ::new ( memory_db ) ;
assert_matches! ( conn . charsheet ( CharacterId ::from ( " 1 " ) ) . await , Ok ( None ) ) ;
}
2024-11-30 04:14:52 +00:00
}