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 17:20:38 +00:00
use async_std ::channel ::{ bounded , Receiver , Sender } ;
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 ;
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 " ) ]
2024-11-30 17:20:38 +00:00
NoResponse ,
2024-11-30 16:48:35 +00:00
}
#[ derive(Debug) ]
enum Request {
Charsheet ( CharacterId ) ,
}
#[ 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-11-30 04:14:52 +00:00
}
2024-12-01 04:03:52 +00:00
#[ 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 < String > for UserId {
fn from ( s : String ) -> Self {
Self ( s )
}
}
#[ 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 < String > for GameId {
fn from ( s : String ) -> Self {
Self ( s )
}
}
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 {
2024-12-01 04:03:52 +00:00
Self ( format! ( " {} " , Uuid ::new_v4 ( ) . hyphenated ( ) ) )
2024-11-30 04:14:52 +00:00
}
pub fn as_str < ' a > ( & ' a self ) -> & ' a str {
& self . 0
}
}
impl From < & str > for CharacterId {
fn from ( s : & str ) -> Self {
2024-12-01 04:03:52 +00:00
Self ( s . to_owned ( ) )
2024-11-30 04:14:52 +00:00
}
}
impl From < String > for CharacterId {
fn from ( s : String ) -> Self {
2024-12-01 04:03:52 +00:00
Self ( s )
2024-11-30 04:14:52 +00:00
}
}
2024-12-01 04:03:52 +00:00
#[ derive(Clone, Debug) ]
pub struct UserRow {
id : String ,
name : String ,
password : String ,
admin : bool ,
enabled : bool ,
}
#[ derive(Clone, Debug) ]
pub struct Role {
userid : String ,
gameid : String ,
role : String ,
}
2024-11-30 04:14:52 +00:00
#[ derive(Clone, Debug) ]
pub struct CharsheetRow {
id : String ,
2024-12-01 04:03:52 +00:00
game : String ,
2024-11-30 20:24:57 +00:00
pub data : serde_json ::Value ,
2024-11-30 04:14:52 +00:00
}
2024-11-30 16:48:35 +00:00
#[ async_trait ]
pub trait Database : Send + Sync {
2024-11-30 17:20:38 +00:00
async fn charsheet ( & mut self , id : CharacterId ) -> Result < Option < CharsheetRow > , Error > ;
2024-11-30 04:14:52 +00:00
}
pub struct DiskDb {
conn : Connection ,
}
2024-11-30 23:55:51 +00:00
fn setup_test_database ( conn : & Connection ) {
let mut gamecount_stmt = conn . prepare ( " SELECT count(*) FROM games " ) . unwrap ( ) ;
let mut count = gamecount_stmt . query ( [ ] ) . unwrap ( ) ;
if count . next ( ) . unwrap ( ) . unwrap ( ) . get ::< usize , usize > ( 0 ) = = Ok ( 0 ) {
2024-12-01 04:03:52 +00:00
let admin_id = format! ( " {} " , Uuid ::new_v4 ( ) ) ;
let user_id = format! ( " {} " , Uuid ::new_v4 ( ) ) ;
2024-11-30 23:55:51 +00:00
let game_id = format! ( " {} " , Uuid ::new_v4 ( ) ) ;
let char_id = CharacterId ::new ( ) ;
2024-12-01 04:03:52 +00:00
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 ( ) ;
2024-11-30 23:55:51 +00:00
let mut game_stmt = conn . prepare ( " INSERT INTO games VALUES (?, ?) " ) . unwrap ( ) ;
2024-12-01 04:03:52 +00:00
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 ( ) ;
2024-11-30 23:55:51 +00:00
let mut sheet_stmt = conn
. prepare ( " INSERT INTO characters VALUES (?, ?, ?) " )
. unwrap ( ) ;
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 ( ) ;
}
}
2024-11-30 04:14:52 +00:00
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 " ) ;
2024-11-30 17:20:38 +00:00
2024-11-30 23:55:51 +00:00
setup_test_database ( & conn ) ;
2024-11-30 17:20:38 +00:00
2024-11-30 04:14:52 +00:00
Ok ( DiskDb { conn } )
}
2024-12-01 04:03:52 +00:00
fn user ( & self , id : UserId ) -> Result < Option < UserRow > , Error > {
let mut stmt = self
. conn
. prepare ( " SELECT uuid, name, password, admin, enabled WHERE uuid=? " )
. unwrap ( ) ;
let items : Vec < UserRow > = 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 ::< Result < Vec < UserRow > , rusqlite ::Error > > ( ) . unwrap ( ) ;
match & items [ .. ] {
[ ] = > Ok ( None ) ,
[ item ] = > Ok ( Some ( item . clone ( ) ) ) ,
_ = > unimplemented! ( ) ,
}
}
2024-11-30 04:14:52 +00:00
fn charsheet ( & self , id : CharacterId ) -> Result < Option < CharsheetRow > , Error > {
let mut stmt = self
. conn
2024-12-01 04:03:52 +00:00
. prepare ( " SELECT uuid, game, data FROM charsheet WHERE uuid=? " )
2024-11-30 04:14:52 +00:00
. 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 ( ) ,
2024-12-01 04:03:52 +00:00
game : row . get ( 1 ) . unwrap ( ) ,
2024-11-30 04:14:52 +00:00
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 20:24:57 +00:00
async fn db_handler ( db : DiskDb , requestor : Receiver < DatabaseRequest > ) {
2024-11-30 16:48:35 +00:00
println! ( " Starting db_handler " ) ;
2024-11-30 17:20:38 +00:00
while let Ok ( DatabaseRequest { tx , req } ) = requestor . recv ( ) . await {
2024-11-30 16:48:35 +00:00
println! ( " Request received: {:?} " , req ) ;
match req {
Request ::Charsheet ( id ) = > {
let sheet = db . charsheet ( id ) ;
println! ( " sheet retrieved: {:?} " , sheet ) ;
match sheet {
2024-11-30 17:05:31 +00:00
Ok ( sheet ) = > {
tx . send ( DatabaseResponse ::Charsheet ( sheet ) ) . await . unwrap ( ) ;
2024-11-30 17:20:38 +00:00
}
2024-11-30 16:48:35 +00:00
_ = > unimplemented! ( ) ,
}
}
}
}
println! ( " ending db_handler " ) ;
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-11-30 16:48:35 +00:00
#[ async_trait ]
impl Database for DbConn {
2024-11-30 17:20:38 +00:00
async fn charsheet ( & mut self , id : CharacterId ) -> Result < Option < CharsheetRow > , Error > {
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 ) ,
} ;
self . conn . send ( request ) . await . unwrap ( ) ;
2024-11-30 17:05:31 +00:00
match rx . recv ( ) . await {
2024-11-30 16:48:35 +00:00
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
}