Set up a database and serve character sheets from it #277

Merged
savanni merged 7 commits from visions-database into main 2024-11-30 23:56:21 +00:00
5 changed files with 114 additions and 47 deletions
Showing only changes of commit d78a471437 - Show all commits

View File

@ -15,7 +15,6 @@ warp = { version = "0.3" }
mime_guess = "2.0.5" mime_guess = "2.0.5"
mime = "0.3.17" mime = "0.3.17"
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.11.0", features = ["v4"] }
futures = "0.3.31"
tokio-stream = "0.1.16" tokio-stream = "0.1.16"
typeshare = "1.0.4" typeshare = "1.0.4"
urlencoding = "2.1.3" urlencoding = "2.1.3"
@ -24,6 +23,8 @@ rusqlite = "0.32.1"
rusqlite_migration = { version = "1.3.1", features = ["from-directory"] } rusqlite_migration = { version = "1.3.1", features = ["from-directory"] }
lazy_static = "1.5.0" lazy_static = "1.5.0"
include_dir = "0.7.4" include_dir = "0.7.4"
async-trait = "0.1.83"
futures = "0.3.31"
[dev-dependencies] [dev-dependencies]
cool_asserts = "2.0.3" cool_asserts = "2.0.3"

View File

@ -7,7 +7,7 @@ tasks:
test: test:
cmds: cmds:
- cargo watch -x test - cargo watch -x 'test -- --nocapture'
dev: dev:
cmds: cmds:

View File

@ -1,13 +1,10 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::Read,
path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use mime::Mime; use mime::Mime;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use urlencoding::decode;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@ -27,6 +24,7 @@ struct WebsocketClient {
pub struct AppState { pub struct AppState {
pub asset_store: Box<dyn Assets + Sync + Send + 'static>, pub asset_store: Box<dyn Assets + Sync + Send + 'static>,
pub db: Box<dyn Database + Sync + Send + 'static>,
pub clients: HashMap<String, WebsocketClient>, pub clients: HashMap<String, WebsocketClient>,
pub tabletop: Tabletop, pub tabletop: Tabletop,
@ -36,12 +34,14 @@ pub struct AppState {
pub struct Core(Arc<RwLock<AppState>>); pub struct Core(Arc<RwLock<AppState>>);
impl Core { impl Core {
pub fn new<A>(assetdb: A) -> Self pub fn new<A, DB>(assetdb: A, db: DB) -> Self
where where
A: Assets + Sync + Send + 'static, A: Assets + Sync + Send + 'static,
DB: Database + Sync + Send + 'static,
{ {
Self(Arc::new(RwLock::new(AppState { Self(Arc::new(RwLock::new(AppState {
asset_store: Box::new(assetdb), asset_store: Box::new(assetdb),
db: Box::new(db),
clients: HashMap::new(), clients: HashMap::new(),
tabletop: Tabletop { tabletop: Tabletop {
background_color: DEFAULT_BACKGROUND_COLOR, background_color: DEFAULT_BACKGROUND_COLOR,
@ -135,11 +135,13 @@ impl Core {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::path::PathBuf;
use super::*; use super::*;
use cool_asserts::assert_matches; use cool_asserts::assert_matches;
use crate::asset_db::mocks::MemoryAssets; use crate::{asset_db::mocks::MemoryAssets, database::{DbConn, DiskDb}};
fn test_core() -> Core { fn test_core() -> Core {
let assets = MemoryAssets::new(vec![ let assets = MemoryAssets::new(vec![
@ -169,7 +171,9 @@ mod test {
String::from("abcdefg").into_bytes(), String::from("abcdefg").into_bytes(),
), ),
]); ]);
Core::new(assets) let memory_db: Option<PathBuf> = None;
let conn = DbConn::new(memory_db);
Core::new(assets, conn)
} }
#[tokio::test] #[tokio::test]

View File

@ -1,15 +1,16 @@
use std::{ use std::{
path::PathBuf, path::{Path, PathBuf},
sync::mpsc::{channel, Receiver, Sender},
thread::JoinHandle, thread::JoinHandle,
}; };
use async_trait::async_trait;
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rusqlite::Connection; use rusqlite::Connection;
use rusqlite_migration::Migrations; use rusqlite_migration::Migrations;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use uuid::Uuid; use uuid::Uuid;
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
@ -26,6 +27,25 @@ pub enum Error {
#[error("Unexpected response for message")] #[error("Unexpected response for message")]
MessageMismatch, MessageMismatch,
#[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>),
} }
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
@ -60,8 +80,12 @@ pub struct CharsheetRow {
data: serde_json::Value, data: serde_json::Value,
} }
pub trait Database { #[async_trait]
async fn charsheet(&self, id: CharacterId) -> Result<Option<CharsheetRow>, Error>; pub trait Database: Send + Sync {
async fn charsheet(
&mut self,
id: CharacterId,
) -> Result<Option<CharsheetRow>, Error>;
} }
pub struct DiskDb { pub struct DiskDb {
@ -69,7 +93,10 @@ pub struct DiskDb {
} }
impl DiskDb { impl DiskDb {
pub fn new(path: Option<PathBuf>) -> Result<Self, Error> { pub fn new<P>(path: Option<P>) -> Result<Self, Error>
where
P: AsRef<Path>,
{
let mut conn = match path { let mut conn = match path {
None => Connection::open(":memory:").expect("to create a memory connection"), None => Connection::open(":memory:").expect("to create a memory connection"),
Some(path) => Connection::open(path).expect("to create connection"), Some(path) => Connection::open(path).expect("to create connection"),
@ -134,42 +161,66 @@ impl DiskDb {
} }
} }
/* async fn db_handler(db: DiskDb, mut requestor: mpsc::Receiver<DatabaseRequest>) {
enum DatabaseRequest { println!("Starting db_handler");
Charsheet(CharacterId), while let Some(DatabaseRequest{ tx, req }) = requestor.recv().await {
} println!("Request received: {:?}", req);
match req {
enum DatabaseResponse { Request::Charsheet(id) => {
Charsheet(Option<CharsheetRow>), let sheet = db.charsheet(id);
} println!("sheet retrieved: {:?}", sheet);
match sheet {
pub struct DiskDb { Ok(sheet) => tx.send(DatabaseResponse::Charsheet(sheet)).unwrap(),
db_handle: JoinHandle<()>, _ => unimplemented!(),
to_db: Sender<DatabaseRequest>,
from_db: Receiver<DatabaseResponse>,
}
impl DiskDb {
pub fn new(path: PathBuf) -> DiskDb {
DiskDb {
db_handle,
to_db: interface_to_db_tx,
from_db: db_to_interface_rx,
} }
} }
}
}
println!("ending db_handler");
}
pub struct DbConn {
conn: mpsc::Sender<DatabaseRequest>,
handle: tokio::task::JoinHandle<()>,
}
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();
let handle = tokio::spawn(async move { db_handler(db, rx).await; });
Self { conn: tx, handle }
}
} }
impl Database for DiskDb { #[async_trait]
async fn charsheet(&self, id: CharacterId) -> Result<Option<CharsheetRow>, Error> { impl Database for DbConn {
self.to_db(DatabaseRequest::Charsheet(id)).unwrap(); async fn charsheet(
match self.from_db.recv() { &mut self,
Ok(DatabaseResponse::Charsheet(sheet)) => Ok(sheet), 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),
Ok(_) => Err(Error::MessageMismatch), Ok(_) => Err(Error::MessageMismatch),
Err(_) => unimplemented!(), Err(err) => {
println!("error: {:?}", err);
Err(Error::NoResponse)
}
} }
} }
} }
*/
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@ -181,12 +232,23 @@ mod test {
#[test] #[test]
fn it_can_retrieve_a_charsheet() { fn it_can_retrieve_a_charsheet() {
let db = DiskDb::new(None).unwrap(); let no_path: Option<PathBuf> = None;
let db = DiskDb::new(no_path).unwrap();
assert_matches!(db.charsheet(CharacterId::from("1")), Ok(None)); assert_matches!(db.charsheet(CharacterId::from("1")), Ok(None));
let js: serde_json::Value = serde_json::from_str(soren).unwrap(); let js: serde_json::Value = serde_json::from_str(soren).unwrap();
let soren_id = db.save_charsheet(None, "candela".to_owned(), js.clone()).unwrap(); let soren_id = db
.save_charsheet(None, "candela".to_owned(), js.clone())
.unwrap();
assert_matches!(db.charsheet(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data)); assert_matches!(db.charsheet(soren_id).unwrap(), Some(CharsheetRow{ data, .. }) => assert_eq!(js, data));
} }
#[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));
}
} }

View File

@ -6,7 +6,7 @@ use std::{
use asset_db::{AssetId, FsAssets}; use asset_db::{AssetId, FsAssets};
use authdb::AuthError; use authdb::AuthError;
use database::{CharacterId, Database}; use database::DbConn;
use handlers::{ use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_register_client, handle_available_images, handle_connect_websocket, handle_file, handle_register_client,
handle_set_background_image, handle_unregister_client, RegisterRequest, handle_set_background_image, handle_unregister_client, RegisterRequest,
@ -100,9 +100,9 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
#[tokio::main] #[tokio::main]
pub async fn main() { pub async fn main() {
let core = core::Core::new( let conn = DbConn::new(Some("/home/savanni/game.db"));
FsAssets::new(PathBuf::from("/home/savanni/Pictures"))
); let core = core::Core::new(FsAssets::new(PathBuf::from("/home/savanni/Pictures")), conn);
let log = warp::log("visions::api"); let log = warp::log("visions::api");
let route_image = warp::path!("api" / "v1" / "image" / String) let route_image = warp::path!("api" / "v1" / "image" / String)