Compare commits
3 Commits
ba56a82926
...
83ad4ca784
| Author | SHA1 | Date | |
|---|---|---|---|
| 83ad4ca784 | |||
| d6972e604e | |||
| 9d02ae1c6b |
108
Cargo.lock
generated
108
Cargo.lock
generated
@@ -26,6 +26,21 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstream"
|
name = "anstream"
|
||||||
version = "0.6.18"
|
version = "0.6.18"
|
||||||
@@ -105,7 +120,9 @@ name = "ascend"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"bincode",
|
||||||
"camino",
|
"camino",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"confik",
|
"confik",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
@@ -117,6 +134,7 @@ dependencies = [
|
|||||||
"leptos_router",
|
"leptos_router",
|
||||||
"moonboard-parser",
|
"moonboard-parser",
|
||||||
"rand",
|
"rand",
|
||||||
|
"redb",
|
||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -129,6 +147,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"tracing-subscriber-wasm",
|
"tracing-subscriber-wasm",
|
||||||
"type-toppings",
|
"type-toppings",
|
||||||
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"xdg",
|
"xdg",
|
||||||
@@ -320,12 +339,36 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
|
||||||
|
dependencies = [
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.39"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ciborium"
|
name = "ciborium"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -510,6 +553,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -1174,6 +1223,29 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.61"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1776,6 +1848,15 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
@@ -2087,6 +2168,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redb"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea0a72cd7140de9fc3e318823b883abf819c20d478ec89ce880466dc2ef263c6"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -2370,6 +2460,12 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -2906,6 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3048,7 +3145,7 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3057,6 +3154,15 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -9,33 +9,37 @@ publish = false
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
moonboard-parser = { workspace = true, optional = true }
|
|
||||||
axum = { version = "0.7", optional = true }
|
axum = { version = "0.7", optional = true }
|
||||||
|
camino = { version = "1.1", optional = true }
|
||||||
|
chrono = { version = "0.4.39", features = ["now", "serde"] }
|
||||||
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
|
confik = { version = "0.12", optional = true, features = ["camino"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
derive_more = { version = "1", features = ["display", "error", "from"] }
|
||||||
|
http = "1"
|
||||||
leptos = { version = "0.7.4", features = ["tracing"] }
|
leptos = { version = "0.7.4", features = ["tracing"] }
|
||||||
server_fn = { version = "0.7.4", features = ["cbor"] }
|
|
||||||
leptos_axum = { version = "0.7", optional = true }
|
leptos_axum = { version = "0.7", optional = true }
|
||||||
leptos_meta = { version = "0.7" }
|
leptos_meta = { version = "0.7" }
|
||||||
leptos_router = { version = "0.7.0" }
|
leptos_router = { version = "0.7.0" }
|
||||||
|
moonboard-parser = { workspace = true, optional = true }
|
||||||
|
rand = { version = "0.8", optional = true }
|
||||||
|
ron = { version = "0.8" }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
server_fn = { version = "0.7.4", features = ["cbor"] }
|
||||||
|
smart-default = "0.7.1"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
|
||||||
tower = { version = "0.4", optional = true }
|
tower = { version = "0.4", optional = true }
|
||||||
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
tower-http = { version = "0.5", features = ["fs"], optional = true }
|
||||||
wasm-bindgen = "=0.2.99"
|
|
||||||
http = "1"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
derive_more = { version = "1", features = ["display", "error", "from"] }
|
|
||||||
clap = { version = "4.5.7", features = ["derive"] }
|
|
||||||
camino = { version = "1.1", optional = true }
|
|
||||||
type-toppings = { version = "0.2.1", features = ["result"] }
|
|
||||||
tracing = { version = "0.1" }
|
tracing = { version = "0.1" }
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
tracing-subscriber-wasm = "0.1.0"
|
tracing-subscriber-wasm = "0.1.0"
|
||||||
ron = { version = "0.8" }
|
type-toppings = { version = "0.2.1", features = ["result"] }
|
||||||
rand = { version = "0.8", optional = true }
|
wasm-bindgen = "=0.2.99"
|
||||||
web-sys = { version = "0.3.76", features = ["File", "FileList"] }
|
web-sys = { version = "0.3.76", features = ["File", "FileList"] }
|
||||||
smart-default = "0.7.1"
|
|
||||||
confik = { version = "0.12", optional = true, features = ["camino"] }
|
|
||||||
xdg = { version = "2.5", optional = true }
|
xdg = { version = "2.5", optional = true }
|
||||||
|
uuid = { version = "1.12", features = ["serde", "v4"] }
|
||||||
|
redb = { version = "2.4", optional = true }
|
||||||
|
bincode = { version = "1.3", optional = true }
|
||||||
|
|
||||||
[dev-dependencies.serde_json]
|
[dev-dependencies.serde_json]
|
||||||
version = "1"
|
version = "1"
|
||||||
@@ -44,6 +48,8 @@ version = "1"
|
|||||||
hydrate = ["leptos/hydrate"]
|
hydrate = ["leptos/hydrate"]
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
|
"dep:redb",
|
||||||
|
"dep:bincode",
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
"dep:rand",
|
"dep:rand",
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
|
|||||||
@@ -14,59 +14,12 @@ pub struct HeaderItem {
|
|||||||
/// Header with background color etc.
|
/// Header with background color etc.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
pub fn StyledHeader(items: HeaderItems) -> impl IntoView {
|
||||||
let fancy = false;
|
view! {
|
||||||
|
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
|
||||||
if fancy {
|
// <div class="container mx-auto" >
|
||||||
view! {
|
<Header items />
|
||||||
<div class="flex">
|
// </div>
|
||||||
// Left gradient chunk
|
</div>
|
||||||
<div class="flex-grow">
|
|
||||||
<div class="h-2/5" style="background: #eaac53" />
|
|
||||||
<div
|
|
||||||
class="h-3/5"
|
|
||||||
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-none container mx-auto text-black" style="background: #eaac53">
|
|
||||||
<Header items />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Right gradient chunk
|
|
||||||
<div class="flex-grow" style="background: #eaac53" />
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
// Left gradient chunk
|
|
||||||
<div class="flex-grow" />
|
|
||||||
|
|
||||||
<div class="flex-none container mx-auto">
|
|
||||||
// Background color gradient
|
|
||||||
<div
|
|
||||||
class="h-6"
|
|
||||||
style="background: linear-gradient(to bottom left, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
// Right gradient chunk
|
|
||||||
<div class="flex-grow">
|
|
||||||
<div class="h-4/5" style="background: #eaac53" />
|
|
||||||
<div
|
|
||||||
class="h-1/5"
|
|
||||||
style="background: linear-gradient(to bottom right, #eaac53 49.5%, rgb(15 23 42) 50.5%)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
} else {
|
|
||||||
view! {
|
|
||||||
<div class="bg-orange-300 text-black border-b-2 border-b-orange-400">
|
|
||||||
<div class="container mx-auto" >
|
|
||||||
<Header items />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,158 @@
|
|||||||
//! Shared models between server and client code.
|
//! Shared models between server and client code.
|
||||||
|
|
||||||
use serde::Deserialize;
|
pub use v1::Hold;
|
||||||
use serde::Serialize;
|
pub use v1::HoldPosition;
|
||||||
use std::collections::BTreeMap;
|
pub use v1::HoldRole;
|
||||||
|
pub use v1::Image;
|
||||||
|
pub use v2::Method;
|
||||||
|
pub use v2::Problem;
|
||||||
|
pub use v2::ProblemId;
|
||||||
|
pub use v2::Root;
|
||||||
|
pub use v2::Wall;
|
||||||
|
pub use v2::WallId;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
pub mod v2 {
|
||||||
pub struct Wall {
|
use super::v1;
|
||||||
pub rows: u64,
|
use serde::Deserialize;
|
||||||
pub cols: u64,
|
use serde::Serialize;
|
||||||
pub holds: BTreeMap<HoldPosition, Hold>,
|
use std::collections::BTreeMap;
|
||||||
}
|
use std::collections::BTreeSet;
|
||||||
impl Wall {
|
|
||||||
pub fn new(rows: u64, cols: u64) -> Self {
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
let mut holds = BTreeMap::new();
|
pub struct Root {
|
||||||
for row in 0..rows {
|
pub walls: BTreeSet<WallId>,
|
||||||
for col in 0..cols {
|
}
|
||||||
let position = HoldPosition { row, col };
|
|
||||||
let hold = Hold { position, image: None };
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
holds.insert(position, hold);
|
pub struct Wall {
|
||||||
}
|
pub uid: WallId,
|
||||||
|
pub rows: u64,
|
||||||
|
pub cols: u64,
|
||||||
|
pub holds: BTreeMap<v1::HoldPosition, v1::Hold>,
|
||||||
|
pub problems: BTreeSet<ProblemId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||||
|
pub struct WallId(pub uuid::Uuid);
|
||||||
|
impl WallId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4())
|
||||||
}
|
}
|
||||||
Self { rows, cols, holds }
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
impl Default for Wall {
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
fn default() -> Self {
|
pub struct Problem {
|
||||||
Self::new(12, 12)
|
pub uid: ProblemId,
|
||||||
|
pub name: String,
|
||||||
|
pub set_by: String,
|
||||||
|
pub holds: BTreeMap<v1::HoldPosition, v1::HoldRole>,
|
||||||
|
pub method: Method,
|
||||||
|
pub date_added: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||||
|
pub struct ProblemId(pub uuid::Uuid);
|
||||||
|
impl ProblemId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Method {
|
||||||
|
FeetFollowHands,
|
||||||
|
Footless,
|
||||||
|
FootlessPlusKickboard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
pub mod v1 {
|
||||||
pub struct HoldPosition {
|
use serde::Deserialize;
|
||||||
/// Starting from 0
|
use serde::Serialize;
|
||||||
pub row: u64,
|
use smart_default::SmartDefault;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
/// Starting from 0
|
const STATE_VERSION: u64 = 1;
|
||||||
pub col: u64,
|
|
||||||
}
|
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
|
||||||
|
pub struct PersistentState {
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
/// State schema version
|
||||||
pub struct Problem {
|
#[default(STATE_VERSION)]
|
||||||
pub holds: BTreeMap<HoldPosition, HoldRole>,
|
pub version: u64,
|
||||||
}
|
|
||||||
|
pub wall: Wall,
|
||||||
/// The role of a hold on a route
|
pub problems: Problems,
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
}
|
||||||
pub enum HoldRole {
|
|
||||||
/// Start hold
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
Start,
|
pub struct Problems {
|
||||||
|
pub problems: BTreeSet<Problem>,
|
||||||
/// Any hold on the route without a specific role
|
}
|
||||||
Normal,
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
/// Zone hold
|
pub struct Wall {
|
||||||
Zone,
|
pub rows: u64,
|
||||||
|
pub cols: u64,
|
||||||
/// End hold
|
pub holds: BTreeMap<HoldPosition, Hold>,
|
||||||
End,
|
}
|
||||||
}
|
impl Wall {
|
||||||
|
pub fn new(rows: u64, cols: u64) -> Self {
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
let mut holds = BTreeMap::new();
|
||||||
pub struct Hold {
|
for row in 0..rows {
|
||||||
pub position: HoldPosition,
|
for col in 0..cols {
|
||||||
pub image: Option<Image>,
|
let position = HoldPosition { row, col };
|
||||||
}
|
let hold = Hold { position, image: None };
|
||||||
|
holds.insert(position, hold);
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
}
|
||||||
pub struct Image {
|
}
|
||||||
pub filename: String,
|
Self { rows, cols, holds }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for Wall {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(12, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
||||||
|
pub struct HoldPosition {
|
||||||
|
/// Starting from 0
|
||||||
|
pub row: u64,
|
||||||
|
|
||||||
|
/// Starting from 0
|
||||||
|
pub col: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct Problem {
|
||||||
|
pub holds: BTreeMap<HoldPosition, HoldRole>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The role of a hold on a route
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub enum HoldRole {
|
||||||
|
/// Start hold
|
||||||
|
Start,
|
||||||
|
|
||||||
|
/// Any hold on the route without a specific role
|
||||||
|
Normal,
|
||||||
|
|
||||||
|
/// Zone hold
|
||||||
|
Zone,
|
||||||
|
|
||||||
|
/// End hold
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Hold {
|
||||||
|
pub position: HoldPosition,
|
||||||
|
pub image: Option<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Image {
|
||||||
|
pub filename: String,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ pub fn EditWall() -> impl leptos::IntoView {
|
|||||||
link: Some("/".to_string()),
|
link: Some("/".to_string()),
|
||||||
}],
|
}],
|
||||||
middle: vec![HeaderItem {
|
middle: vec![HeaderItem {
|
||||||
text: "EDIT WALL".to_string(),
|
text: "HOLDS".to_string(),
|
||||||
link: None,
|
link: None,
|
||||||
}],
|
}],
|
||||||
right: vec![],
|
right: vec![],
|
||||||
@@ -151,12 +151,9 @@ pub struct Image {
|
|||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||||
use crate::server::state::State;
|
todo!()
|
||||||
|
// let wall = state.persistent.with(|s| s.wall.clone()).await;
|
||||||
let state = expect_context::<State>();
|
// Ok(RonCodec::new(InitialData { wall }))
|
||||||
|
|
||||||
let wall = state.persistent.with(|s| s.wall.clone()).await;
|
|
||||||
Ok(RonCodec::new(InitialData { wall }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(name = SetImage, input = Cbor)]
|
#[server(name = SetImage, input = Cbor)]
|
||||||
@@ -164,25 +161,24 @@ async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
|||||||
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
|
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
|
||||||
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());
|
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());
|
||||||
|
|
||||||
use crate::server::state::State;
|
|
||||||
|
|
||||||
// TODO: Fix file extension presumption, and possibly use uuid
|
// TODO: Fix file extension presumption, and possibly use uuid
|
||||||
let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col);
|
let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col);
|
||||||
tokio::fs::create_dir_all("datastore/public/holds").await?;
|
tokio::fs::create_dir_all("datastore/public/holds").await?;
|
||||||
tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?;
|
tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?;
|
||||||
|
|
||||||
let state = expect_context::<State>();
|
todo!()
|
||||||
state
|
// let state = expect_context::<State>();
|
||||||
.persistent
|
// state
|
||||||
.update(|s| {
|
// .persistent
|
||||||
if let Some(hold) = s.wall.holds.get_mut(&hold_position) {
|
// .update(|s| {
|
||||||
hold.image = Some(models::Image { filename });
|
// if let Some(hold) = s.wall.holds.get_mut(&hold_position) {
|
||||||
}
|
// hold.image = Some(models::Image { filename });
|
||||||
})
|
// }
|
||||||
.await?;
|
// })
|
||||||
|
// .await?;
|
||||||
|
|
||||||
// Return updated hold
|
// // Return updated hold
|
||||||
let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await;
|
// let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await;
|
||||||
|
|
||||||
Ok(hold)
|
// Ok(hold)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ use crate::codec::ron::RonCodec;
|
|||||||
use crate::components::header::HeaderItem;
|
use crate::components::header::HeaderItem;
|
||||||
use crate::components::header::HeaderItems;
|
use crate::components::header::HeaderItems;
|
||||||
use crate::components::header::StyledHeader;
|
use crate::components::header::StyledHeader;
|
||||||
|
use crate::models;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -56,31 +58,39 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct InitialData {}
|
pub struct InitialData {
|
||||||
|
problems: BTreeSet<models::Problem>,
|
||||||
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||||
// use crate::server::state::State;
|
todo!()
|
||||||
// let state = expect_context::<State>();
|
// let state = expect_context::<State>();
|
||||||
|
|
||||||
// TODO: provide info on current routes set
|
// let problems = state
|
||||||
|
// .persistent
|
||||||
|
// .with(|s| {
|
||||||
|
// let problems = &s.problems.problems;
|
||||||
|
// problems.clone()
|
||||||
|
// })
|
||||||
|
// .await;
|
||||||
|
|
||||||
Ok(RonCodec::new(InitialData {}))
|
// Ok(RonCodec::new(InitialData { problems }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(name = ImportFromMiniMoonboard)]
|
#[server(name = ImportFromMiniMoonboard)]
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
|
async fn import_from_mini_moonboard() -> Result<(), ServerFnError> {
|
||||||
use crate::server::config::Config;
|
use crate::server::config::Config;
|
||||||
use crate::server::state::State;
|
|
||||||
|
|
||||||
tracing::info!("Importing mini moonboard problems");
|
todo!()
|
||||||
|
// tracing::info!("Importing mini moonboard problems");
|
||||||
|
|
||||||
let config = expect_context::<Config>();
|
// let config = expect_context::<Config>();
|
||||||
let state = expect_context::<State>();
|
// let state = expect_context::<State>();
|
||||||
|
|
||||||
crate::server::operations::import_mini_moonboard_problems(&config, &state).await?;
|
// crate::server::operations::import_mini_moonboard_problems(&config, &state).await?;
|
||||||
|
|
||||||
// TODO: Return information about what was done
|
// // TODO: Return information about what was done
|
||||||
Ok(())
|
// Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use leptos::reactive::graph::ReactiveNode;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Wall() -> impl leptos::IntoView {
|
pub fn Wall() -> impl leptos::IntoView {
|
||||||
@@ -80,7 +81,13 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
|||||||
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
|
<div style="max-height: 90vh; max-width: 90vh;" class=move || { grid_classes.clone() }>{cells}</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button onclick=move |_| problem_fetcher.mark_dirty() text="Next problem ➤" />
|
<div>
|
||||||
|
// TODO:
|
||||||
|
// <p>{current_problem.read().as_ref().map(|p| p.name.clone())}</p>
|
||||||
|
// <p>{current_problem.read().as_ref().map(|p| p.set_by.clone())}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onclick=move |_| problem_fetcher.mark_dirty() text="➤ Next problem" />
|
||||||
</ div>
|
</ div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -119,32 +126,47 @@ pub struct InitialData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
#[tracing::instrument(skip_all, err)]
|
||||||
use crate::server::state::State;
|
async fn load_initial_data(wall_id: models::WallId) -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||||
|
let db = expect_context::<Arc<redb::Database>>();
|
||||||
|
|
||||||
let state = expect_context::<State>();
|
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||||
|
enum Error {
|
||||||
|
#[display("Wall not found: {_0:?}")]
|
||||||
|
NotFound(#[error(not(source))] models::WallId),
|
||||||
|
}
|
||||||
|
|
||||||
|
let wall = tokio::task::spawn_blocking(move || -> Result<models::Wall, ServerFnError> {
|
||||||
|
let read_txn = db.begin_read()?;
|
||||||
|
|
||||||
|
let walls_table = read_txn.open_table(crate::server::db::current::TABLE_WALLS)?;
|
||||||
|
let wall = walls_table.get(wall_id)?.ok_or(Error::NotFound(wall_id))?.value();
|
||||||
|
|
||||||
|
Ok(wall)
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
let wall = state.persistent.with(|s| s.wall.clone()).await;
|
|
||||||
Ok(RonCodec::new(InitialData { wall }))
|
Ok(RonCodec::new(InitialData { wall }))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
|
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
|
||||||
use crate::server::state::State;
|
todo!()
|
||||||
use rand::seq::IteratorRandom;
|
// use rand::seq::IteratorRandom;
|
||||||
|
|
||||||
let state = expect_context::<State>();
|
// let state = expect_context::<State>();
|
||||||
|
|
||||||
let problem = state
|
// let problem = state
|
||||||
.persistent
|
// .persistent
|
||||||
.with(|s| {
|
// .with(|s| {
|
||||||
let problems = &s.problems.problems;
|
// let problems = &s.problems.problems;
|
||||||
let rng = &mut rand::thread_rng();
|
// let rng = &mut rand::thread_rng();
|
||||||
problems.iter().choose(rng).cloned()
|
// problems.iter().choose(rng).cloned()
|
||||||
})
|
// })
|
||||||
.await;
|
// .await;
|
||||||
|
|
||||||
tracing::debug!("Returning randomized problem: {problem:?}");
|
// tracing::debug!("Returning randomized problem: {problem:?}");
|
||||||
|
|
||||||
Ok(RonCodec::new(problem))
|
// Ok(RonCodec::new(problem))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,17 @@ use cli::Cli;
|
|||||||
use config::Config;
|
use config::Config;
|
||||||
use confik::Configuration;
|
use confik::Configuration;
|
||||||
use confik::EnvSource;
|
use confik::EnvSource;
|
||||||
use persistence::Persistent;
|
|
||||||
use state::PersistentState;
|
|
||||||
use state::State;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::sync::Arc;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use type_toppings::ResultExt;
|
use type_toppings::ResultExt;
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod db;
|
||||||
mod migrations;
|
mod migrations;
|
||||||
pub mod operations;
|
pub mod operations;
|
||||||
pub mod persistence;
|
|
||||||
pub mod state;
|
|
||||||
|
|
||||||
pub const STATE_FILE: &str = "datastore/private/state.ron";
|
pub const STATE_FILE: &str = "datastore/private/state.ron";
|
||||||
|
|
||||||
@@ -43,12 +39,6 @@ pub async fn main() {
|
|||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve => serve(cli).await.unwrap_or_report(),
|
Command::Serve => serve(cli).await.unwrap_or_report(),
|
||||||
Command::ResetState => {
|
|
||||||
let s = PersistentState::default();
|
|
||||||
let p = Path::new(STATE_FILE);
|
|
||||||
tracing::info!("Resetting state to default: {}", p.display());
|
|
||||||
Persistent::persist(p, &s).await.unwrap_or_report();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,32 +47,34 @@ async fn serve(cli: Cli) -> Result<(), Error> {
|
|||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::app::shell;
|
use crate::app::shell;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::prelude::*;
|
|
||||||
use leptos_axum::LeptosRoutes;
|
use leptos_axum::LeptosRoutes;
|
||||||
use leptos_axum::generate_route_list;
|
use leptos_axum::generate_route_list;
|
||||||
|
|
||||||
migrations::run_migrations().await;
|
tracing::debug!("Creating DB");
|
||||||
|
let db = Arc::new(db::create()?);
|
||||||
|
|
||||||
|
migrations::run_migrations(&db).await.map_err(Error::Migration)?;
|
||||||
|
|
||||||
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
// Setting get_configuration(None) means we'll be using cargo-leptos's env values
|
||||||
// For deployment these variables are:
|
// For deployment these variables are:
|
||||||
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
// <https://github.com/leptos-rs/start-axum#executing-a-server-on-a-remote-machine-without-the-toolchain>
|
||||||
// Alternately a file can be specified such as Some("Cargo.toml")
|
// Alternately a file can be specified such as Some("Cargo.toml")
|
||||||
// The file would need to be included with the executable when moved to deployment
|
// The file would need to be included with the executable when moved to deployment
|
||||||
let leptos_conf_file = get_configuration(None).unwrap_or_report();
|
let leptos_conf_file = leptos::config::get_configuration(None).unwrap_or_report();
|
||||||
let leptos_options = leptos_conf_file.leptos_options;
|
let leptos_options = leptos_conf_file.leptos_options;
|
||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
let config = load_config(cli)?;
|
let config = load_config(cli)?;
|
||||||
let server_state = load_state().await?;
|
|
||||||
|
|
||||||
|
tracing::debug!("Creating app router");
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.leptos_routes_with_context(
|
.leptos_routes_with_context(
|
||||||
&leptos_options,
|
&leptos_options,
|
||||||
routes,
|
routes,
|
||||||
move || {
|
move || {
|
||||||
provide_context(server_state.clone());
|
leptos::prelude::provide_context(Arc::clone(&db));
|
||||||
provide_context(config.clone())
|
leptos::prelude::provide_context(config.clone())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let leptos_options = leptos_options.clone();
|
let leptos_options = leptos_options.clone();
|
||||||
@@ -93,7 +85,9 @@ async fn serve(cli: Cli) -> Result<(), Error> {
|
|||||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||||
.with_state(leptos_options);
|
.with_state(leptos_options);
|
||||||
|
|
||||||
|
tracing::debug!("Binding TCP listener");
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||||
|
|
||||||
tracing::info!("Listening on http://{addr}");
|
tracing::info!("Listening on http://{addr}");
|
||||||
axum::serve(listener, app.into_make_service()).await?;
|
axum::serve(listener, app.into_make_service()).await?;
|
||||||
|
|
||||||
@@ -106,22 +100,6 @@ fn file_service(path: impl AsRef<Path>) -> ServeDir {
|
|||||||
ServeDir::new(path)
|
ServeDir::new(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
|
||||||
async fn load_state() -> Result<State, Error> {
|
|
||||||
tracing::info!("Loading state");
|
|
||||||
|
|
||||||
let p = PathBuf::from(STATE_FILE);
|
|
||||||
|
|
||||||
let persistent = if p.try_exists()? {
|
|
||||||
Persistent::<PersistentState>::load(&p).await?
|
|
||||||
} else {
|
|
||||||
tracing::info!("No state found at {STATE_FILE}, creating default state");
|
|
||||||
Persistent::<PersistentState>::new(PersistentState::default(), p)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(State { persistent })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_config(cli: Cli) -> Result<Config, Error> {
|
fn load_config(cli: Cli) -> Result<Config, Error> {
|
||||||
let mut builder = config::Config::builder();
|
let mut builder = config::Config::builder();
|
||||||
if cli
|
if cli
|
||||||
@@ -139,11 +117,14 @@ fn load_config(cli: Cli) -> Result<Config, Error> {
|
|||||||
#[display("Server crash: {_variant}")]
|
#[display("Server crash: {_variant}")]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Persistence(persistence::Error),
|
|
||||||
Parser(moonboard_parser::Error),
|
Parser(moonboard_parser::Error),
|
||||||
|
|
||||||
#[display("Failed migration")]
|
#[display("Failed migration")]
|
||||||
|
#[from(ignore)]
|
||||||
Migration(Box<dyn std::error::Error>),
|
Migration(Box<dyn std::error::Error>),
|
||||||
|
|
||||||
Confik(confik::Error),
|
Confik(confik::Error),
|
||||||
|
|
||||||
|
Database(redb::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ pub struct Cli {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
#[default]
|
#[default]
|
||||||
Serve,
|
Serve,
|
||||||
|
|
||||||
/// Resets state, replacing it with defaults
|
|
||||||
ResetState,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_config_location() -> camino::Utf8PathBuf {
|
fn default_config_location() -> camino::Utf8PathBuf {
|
||||||
|
|||||||
65
crates/ascend/src/server/db.rs
Normal file
65
crates/ascend/src/server/db.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use bincode::Bincode;
|
||||||
|
use redb::Database;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod bincode;
|
||||||
|
|
||||||
|
pub const DB_FILE: &str = "datastore/private/ascend.redb";
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
pub fn create() -> Result<Database, redb::Error> {
|
||||||
|
let file = PathBuf::from(DB_FILE);
|
||||||
|
|
||||||
|
// Create parent dirs
|
||||||
|
if let Some(parent_dir) = file.parent() {
|
||||||
|
std::fs::create_dir_all(parent_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let db = Database::create(file)?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub fn get_version(db: &Database) -> Result<Option<Version>, redb::Error> {
|
||||||
|
let txn = db.begin_read()?;
|
||||||
|
let version = txn.open_table(TABLE_VERSION)?.get(())?.map(|v| v.value());
|
||||||
|
Ok(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TABLE_VERSION: TableDefinition<(), Bincode<Version>> = TableDefinition::new("version");
|
||||||
|
#[derive(Serialize, Deserialize, Debug, derive_more::Display)]
|
||||||
|
#[display("{version}")]
|
||||||
|
pub struct Version {
|
||||||
|
pub version: u64,
|
||||||
|
}
|
||||||
|
impl Version {
|
||||||
|
pub fn current() -> Version {
|
||||||
|
Version { version: current::VERSION }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use v2 as current;
|
||||||
|
|
||||||
|
pub mod v2 {
|
||||||
|
use crate::models;
|
||||||
|
use crate::server::db::bincode::Bincode;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
|
||||||
|
pub const VERSION: u64 = 2;
|
||||||
|
|
||||||
|
pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v2::Root>> = TableDefinition::new("root");
|
||||||
|
pub const TABLE_WALLS: TableDefinition<Bincode<models::v2::WallId>, Bincode<models::v2::Wall>> = TableDefinition::new("walls");
|
||||||
|
pub const TABLE_PROBLEMS: TableDefinition<Bincode<(models::v2::WallId, models::v2::ProblemId)>, Bincode<models::v2::Problem>> =
|
||||||
|
TableDefinition::new("problems");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod v1 {
|
||||||
|
use crate::models;
|
||||||
|
use crate::server::db::bincode::Bincode;
|
||||||
|
use redb::TableDefinition;
|
||||||
|
|
||||||
|
pub const TABLE_ROOT: TableDefinition<(), Bincode<models::v1::PersistentState>> = TableDefinition::new("root");
|
||||||
|
}
|
||||||
52
crates/ascend/src/server/db/bincode.rs
Normal file
52
crates/ascend/src/server/db/bincode.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use redb::Value;
|
||||||
|
|
||||||
|
/// Wrapper type to handle keys and values using bincode serialization
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bincode<T>(pub T);
|
||||||
|
|
||||||
|
impl<T> Value for Bincode<T>
|
||||||
|
where
|
||||||
|
T: std::fmt::Debug + serde::Serialize + for<'a> serde::Deserialize<'a>,
|
||||||
|
{
|
||||||
|
type SelfType<'a>
|
||||||
|
= T
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
type AsBytes<'a>
|
||||||
|
= Vec<u8>
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
fn fixed_width() -> Option<usize> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
{
|
||||||
|
bincode::deserialize(data).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
|
||||||
|
where
|
||||||
|
Self: 'a,
|
||||||
|
Self: 'b,
|
||||||
|
{
|
||||||
|
bincode::serialize(value).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_name() -> redb::TypeName {
|
||||||
|
redb::TypeName::new(&format!("Bincode<{}>", std::any::type_name::<T>()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> redb::Key for Bincode<T>
|
||||||
|
where
|
||||||
|
T: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned + Ord,
|
||||||
|
{
|
||||||
|
fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering {
|
||||||
|
Self::from_bytes(data1).cmp(&Self::from_bytes(data2))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,163 @@
|
|||||||
use crate::server::STATE_FILE;
|
use super::db;
|
||||||
|
use crate::models;
|
||||||
|
use redb::Database;
|
||||||
|
use redb::ReadableTable;
|
||||||
|
use redb::ReadableTableMetadata;
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use type_toppings::ResultExt;
|
use type_toppings::ResultExt;
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument(skip_all, err)]
|
||||||
pub async fn run_migrations() {
|
pub async fn run_migrations(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
migrate_state_file().await;
|
migrate_from_ron_to_redb(db).await?;
|
||||||
|
init_at_current_version(db).await?;
|
||||||
|
migrate_to_v2(db).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State file moved to datastore/private
|
/// Use redb DB instead of Ron state file
|
||||||
#[tracing::instrument]
|
#[tracing::instrument(skip_all, err)]
|
||||||
async fn migrate_state_file() {
|
async fn migrate_from_ron_to_redb(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let m = PathBuf::from("state.ron");
|
let ron_state_file_path = PathBuf::from(super::STATE_FILE);
|
||||||
if m.try_exists().expect_or_report_with(|| format!("Failed to read {}", m.display())) {
|
|
||||||
tracing::warn!("MIGRATING STATE FILE");
|
if ron_state_file_path
|
||||||
let p = PathBuf::from(STATE_FILE);
|
.try_exists()
|
||||||
tokio::fs::create_dir_all(p.parent().unwrap()).await.unwrap_or_report();
|
.expect_or_report_with(|| format!("Failed to read {}", ron_state_file_path.display()))
|
||||||
tokio::fs::rename(m, &p).await.unwrap_or_report();
|
{
|
||||||
|
tracing::warn!("MIGRATING");
|
||||||
|
|
||||||
|
let ron_state: models::v1::PersistentState = {
|
||||||
|
let content = tokio::fs::read_to_string(&ron_state_file_path).await?;
|
||||||
|
ron::from_str(&content)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_txn = db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut version_table = write_txn.open_table(db::TABLE_VERSION)?;
|
||||||
|
assert!(version_table.is_empty()?);
|
||||||
|
version_table.insert((), db::Version { version: 1 })?;
|
||||||
|
|
||||||
|
let mut root_table = write_txn.open_table(db::v1::TABLE_ROOT)?;
|
||||||
|
assert!(root_table.is_empty()?);
|
||||||
|
|
||||||
|
let persistent_state = models::v1::PersistentState {
|
||||||
|
version: ron_state.version,
|
||||||
|
wall: ron_state.wall,
|
||||||
|
problems: ron_state.problems,
|
||||||
|
};
|
||||||
|
|
||||||
|
root_table.insert((), persistent_state)?;
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
|
||||||
|
tracing::info!("Removing ron state");
|
||||||
|
tokio::fs::remove_file(ron_state_file_path).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move out, is not really a migration
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
async fn init_at_current_version(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let txn = db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut version_table = txn.open_table(db::TABLE_VERSION)?;
|
||||||
|
let is_missing_version = version_table.get(())?.is_none();
|
||||||
|
if is_missing_version {
|
||||||
|
let v = db::Version::current();
|
||||||
|
tracing::warn!("INITIALIZING DATABASE AT VERSION {v}");
|
||||||
|
version_table.insert((), v)?;
|
||||||
|
|
||||||
|
// Root table
|
||||||
|
{
|
||||||
|
let mut table = txn.open_table(db::current::TABLE_ROOT)?;
|
||||||
|
assert!(table.is_empty()?);
|
||||||
|
table.insert((), models::Root { walls: BTreeSet::new() })?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walls table
|
||||||
|
{
|
||||||
|
// Opening the table creates the table
|
||||||
|
let table = txn.open_table(db::current::TABLE_WALLS)?;
|
||||||
|
assert!(table.is_empty()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Problems table
|
||||||
|
{
|
||||||
|
// Opening the table creates the table
|
||||||
|
let table = txn.open_table(db::current::TABLE_PROBLEMS)?;
|
||||||
|
assert!(table.is_empty()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, err)]
|
||||||
|
async fn migrate_to_v2(db: &Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
use super::db;
|
||||||
|
|
||||||
|
let txn = db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut version_table = txn.open_table(db::TABLE_VERSION)?;
|
||||||
|
let version = version_table.get(())?.unwrap().value().version;
|
||||||
|
if version == 1 {
|
||||||
|
tracing::warn!("MIGRATING");
|
||||||
|
version_table.insert((), db::Version { version: 2 })?;
|
||||||
|
|
||||||
|
let root_table_v1 = txn.open_table(db::v1::TABLE_ROOT)?;
|
||||||
|
let root_v1 = root_table_v1.get(())?.unwrap().value();
|
||||||
|
txn.delete_table(db::v1::TABLE_ROOT)?;
|
||||||
|
|
||||||
|
let models::v1::PersistentState { version: _, wall, problems } = root_v1;
|
||||||
|
|
||||||
|
// we'll reimport them instead of a lossy conversion.
|
||||||
|
drop(problems);
|
||||||
|
|
||||||
|
let mut walls = BTreeSet::new();
|
||||||
|
let wall_uid = models::v2::WallId(uuid::Uuid::new_v4());
|
||||||
|
let holds = wall
|
||||||
|
.holds
|
||||||
|
.into_iter()
|
||||||
|
.map(|(hold_position, hold)| {
|
||||||
|
(
|
||||||
|
models::v1::HoldPosition {
|
||||||
|
row: hold_position.row,
|
||||||
|
col: hold_position.col,
|
||||||
|
},
|
||||||
|
models::v1::Hold {
|
||||||
|
position: models::v1::HoldPosition {
|
||||||
|
row: hold.position.row,
|
||||||
|
col: hold.position.col,
|
||||||
|
},
|
||||||
|
image: hold.image.map(|i| models::v1::Image { filename: i.filename }),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let wall_v2 = models::v2::Wall {
|
||||||
|
uid: wall_uid,
|
||||||
|
rows: wall.rows,
|
||||||
|
cols: wall.cols,
|
||||||
|
holds,
|
||||||
|
problems: BTreeSet::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
walls.insert(wall_v2.uid);
|
||||||
|
let root_v2 = models::v2::Root { walls };
|
||||||
|
|
||||||
|
let mut root_table_v2 = txn.open_table(db::v2::TABLE_ROOT)?;
|
||||||
|
root_table_v2.insert((), root_v2)?;
|
||||||
|
|
||||||
|
let mut walls_table = txn.open_table(db::v2::TABLE_WALLS)?;
|
||||||
|
walls_table.insert(wall_v2.uid, wall_v2)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
//! Server lib module to host re-usable server operations.
|
//! Server lib module to host re-usable server operations.
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
use crate::models::HoldPosition;
|
use crate::models::HoldPosition;
|
||||||
use crate::models::HoldRole;
|
use crate::models::HoldRole;
|
||||||
use crate::models::Problem;
|
|
||||||
use crate::server::config::Config;
|
use crate::server::config::Config;
|
||||||
use crate::server::persistence;
|
use crate::server::db;
|
||||||
use crate::server::state::State;
|
use redb::Database;
|
||||||
|
use redb::ReadableTable;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub(crate) async fn import_mini_moonboard_problems(config: &Config, db: Arc<Database>, wall_id: models::WallId) -> Result<(), Error> {
|
||||||
|
use moonboard_parser::mini_moonboard;
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
|
||||||
pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &State) -> Result<(), Error> {
|
|
||||||
let mut problems = Vec::new();
|
let mut problems = Vec::new();
|
||||||
|
|
||||||
let file_name = "problems Mini MoonBoard 2020 40.json";
|
let file_name = "problems Mini MoonBoard 2020 40.json";
|
||||||
@@ -17,10 +21,12 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat
|
|||||||
|
|
||||||
tracing::info!("Parsing mini moonboard problems from {file_path}");
|
tracing::info!("Parsing mini moonboard problems from {file_path}");
|
||||||
|
|
||||||
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path.as_std_path()).await?;
|
let set_by = "mini-mb-2020-parser";
|
||||||
for problem in mini_moonboard.problems {
|
|
||||||
|
let mini_moonboard = mini_moonboard::parse(file_path.as_std_path()).await?;
|
||||||
|
for mini_mb_problem in mini_moonboard.problems {
|
||||||
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
||||||
for mv in problem.moves {
|
for mv in mini_mb_problem.moves {
|
||||||
let row = mv.description.row();
|
let row = mv.description.row();
|
||||||
let col = mv.description.column();
|
let col = mv.description.column();
|
||||||
let hold_position = HoldPosition { row, col };
|
let hold_position = HoldPosition { row, col };
|
||||||
@@ -33,16 +39,48 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat
|
|||||||
};
|
};
|
||||||
holds.insert(hold_position, role);
|
holds.insert(hold_position, role);
|
||||||
}
|
}
|
||||||
let route = Problem { holds };
|
|
||||||
problems.push(route);
|
let name = mini_mb_problem.name;
|
||||||
|
|
||||||
|
let method = match mini_mb_problem.method {
|
||||||
|
mini_moonboard::Method::FeetFollowHands => models::Method::FeetFollowHands,
|
||||||
|
mini_moonboard::Method::Footless => models::Method::Footless,
|
||||||
|
mini_moonboard::Method::FootlessPlusKickboard => models::Method::FootlessPlusKickboard,
|
||||||
|
};
|
||||||
|
|
||||||
|
let problem_id = models::ProblemId::new();
|
||||||
|
|
||||||
|
let problem = models::Problem {
|
||||||
|
uid: problem_id,
|
||||||
|
name,
|
||||||
|
set_by: set_by.to_owned(),
|
||||||
|
holds,
|
||||||
|
method,
|
||||||
|
date_added: chrono::Utc::now(),
|
||||||
|
};
|
||||||
|
problems.push(problem);
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
tokio::task::spawn_blocking(move || -> Result<(), redb::Error> {
|
||||||
.persistent
|
let write_txn = db.begin_write()?;
|
||||||
.update(|s| {
|
{
|
||||||
s.problems.problems.extend(problems);
|
let mut walls_table = write_txn.open_table(db::current::TABLE_WALLS)?;
|
||||||
})
|
let mut problems_table = write_txn.open_table(db::current::TABLE_PROBLEMS)?;
|
||||||
.await?;
|
|
||||||
|
let mut wall = walls_table.get(wall_id)?.unwrap().value();
|
||||||
|
wall.problems.extend(problems.iter().map(|p| p.uid));
|
||||||
|
walls_table.insert(wall_id, wall)?;
|
||||||
|
|
||||||
|
for problem in problems {
|
||||||
|
let key = (wall_id, problem.uid);
|
||||||
|
problems_table.insert(key, problem)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -50,5 +88,6 @@ pub(crate) async fn import_mini_moonboard_problems(config: &Config, state: &Stat
|
|||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Parser(moonboard_parser::Error),
|
Parser(moonboard_parser::Error),
|
||||||
Persistence(persistence::Error),
|
Redb(redb::Error),
|
||||||
|
Tokio(tokio::task::JoinError),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
use serde::Serialize;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Persistent<T> {
|
|
||||||
state: Arc<Mutex<T>>,
|
|
||||||
file_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Persistent<T> {
|
|
||||||
#[tracing::instrument(skip(state))]
|
|
||||||
pub fn new(state: T, file_path: PathBuf) -> Self {
|
|
||||||
Self {
|
|
||||||
state: Arc::new(Mutex::new(state)),
|
|
||||||
file_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Instantiates state from file system
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub async fn load(file_path: &Path) -> Result<Self, Error>
|
|
||||||
where
|
|
||||||
T: DeserializeOwned,
|
|
||||||
{
|
|
||||||
let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read {
|
|
||||||
file_path: file_path.to_owned(),
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let t = ron::from_str(&content).map_err(|source| Error::Deserialize {
|
|
||||||
file_path: file_path.to_owned(),
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let persistent = Self {
|
|
||||||
state: Arc::new(Mutex::new(t)),
|
|
||||||
file_path: file_path.to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(persistent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This is pretty poor - clones the entire state on access
|
|
||||||
/// Returns state
|
|
||||||
#[tracing::instrument(skip(self))]
|
|
||||||
pub async fn get(&self) -> T
|
|
||||||
where
|
|
||||||
T: Clone,
|
|
||||||
{
|
|
||||||
let state = self.state.lock().await;
|
|
||||||
state.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns state passed through given function
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn with<F, R>(&self, f: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&T) -> R,
|
|
||||||
{
|
|
||||||
let state = self.state.lock().await;
|
|
||||||
f(&state)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates and persists state
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn update<F>(&self, f: F) -> Result<(), Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut T),
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
let mut state = self.state.lock().await;
|
|
||||||
f(&mut state);
|
|
||||||
Self::persist(&self.file_path, &state).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets and persists state
|
|
||||||
#[tracing::instrument(skip_all)]
|
|
||||||
pub async fn set(&self, new_state: T) -> Result<(), Error>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
self.update(move |state| {
|
|
||||||
*state = new_state;
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persist.
|
|
||||||
///
|
|
||||||
/// Implicitly called by `set` and `update`.
|
|
||||||
#[tracing::instrument(skip_all, err)]
|
|
||||||
pub async fn persist(file_path: &Path, state: &T) -> Result<(), Error>
|
|
||||||
where
|
|
||||||
T: Serialize,
|
|
||||||
{
|
|
||||||
tracing::debug!("Persisting state");
|
|
||||||
let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?;
|
|
||||||
if let Some(parent) = file_path.parent() {
|
|
||||||
tokio::fs::create_dir_all(parent).await.map_err(|source| Error::CreateDir {
|
|
||||||
source,
|
|
||||||
dir: parent.to_owned(),
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
|
|
||||||
file_path: file_path.to_owned(),
|
|
||||||
source,
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
|
||||||
#[display("Persistent state error: {_variant}")]
|
|
||||||
pub enum Error {
|
|
||||||
#[display("Failed to read file: {}", file_path.display())]
|
|
||||||
Read { file_path: PathBuf, source: std::io::Error },
|
|
||||||
|
|
||||||
#[display("Failed to deserialize state from file: {}", file_path.display())]
|
|
||||||
Deserialize { file_path: PathBuf, source: ron::error::SpannedError },
|
|
||||||
|
|
||||||
#[display("Failed to serialize state")]
|
|
||||||
Serialize { source: ron::Error },
|
|
||||||
|
|
||||||
#[display("Failed to write file: {}", file_path.display())]
|
|
||||||
Write { file_path: PathBuf, source: std::io::Error },
|
|
||||||
|
|
||||||
#[display("Failed to create directory: {}", dir.display())]
|
|
||||||
CreateDir { dir: PathBuf, source: std::io::Error },
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
//! Server state
|
|
||||||
|
|
||||||
const STATE_VERSION: u64 = 1;
|
|
||||||
|
|
||||||
use super::persistence::Persistent;
|
|
||||||
use crate::models;
|
|
||||||
use crate::models::Wall;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use smart_default::SmartDefault;
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct State {
|
|
||||||
pub persistent: Persistent<PersistentState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, SmartDefault)]
|
|
||||||
pub struct PersistentState {
|
|
||||||
/// State schema version
|
|
||||||
#[default(STATE_VERSION)]
|
|
||||||
pub version: u64,
|
|
||||||
|
|
||||||
pub wall: Wall,
|
|
||||||
pub problems: Problems,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
|
||||||
pub struct Problems {
|
|
||||||
pub problems: BTreeSet<models::Problem>,
|
|
||||||
}
|
|
||||||
11
todo.md
Normal file
11
todo.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
- save images with a uuid
|
||||||
|
- downscale images
|
||||||
|
- associate routes with wall
|
||||||
|
- group routes by pattern (pattern family has shift/mirror variations)
|
||||||
|
- generate pattern families of variations when importing problems
|
||||||
|
- implement pattern challenge (start an "adventure mode" based on a pattern family)
|
||||||
|
- Record problem success (enum: flash, send, no-send)
|
||||||
|
- implement routes page to show all routes for a given wall
|
||||||
|
- implement favorite routes feature
|
||||||
|
- use wall id in URL.
|
||||||
|
- decide on routes vs problems terminology
|
||||||
Reference in New Issue
Block a user