Set up automated tests for the application Core #266

Merged
savanni merged 6 commits from visions-core-test into main 2024-11-24 14:53:18 +00:00
7 changed files with 440 additions and 120 deletions

98
Cargo.lock generated
View File

@ -258,7 +258,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -287,7 +287,7 @@ dependencies = [
"sha2", "sha2",
"sqlformat", "sqlformat",
"sqlx", "sqlx",
"thiserror", "thiserror 1.0.64",
"tokio", "tokio",
"uuid 0.4.0", "uuid 0.4.0",
] ]
@ -368,7 +368,7 @@ dependencies = [
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -470,7 +470,7 @@ dependencies = [
"glib", "glib",
"libc", "libc",
"once_cell", "once_cell",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -605,7 +605,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -652,7 +652,7 @@ dependencies = [
"cool_asserts", "cool_asserts",
"serde 1.0.210", "serde 1.0.210",
"serde_json", "serde_json",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -712,7 +712,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"nom", "nom",
"proptest", "proptest",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -955,7 +955,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -984,7 +984,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"tempfile", "tempfile",
"thiserror", "thiserror 1.0.64",
"uuid 0.8.2", "uuid 0.8.2",
] ]
@ -1130,7 +1130,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"tempdir", "tempdir",
"thiserror", "thiserror 1.0.64",
"tokio", "tokio",
"uuid 0.4.0", "uuid 0.4.0",
"warp", "warp",
@ -1153,7 +1153,7 @@ dependencies = [
"glib-build-tools 0.18.0", "glib-build-tools 0.18.0",
"gtk4", "gtk4",
"libadwaita", "libadwaita",
"thiserror", "thiserror 1.0.64",
"tokio", "tokio",
] ]
@ -1230,7 +1230,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
dependencies = [ dependencies = [
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -1373,7 +1373,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -1540,7 +1540,7 @@ dependencies = [
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -1576,7 +1576,7 @@ dependencies = [
"memchr", "memchr",
"once_cell", "once_cell",
"smallvec", "smallvec",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -1608,7 +1608,7 @@ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -2120,7 +2120,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde 1.0.210", "serde 1.0.210",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -2801,7 +2801,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -2836,7 +2836,7 @@ dependencies = [
"serde 1.0.210", "serde 1.0.210",
"serde_json", "serde_json",
"sgf", "sgf",
"thiserror", "thiserror 1.0.64",
"uuid 0.8.2", "uuid 0.8.2",
] ]
@ -3044,7 +3044,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -3084,7 +3084,7 @@ dependencies = [
"nix", "nix",
"once_cell", "once_cell",
"pipewire-sys", "pipewire-sys",
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -3584,7 +3584,7 @@ dependencies = [
name = "result-extended" name = "result-extended"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"thiserror", "thiserror 1.0.64",
] ]
[[package]] [[package]]
@ -3776,7 +3776,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -3836,7 +3836,7 @@ dependencies = [
"nary_tree", "nary_tree",
"nom", "nom",
"serde 1.0.210", "serde 1.0.210",
"thiserror", "thiserror 1.0.64",
"typeshare", "typeshare",
"uuid 0.8.2", "uuid 0.8.2",
] ]
@ -4025,7 +4025,7 @@ dependencies = [
"sha2", "sha2",
"smallvec", "smallvec",
"sqlformat", "sqlformat",
"thiserror", "thiserror 1.0.64",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@ -4107,7 +4107,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 1.0.64",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -4145,7 +4145,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 1.0.64",
"tracing", "tracing",
"whoami", "whoami",
] ]
@ -4209,9 +4209,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.79" version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4302,7 +4302,16 @@ version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 1.0.64",
]
[[package]]
name = "thiserror"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa"
dependencies = [
"thiserror-impl 2.0.3",
] ]
[[package]] [[package]]
@ -4313,7 +4322,18 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
]
[[package]]
name = "thiserror-impl"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
] ]
[[package]] [[package]]
@ -4438,7 +4458,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -4558,7 +4578,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -4600,7 +4620,7 @@ dependencies = [
"log 0.4.22", "log 0.4.22",
"rand 0.8.5", "rand 0.8.5",
"sha1", "sha1",
"thiserror", "thiserror 1.0.64",
"url 2.5.2", "url 2.5.2",
"utf-8", "utf-8",
] ]
@ -4654,7 +4674,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]
@ -4857,12 +4877,14 @@ name = "visions"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"authdb", "authdb",
"cool_asserts",
"futures", "futures",
"http 1.1.0", "http 1.1.0",
"mime 0.3.17", "mime 0.3.17",
"mime_guess 2.0.5", "mime_guess 2.0.5",
"serde 1.0.210", "serde 1.0.210",
"serde_json", "serde_json",
"thiserror 2.0.3",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"typeshare", "typeshare",
@ -4958,7 +4980,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4992,7 +5014,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -5263,7 +5285,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.79", "syn 2.0.87",
] ]
[[package]] [[package]]

View File

@ -19,3 +19,7 @@ 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"
thiserror = "2.0.3"
[dev-dependencies]
cool_asserts = "2.0.3"

View File

@ -0,0 +1,146 @@
use std::{
collections::{hash_map::Iter, HashMap},
fmt::{self, Display},
io::Read,
};
use mime::Mime;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Asset could not be found")]
NotFound,
#[error("Asset could not be opened")]
Inaccessible,
#[error("An unexpected IO error occured when retrieving an asset {0}")]
UnexpectedError(std::io::Error),
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
use std::io::ErrorKind::*;
match err.kind() {
NotFound => Error::NotFound,
PermissionDenied | UnexpectedEof => Error::Inaccessible,
_ => Error::UnexpectedError(err),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct AssetId(String);
impl Display for AssetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AssetId({})", self.0)
}
}
impl From<&str> for AssetId {
fn from(s: &str) -> Self {
AssetId(s.to_owned())
}
}
impl From<String> for AssetId {
fn from(s: String) -> Self {
AssetId(s)
}
}
pub struct AssetIter<'a>(Iter<'a, AssetId, String>);
impl<'a> Iterator for AssetIter<'a> {
type Item = (&'a AssetId, &'a String);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
pub trait Assets {
fn assets<'a>(&'a self) -> AssetIter<'a>;
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error>;
}
pub struct FsAssets {
assets: HashMap<AssetId, String>,
}
impl FsAssets {
pub fn new() -> Self {
Self {
assets: HashMap::new(),
}
}
fn assets<'a>(&'a self) -> impl Iterator<Item = &'a AssetId> {
self.assets.keys()
}
}
impl Assets for FsAssets {
fn assets<'a>(&'a self) -> AssetIter<'a> {
AssetIter(self.assets.iter())
}
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error> {
let path = match self.assets.get(&asset_id) {
Some(asset) => Ok(asset),
None => Err(Error::NotFound),
}?;
let mime = mime_guess::from_path(&path).first().unwrap();
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(&path)?;
file.read_to_end(&mut content)?;
Ok((mime, content))
}
}
#[cfg(test)]
pub mod mocks {
use std::collections::HashMap;
use super::*;
pub struct MemoryAssets {
asset_paths: HashMap<AssetId, String>,
assets: HashMap<AssetId, Vec<u8>>,
}
impl MemoryAssets {
pub fn new(data: Vec<(AssetId, String, Vec<u8>)>) -> Self {
let mut asset_paths = HashMap::new();
let mut assets = HashMap::new();
data.into_iter().for_each(|(asset, path, data)| {
asset_paths.insert(asset.clone(), path);
assets.insert(asset, data);
});
Self {
asset_paths,
assets,
}
}
}
impl Assets for MemoryAssets {
fn assets<'a>(&'a self) -> AssetIter<'a> {
AssetIter(self.asset_paths.iter())
}
fn get(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), Error> {
match (self.asset_paths.get(&asset_id), self.assets.get(&asset_id)) {
(Some(path), Some(data)) => {
let mime = mime_guess::from_path(&path).first().unwrap();
Ok((mime, data.to_vec()))
}
_ => Err(Error::NotFound),
}
}
}
}

View File

@ -5,36 +5,49 @@ use std::{
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
use mime::Mime;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use urlencoding::decode; use urlencoding::decode;
use uuid::Uuid; use uuid::Uuid;
use crate::types::{AppError, Message, Tabletop, RGB}; use crate::{
asset_db::{self, AssetId, Assets},
types::{AppError, Message, Tabletop, RGB},
};
const DEFAULT_BACKGROUND_COLOR: RGB = RGB {
red: 0xca,
green: 0xb9,
blue: 0xbb,
};
#[derive(Debug)] #[derive(Debug)]
struct WebsocketClient { struct WebsocketClient {
sender: Option<UnboundedSender<Message>>, sender: Option<UnboundedSender<Message>>,
} }
#[derive(Debug)]
pub struct AppState { pub struct AppState {
pub image_base: PathBuf, pub asset_db: Box<dyn Assets + Sync + Send + 'static>,
pub tabletop: Tabletop,
pub clients: HashMap<String, WebsocketClient>, pub clients: HashMap<String, WebsocketClient>,
pub tabletop: Tabletop,
} }
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct Core(pub Arc<RwLock<AppState>>); pub struct Core(Arc<RwLock<AppState>>);
impl Core { impl Core {
pub fn new() -> Self { pub fn new<A>(assetdb: A) -> Self
where
A: Assets + Sync + Send + 'static,
{
Self(Arc::new(RwLock::new(AppState { Self(Arc::new(RwLock::new(AppState {
image_base: PathBuf::from("/home/savanni/Pictures"), asset_db: Box::new(assetdb),
clients: HashMap::new(),
tabletop: Tabletop { tabletop: Tabletop {
background_color: RGB{ red: 0xca, green: 0xb9, blue: 0xbb }, background_color: DEFAULT_BACKGROUND_COLOR,
background_image: None, background_image: None,
}, },
clients: HashMap::new(),
}))) })))
} }
@ -68,43 +81,43 @@ impl Core {
} }
} }
pub fn get_file(&self, file_name: String) -> Vec<u8> { pub fn tabletop(&self) -> Tabletop {
let file_name = decode(&file_name).expect("UTF-8"); self.0.read().unwrap().tabletop.clone()
let mut full_path = self.0.read().unwrap().image_base.clone();
full_path.push(file_name.to_string());
let mut content: Vec<u8> = Vec::new();
let mut file = std::fs::File::open(&full_path).unwrap();
file.read_to_end(&mut content).unwrap();
content
} }
pub fn available_images(&self) -> Vec<String> { pub async fn get_asset(&self, asset_id: AssetId) -> Result<(Mime, Vec<u8>), AppError> {
std::fs::read_dir(&self.0.read().unwrap().image_base) self.0
.read()
.unwrap() .unwrap()
.filter_map(|entry| match entry { .asset_db
Ok(entry_) => match mime_guess::from_path(entry_.path()).first() { .get(asset_id.clone())
Some(mime) if mime.type_() == mime::IMAGE => Some( .map_err(|err| match err {
entry_ asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)),
.path() asset_db::Error::Inaccessible => AppError::Inaccessible(format!("{}", asset_id)),
.file_name() asset_db::Error::UnexpectedError(err) => AppError::Inaccessible(format!("{}", err)),
.and_then(|filename| filename.to_str()) })
.and_then(|filename| Some(filename.to_owned())) }
.unwrap(),
), pub fn available_images(&self) -> Vec<AssetId> {
self.0
.read()
.unwrap()
.asset_db
.assets()
.filter_map(|(asset_id, value)| {
println!("[{:?}] {}", mime_guess::from_path(&value).first(), value);
match mime_guess::from_path(&value).first() {
Some(mime) if mime.type_() == mime::IMAGE => Some(asset_id.clone()),
_ => None, _ => None,
}, }
Err(_) => None,
}) })
.collect() .collect()
} }
pub fn set_background_image(&self, path: String) -> Result<(), AppError> { pub fn set_background_image(&self, asset: AssetId) -> Result<(), AppError> {
let tabletop = { let tabletop = {
let mut state = self.0.write().unwrap(); let mut state = self.0.write().unwrap();
state.tabletop.background_image = Some(path.clone()); state.tabletop.background_image = Some(asset.clone());
state.tabletop.clone() state.tabletop.clone()
}; };
self.publish(Message::UpdateTabletop(tabletop)); self.publish(Message::UpdateTabletop(tabletop));
@ -121,3 +134,97 @@ impl Core {
}); });
} }
} }
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use crate::asset_db::mocks::MemoryAssets;
fn test_core() -> Core {
let assets = MemoryAssets::new(vec![
(
AssetId::from("asset_1"),
"asset_1.png".to_owned(),
String::from("abcdefg").into_bytes(),
),
(
AssetId::from("asset_2"),
"asset_2.jpg".to_owned(),
String::from("abcdefg").into_bytes(),
),
(
AssetId::from("asset_3"),
"asset_3".to_owned(),
String::from("abcdefg").into_bytes(),
),
(
AssetId::from("asset_4"),
"asset_4".to_owned(),
String::from("abcdefg").into_bytes(),
),
(
AssetId::from("asset_5"),
"asset_5".to_owned(),
String::from("abcdefg").into_bytes(),
),
]);
Core::new(assets)
}
#[tokio::test]
async fn it_lists_available_images() {
let core = test_core();
let image_paths = core.available_images();
assert_eq!(image_paths.len(), 2);
}
#[tokio::test]
async fn it_retrieves_an_asset() {
let core = test_core();
assert_matches!(core.get_asset(AssetId::from("asset_1")).await, Ok((mime, data)) => {
assert_eq!(mime.type_(), mime::IMAGE);
assert_eq!(data, "abcdefg".as_bytes());
});
}
#[tokio::test]
async fn it_can_retrieve_the_default_tabletop() {
let core = test_core();
assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => {
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
assert_eq!(background_image, None);
});
}
#[tokio::test]
async fn it_can_change_the_tabletop_background() {
let core = test_core();
assert_matches!(core.set_background_image(AssetId::from("asset_1")), Ok(()));
assert_matches!(core.tabletop(), Tabletop{ background_color, background_image } => {
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
assert_eq!(background_image, Some(AssetId::from("asset_1")));
});
}
#[tokio::test]
async fn it_sends_notices_to_clients_on_tabletop_change() {
let core = test_core();
let client_id = core.register_client();
let mut receiver = core.connect_client(client_id);
assert_matches!(core.set_background_image(AssetId::from("asset_1")), Ok(()));
match receiver.recv().await {
Some(Message::UpdateTabletop(Tabletop {
background_color,
background_image,
})) => {
assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR);
assert_eq!(background_image, Some(AssetId::from("asset_1")));
}
None => panic!("receiver did not get a message"),
}
}
}

View File

@ -1,11 +1,10 @@
use std::{pin::Pin, time::Duration}; use std::future::Future;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio_stream::wrappers::UnboundedReceiverStream; use warp::{http::Response, http::StatusCode, reply::Reply, ws::Message};
use warp::{http::Response, reply::Reply, ws::Message};
use crate::core::Core; use crate::{asset_db::AssetId, core::Core, types::AppError};
/* /*
pub async fn handle_auth( pub async fn handle_auth(
@ -31,21 +30,49 @@ pub async fn handle_auth(
} }
*/ */
pub async fn handle_file(core: Core, file_name: String) -> impl Reply { pub async fn handler<F>(f: F) -> impl Reply
let mimetype = mime_guess::from_path(&file_name).first().unwrap(); where
let bytes = core.get_file(file_name); F: Future<Output = Result<Response<Vec<u8>>, AppError>>,
Response::builder() {
.header("application-type", mimetype.to_string()) match f.await {
Ok(response) => response,
Err(AppError::NotFound(_)) => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(vec![])
.unwrap(),
Err(_) => Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(vec![])
.unwrap(),
}
}
pub async fn handle_file(core: Core, asset_id: AssetId) -> impl Reply {
handler(async move {
let (mime, bytes) = core.get_asset(asset_id).await?;
Ok(Response::builder()
.header("application-type", mime.to_string())
.body(bytes) .body(bytes)
.unwrap() .unwrap())
})
.await
} }
pub async fn handle_available_images(core: Core) -> impl Reply { pub async fn handle_available_images(core: Core) -> impl Reply {
Response::builder() handler(async move {
let image_paths: Vec<String> = core
.available_images()
.into_iter()
.map(|path| format!("{}", path))
.collect();
Ok(Response::builder()
.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Origin", "*")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(serde_json::to_string(&core.available_images()).unwrap()) .body(serde_json::to_vec(&image_paths).unwrap())
.unwrap() .unwrap())
})
.await
} }
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
@ -56,25 +83,31 @@ pub struct RegisterResponse {
url: String, url: String,
} }
pub async fn handle_register_client(core: Core, request: RegisterRequest) -> impl Reply { pub async fn handle_register_client(core: Core, _request: RegisterRequest) -> impl Reply {
handler(async move {
let client_id = core.register_client(); let client_id = core.register_client();
Response::builder() Ok(Response::builder()
.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Origin", "*")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body( .body(
serde_json::to_string(&RegisterResponse { serde_json::to_vec(&RegisterResponse {
url: format!("ws://127.0.0.1:8001/ws/{}", client_id), url: format!("ws://127.0.0.1:8001/ws/{}", client_id),
}) })
.unwrap(), .unwrap(),
) )
.unwrap() .unwrap())
})
.await
} }
pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Reply { pub async fn handle_unregister_client(core: Core, client_id: String) -> impl Reply {
handler(async move {
core.unregister_client(client_id); core.unregister_client(client_id);
warp::reply::reply() Ok(Response::builder().status(StatusCode::NO_CONTENT).body(vec![]).unwrap())
})
.await
} }
pub async fn handle_connect_websocket( pub async fn handle_connect_websocket(
@ -82,7 +115,6 @@ pub async fn handle_connect_websocket(
ws: warp::ws::Ws, ws: warp::ws::Ws,
client_id: String, client_id: String,
) -> impl Reply { ) -> impl Reply {
// println!("handle_connect_websocket: {}", client_id);
ws.on_upgrade(move |socket| { ws.on_upgrade(move |socket| {
let core = core.clone(); let core = core.clone();
async move { async move {
@ -90,7 +122,7 @@ pub async fn handle_connect_websocket(
let mut receiver = core.connect_client(client_id.clone()); let mut receiver = core.connect_client(client_id.clone());
tokio::task::spawn(async move { tokio::task::spawn(async move {
let tabletop = core.0.read().unwrap().tabletop.clone(); let tabletop = core.tabletop();
let _ = ws_sender let _ = ws_sender
.send(Message::text( .send(Message::text(
serde_json::to_string(&crate::types::Message::UpdateTabletop(tabletop)) serde_json::to_string(&crate::types::Message::UpdateTabletop(tabletop))
@ -110,12 +142,15 @@ pub async fn handle_connect_websocket(
} }
pub async fn handle_set_background_image(core: Core, image_name: String) -> impl Reply { pub async fn handle_set_background_image(core: Core, image_name: String) -> impl Reply {
let _ = core.set_background_image(image_name); handler(async move {
let _ = core.set_background_image(AssetId::from(image_name));
Response::builder() Ok(Response::builder()
.header("Access-Control-Allow-Origin", "*") .header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "*") .header("Access-Control-Allow-Methods", "*")
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body("") .body(vec![])
.unwrap() .unwrap())
})
.await
} }

View File

@ -1,3 +1,4 @@
use asset_db::{AssetId, FsAssets};
use authdb::AuthError; use authdb::AuthError;
use handlers::{ use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest handle_available_images, handle_connect_websocket, handle_file, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
@ -13,10 +14,9 @@ use warp::{
Filter, Filter,
}; };
mod asset_db;
mod core; mod core;
mod handlers; mod handlers;
// use handlers::handle_auth;
mod types; mod types;
@ -96,14 +96,14 @@ 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 core = core::Core::new(FsAssets::new());
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)
.and(warp::get()) .and(warp::get())
.then({ .then({
let core = core.clone(); let core = core.clone();
move |file_name| handle_file(core.clone(), file_name) move |file_name| handle_file(core.clone(), AssetId::from(file_name))
}); });
let route_available_images = warp::path!("api" / "v1" / "image").and(warp::get()).then({ let route_available_images = warp::path!("api" / "v1" / "image").and(warp::get()).then({

View File

@ -1,12 +1,18 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare; use typeshare::typeshare;
use crate::asset_db::AssetId;
#[derive(Debug)] #[derive(Debug)]
pub enum AppError { pub enum AppError {
NotFound(String),
Inaccessible(String),
JsonError(serde_json::Error), JsonError(serde_json::Error),
UnexpectedError(String),
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[typeshare] #[typeshare]
pub struct RGB { pub struct RGB {
@ -20,7 +26,7 @@ pub struct RGB {
#[typeshare] #[typeshare]
pub struct Tabletop { pub struct Tabletop {
pub background_color: RGB, pub background_color: RGB,
pub background_image: Option<String>, pub background_image: Option<AssetId>,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]