Set up automated tests for the application Core #266
|
@ -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]]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
.body(bytes)
|
Ok(response) => response,
|
||||||
.unwrap()
|
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)
|
||||||
|
.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 {
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
let image_paths: Vec<String> = core
|
||||||
.header("Content-Type", "application/json")
|
.available_images()
|
||||||
.body(serde_json::to_string(&core.available_images()).unwrap())
|
.into_iter()
|
||||||
.unwrap()
|
.map(|path| format!("{}", path))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.body(serde_json::to_vec(&image_paths).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 {
|
||||||
let client_id = core.register_client();
|
handler(async move {
|
||||||
|
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 {
|
||||||
core.unregister_client(client_id);
|
handler(async move {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
Loading…
Reference in New Issue