Set up a database and serve character sheets from it #277
|
@ -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"
|
||||||
|
|
|
@ -7,7 +7,7 @@ tasks:
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cmds:
|
cmds:
|
||||||
- cargo watch -x test
|
- cargo watch -x 'test -- --nocapture'
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
cmds:
|
cmds:
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue