ron codec
This commit is contained in:
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -107,6 +107,8 @@ dependencies = [
|
|||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
|
"moonboard-parser",
|
||||||
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -247,6 +249,12 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -258,6 +266,9 @@ name = "bitflags"
|
|||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
@@ -1053,7 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a8a90c679094979aa12927e8e925fe8eead1420d69420b2d8c6540863937ca75"
|
checksum = "a8a90c679094979aa12927e8e925fe8eead1420d69420b2d8c6540863937ca75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"either_of",
|
"either_of",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -1253,7 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0fb23bd110ac04c7276aae3d8ba523f94cf06989d00b4e76eaee89451b06b494"
|
checksum = "0fb23bd110ac04c7276aae3d8ba523f94cf06989d00b4e76eaee89451b06b494"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"any_spawner",
|
"any_spawner",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"codee",
|
"codee",
|
||||||
"futures",
|
"futures",
|
||||||
"hydration_context",
|
"hydration_context",
|
||||||
@@ -1391,11 +1402,11 @@ dependencies = [
|
|||||||
name = "moonboard-parser"
|
name = "moonboard-parser"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"camino",
|
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"type-toppings",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1828,6 +1839,18 @@ version = "0.8.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ron"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bitflags",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rstml"
|
name = "rstml"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ members = ["crates/ascend", "crates/moonboard-parser"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Asger Juul Brunshøj <asgerbrunshoj@gmail.com>"]
|
authors = ["Asger Juul Brunshøj <asgerbrunshoj@gmail.com>"]
|
||||||
|
|
||||||
|
[workspace.dependencies.moonboard-parser]
|
||||||
|
path = "crates/moonboard-parser"
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
future_incompatible = { level = "deny", priority = -1 }
|
future_incompatible = { level = "deny", priority = -1 }
|
||||||
nonstandard_style = { level = "deny", priority = -1 }
|
nonstandard_style = { level = "deny", priority = -1 }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
leptos = { version = "0.7.3" }
|
leptos = { version = "0.7.3" }
|
||||||
@@ -23,16 +24,17 @@ http = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
derive_more = { version = "1", features = ["display", "error", "from"] }
|
derive_more = { version = "1", features = ["display", "error", "from"] }
|
||||||
clap = { version = "4.5.7", features = ["derive"] }
|
clap = { version = "4.5.7", features = ["derive"] }
|
||||||
serde_json = { version = "1", optional = true }
|
|
||||||
camino = { version = "1.1", optional = true }
|
camino = { version = "1.1", optional = true }
|
||||||
type-toppings = { version = "0.2.1", features = ["result"] }
|
type-toppings = { version = "0.2.1", features = ["result"] }
|
||||||
|
|
||||||
# Tracing
|
# Tracing
|
||||||
tracing = { version = "0.1", optional = true }
|
tracing = { version = "0.1", optional = true }
|
||||||
tracing-subscriber = { version = "0.3.18", features = [
|
tracing-subscriber = { version = "0.3.18", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
|
ron = { version = "0.8" }
|
||||||
|
|
||||||
|
[dev-dependencies.serde_json]
|
||||||
|
version = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = ["leptos/hydrate"]
|
hydrate = ["leptos/hydrate"]
|
||||||
@@ -43,7 +45,7 @@ ssr = [
|
|||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:camino",
|
"dep:camino",
|
||||||
"dep:serde_json",
|
"dep:moonboard-parser",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[leptos::component]
|
#[component]
|
||||||
pub fn App() -> impl leptos::IntoView {
|
pub fn App() -> impl leptos::IntoView {
|
||||||
use leptos_meta::Stylesheet;
|
use leptos_meta::Stylesheet;
|
||||||
use leptos_meta::Title;
|
use leptos_meta::Title;
|
||||||
|
|
||||||
|
// TODO: look at tracing-subscriber-wasm
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
tracing::debug!("Rendering root component");
|
tracing::debug!("Rendering root component");
|
||||||
|
|
||||||
@@ -43,7 +44,9 @@ pub fn App() -> impl leptos::IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[leptos::component]
|
#[component]
|
||||||
fn Ascend() -> impl leptos::IntoView {
|
fn Ascend() -> impl leptos::IntoView {
|
||||||
leptos::view! { <crate::pages::wall::Wall /> }
|
use crate::pages::wall::Wall;
|
||||||
|
|
||||||
|
leptos::view! { <Wall /> }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ pub mod components {}
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod server;
|
pub mod server;
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn hydrate() {
|
pub fn hydrate() {
|
||||||
|
|||||||
35
crates/ascend/src/models.rs
Normal file
35
crates/ascend/src/models.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//! Shared models between server and client code.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -1,21 +1,128 @@
|
|||||||
|
use crate::models;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use ron_codec::RonCodec;
|
||||||
|
|
||||||
#[leptos::component]
|
#[component]
|
||||||
pub fn Wall() -> impl leptos::IntoView {
|
pub fn Wall() -> impl leptos::IntoView {
|
||||||
|
// TODO: What to do about this unwrap?
|
||||||
|
// let random_problem = OnceResource::new(async move { get_random_problem().await.unwrap() });
|
||||||
|
let random_problem = async move { get_random_problem().await.unwrap() };
|
||||||
|
|
||||||
|
// TODO: No hardcoding wall dimensions
|
||||||
let cells = (1..=(12 * 12))
|
let cells = (1..=(12 * 12))
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let i = i.to_string();
|
let i = i.to_string();
|
||||||
view! {
|
view! { <Cell label=i/> }
|
||||||
<div class="aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100">
|
|
||||||
{i}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect_view();
|
.collect_view();
|
||||||
|
|
||||||
leptos::view! {
|
leptos::view! {
|
||||||
|
<Await future=random_problem let:foo>
|
||||||
|
<p>"Got a problem!"</p>
|
||||||
|
</Await>
|
||||||
|
|
||||||
<div class="container mx-auto border">
|
<div class="container mx-auto border">
|
||||||
<div class="grid grid-rows-4 grid-cols-12 gap-4">{cells}</div>
|
<div class="grid grid-rows-4 grid-cols-12 gap-4">{cells}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Cell(label: String) -> impl leptos::IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_random_problem() -> Result<RonCodec<models::Problem>, ServerFnError> {
|
||||||
|
use crate::server::state::State;
|
||||||
|
|
||||||
|
// TODO: Actually randomize
|
||||||
|
|
||||||
|
let state = expect_context::<State>();
|
||||||
|
let persistent_state = state.persistent.get().await;
|
||||||
|
let problem = persistent_state.wall.problems.iter().next().unwrap();
|
||||||
|
Ok(RonCodec::new(problem.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
mod ron_codec {
|
||||||
|
//! Wrap T in RonCodec<T> that when serialized, always serializes to a [ron] string.
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RonCodec<T> {
|
||||||
|
t: T,
|
||||||
|
}
|
||||||
|
impl<T> RonCodec<T> {
|
||||||
|
pub fn into_inner(self) -> T {
|
||||||
|
self.t
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(t: T) -> Self {
|
||||||
|
Self { t }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> serde::Serialize for RonCodec<T>
|
||||||
|
where
|
||||||
|
T: serde::Serialize,
|
||||||
|
{
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let serialized = ron::to_string(&self.t).map_err(serde::ser::Error::custom)?;
|
||||||
|
serializer.serialize_str(&serialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de, T> serde::Deserialize<'de> for RonCodec<T>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
let t: T = ron::from_str(&s).map_err(serde::de::Error::custom)?;
|
||||||
|
Ok(Self { t })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::RonCodec;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||||
|
struct TestStruct {
|
||||||
|
name: String,
|
||||||
|
value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ron_codec() {
|
||||||
|
let original = TestStruct {
|
||||||
|
name: "Test".to_string(),
|
||||||
|
value: 42,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrap in RonCodec
|
||||||
|
let wrapped = RonCodec::new(original.clone());
|
||||||
|
|
||||||
|
// Serialize
|
||||||
|
let serialized = serde_json::to_string(&wrapped).expect("Serialization failed");
|
||||||
|
println!("Serialized: {}", serialized);
|
||||||
|
|
||||||
|
// Deserialize
|
||||||
|
let deserialized: RonCodec<TestStruct> = serde_json::from_str(&serialized).expect("Deserialization failed");
|
||||||
|
|
||||||
|
// Compare
|
||||||
|
assert_eq!(deserialized.into_inner(), original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
//! Server-only features
|
//! Server-only features
|
||||||
|
|
||||||
|
use crate::models;
|
||||||
|
use models::HoldPosition;
|
||||||
|
use models::HoldRole;
|
||||||
|
use models::Problem;
|
||||||
use persistence::Persistent;
|
use persistence::Persistent;
|
||||||
use state::PersistentState;
|
use state::PersistentState;
|
||||||
|
use state::State;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::Path;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
use type_toppings::ResultExt;
|
use type_toppings::ResultExt;
|
||||||
|
|
||||||
pub mod cli {
|
pub mod cli {
|
||||||
//! Server CLI interface
|
//! Server CLI interface
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(clap::Parser)]
|
#[derive(clap::Parser)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
@@ -22,6 +31,10 @@ pub mod cli {
|
|||||||
|
|
||||||
/// Resets state, replacing it with defaults
|
/// Resets state, replacing it with defaults
|
||||||
ResetState,
|
ResetState,
|
||||||
|
|
||||||
|
ImportMiniMoonboardProblems {
|
||||||
|
file_path: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +42,7 @@ pub mod state {
|
|||||||
//! Server state
|
//! Server state
|
||||||
|
|
||||||
use super::persistence::Persistent;
|
use super::persistence::Persistent;
|
||||||
|
use crate::models;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@@ -46,59 +60,25 @@ pub mod state {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
pub struct Wall {
|
pub struct Wall {
|
||||||
pub rows: u32,
|
pub rows: u64,
|
||||||
pub cols: u32,
|
pub cols: u64,
|
||||||
pub holds: BTreeMap<HoldPosition, Hold>,
|
pub holds: BTreeMap<models::HoldPosition, Hold>,
|
||||||
pub routes: BTreeSet<route::Route>,
|
pub problems: BTreeSet<models::Problem>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct HoldPosition {
|
|
||||||
pub row: u32,
|
|
||||||
pub col: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
pub struct Hold {
|
pub struct Hold {
|
||||||
pub position: HoldPosition,
|
pub position: models::HoldPosition,
|
||||||
pub image: Option<Image>,
|
pub image: Option<Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
pub struct Image {}
|
pub struct Image {}
|
||||||
|
|
||||||
mod route {
|
|
||||||
use super::HoldPosition;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct Route {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod persistence;
|
pub mod persistence;
|
||||||
|
|
||||||
pub const STATE_FILE: &str = "state.json";
|
pub const STATE_FILE: &str = "state.ron";
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
@@ -120,6 +100,17 @@ pub async fn main() {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Serve => serve().await.unwrap_or_report(),
|
Command::Serve => serve().await.unwrap_or_report(),
|
||||||
|
Command::ImportMiniMoonboardProblems { file_path } => {
|
||||||
|
let problems = import_mini_moonboard_problems(&file_path).await.unwrap_or_report();
|
||||||
|
let state = load_state().await.unwrap_or_report();
|
||||||
|
state
|
||||||
|
.persistent
|
||||||
|
.update(|s| {
|
||||||
|
s.wall.problems.extend(problems);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_report();
|
||||||
|
}
|
||||||
Command::ResetState => {
|
Command::ResetState => {
|
||||||
let s = PersistentState::default();
|
let s = PersistentState::default();
|
||||||
let p = camino::Utf8Path::new(STATE_FILE);
|
let p = camino::Utf8Path::new(STATE_FILE);
|
||||||
@@ -133,7 +124,6 @@ pub async fn main() {
|
|||||||
async fn serve() -> Result<(), Error> {
|
async fn serve() -> Result<(), Error> {
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::app::shell;
|
use crate::app::shell;
|
||||||
use crate::server::state::State;
|
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_axum::LeptosRoutes;
|
use leptos_axum::LeptosRoutes;
|
||||||
@@ -149,10 +139,7 @@ async fn serve() -> Result<(), Error> {
|
|||||||
let addr = leptos_options.site_addr;
|
let addr = leptos_options.site_addr;
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
|
||||||
tracing::info!("Loading state");
|
let server_state = load_state().await?;
|
||||||
let server_state = State {
|
|
||||||
persistent: Persistent::<PersistentState>::load(STATE_FILE.into()).await?,
|
|
||||||
};
|
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -170,9 +157,46 @@ async fn serve() -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn load_state() -> Result<State, Error> {
|
||||||
|
tracing::info!("Loading state");
|
||||||
|
let state = State {
|
||||||
|
persistent: Persistent::<PersistentState>::load(STATE_FILE.into()).await?,
|
||||||
|
};
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
async fn import_mini_moonboard_problems(file_path: &Path) -> Result<Vec<Problem>, Error> {
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
|
||||||
|
tracing::info!("Parsing mini moonboard problems from {}", file_path.display());
|
||||||
|
let mini_moonboard = moonboard_parser::mini_moonboard::parse(file_path).await?;
|
||||||
|
for problem in mini_moonboard.problems {
|
||||||
|
let mut holds = BTreeMap::<HoldPosition, HoldRole>::new();
|
||||||
|
for mv in problem.moves {
|
||||||
|
let row = mv.description.row();
|
||||||
|
let col = mv.description.column();
|
||||||
|
let hold_position = HoldPosition { row, col };
|
||||||
|
|
||||||
|
let role = match (mv.is_start, mv.is_end) {
|
||||||
|
(true, true) => unreachable!(),
|
||||||
|
(true, false) => HoldRole::Start,
|
||||||
|
(false, true) => HoldRole::End,
|
||||||
|
(false, false) => HoldRole::Normal,
|
||||||
|
};
|
||||||
|
holds.insert(hold_position, role);
|
||||||
|
}
|
||||||
|
let route = Problem { holds };
|
||||||
|
problems.push(route);
|
||||||
|
}
|
||||||
|
Ok(problems)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
#[derive(Debug, derive_more::Error, derive_more::Display, derive_more::From)]
|
||||||
#[display("Server crash")]
|
#[display("Server crash")]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(std::io::Error),
|
Io(std::io::Error),
|
||||||
Persistence(persistence::Error),
|
Persistence(persistence::Error),
|
||||||
|
Parser(moonboard_parser::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ impl<T> Persistent<T> {
|
|||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let t = serde_json::from_str(&content).map_err(|source| Error::Deserialize {
|
let t = ron::from_str(&content).map_err(|source| Error::Deserialize {
|
||||||
file_path: file_path.to_owned(),
|
file_path: file_path.to_owned(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
@@ -43,6 +43,7 @@ impl<T> Persistent<T> {
|
|||||||
Ok(persistent)
|
Ok(persistent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This is pretty poor - clones the entire state on access
|
||||||
/// Returns state
|
/// Returns state
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn get(&self) -> T
|
pub async fn get(&self) -> T
|
||||||
@@ -53,7 +54,7 @@ impl<T> Persistent<T> {
|
|||||||
state.clone()
|
state.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns state
|
/// Returns state passed through given function
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn with<F, R>(&self, f: F) -> R
|
pub async fn with<F, R>(&self, f: F) -> R
|
||||||
where
|
where
|
||||||
@@ -96,7 +97,8 @@ impl<T> Persistent<T> {
|
|||||||
where
|
where
|
||||||
T: Serialize,
|
T: Serialize,
|
||||||
{
|
{
|
||||||
let serialized = serde_json::to_string(state).map_err(|source| Error::Serialize { source })?;
|
tracing::debug!("Persisting state");
|
||||||
|
let serialized = ron::to_string(state).map_err(|source| Error::Serialize { source })?;
|
||||||
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
|
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
|
||||||
file_path: file_path.to_owned(),
|
file_path: file_path.to_owned(),
|
||||||
source,
|
source,
|
||||||
@@ -112,10 +114,13 @@ pub enum Error {
|
|||||||
Read { file_path: Utf8PathBuf, source: std::io::Error },
|
Read { file_path: Utf8PathBuf, source: std::io::Error },
|
||||||
|
|
||||||
#[display("Failed to deserialize state from file: {file_path}")]
|
#[display("Failed to deserialize state from file: {file_path}")]
|
||||||
Deserialize { file_path: Utf8PathBuf, source: serde_json::Error },
|
Deserialize {
|
||||||
|
file_path: Utf8PathBuf,
|
||||||
|
source: ron::error::SpannedError,
|
||||||
|
},
|
||||||
|
|
||||||
#[display("Failed to serialize state")]
|
#[display("Failed to serialize state")]
|
||||||
Serialize { source: serde_json::Error },
|
Serialize { source: ron::Error },
|
||||||
|
|
||||||
#[display("Failed to write file: {file_path}")]
|
#[display("Failed to write file: {file_path}")]
|
||||||
Write { file_path: Utf8PathBuf, source: std::io::Error },
|
Write { file_path: Utf8PathBuf, source: std::io::Error },
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ authors.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies.camino]
|
|
||||||
version = "1"
|
|
||||||
|
|
||||||
[dependencies.derive_more]
|
[dependencies.derive_more]
|
||||||
version = "1"
|
version = "1"
|
||||||
@@ -25,3 +23,7 @@ features = ["fs"]
|
|||||||
[dev-dependencies.tokio]
|
[dev-dependencies.tokio]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = ["rt-multi-thread", "macros"]
|
features = ["rt-multi-thread", "macros"]
|
||||||
|
|
||||||
|
[dev-dependencies.type-toppings]
|
||||||
|
version = "0.2.1"
|
||||||
|
features = ["result"]
|
||||||
|
|||||||
@@ -1,32 +1,149 @@
|
|||||||
use camino::Utf8PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub mod mini_moonboard {
|
pub mod mini_moonboard {
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use camino::Utf8PathBuf;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
pub const MINI_MOONBOARD_PROBLEMS: &str = "moonboard-problems/problems Mini MoonBoard 2020 40.json";
|
pub const MINI_MOONBOARD_PROBLEMS: &str = "../../moonboard-problems/problems Mini MoonBoard 2020 40.json";
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct MiniMoonboardProblems {
|
pub struct MiniMoonboard {
|
||||||
total: u64,
|
pub total: u64,
|
||||||
data: Vec<Data>,
|
|
||||||
|
#[serde(rename = "data")]
|
||||||
|
pub problems: Vec<Problem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Problem {
|
||||||
|
/// Name of the problem
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
pub grade: String,
|
||||||
|
pub user_grade: Option<String>,
|
||||||
|
pub setby_id: String,
|
||||||
|
|
||||||
|
/// Name of the route setter
|
||||||
|
pub setby: String,
|
||||||
|
|
||||||
|
pub method: Method,
|
||||||
|
|
||||||
|
pub user_rating: u64,
|
||||||
|
pub repeats: u64,
|
||||||
|
|
||||||
|
pub is_benchmark: bool,
|
||||||
|
pub is_master: bool,
|
||||||
|
pub upgraded: bool,
|
||||||
|
pub downgraded: bool,
|
||||||
|
pub moves: Vec<Move>,
|
||||||
|
pub holdsets: Vec<HoldSet>,
|
||||||
|
|
||||||
|
pub has_beta_video: bool,
|
||||||
|
pub moon_board_configuration_id: u64,
|
||||||
|
pub api_id: u64,
|
||||||
|
pub date_inserted: String,
|
||||||
|
pub date_updated: Option<String>,
|
||||||
|
pub date_deleted: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Data {}
|
pub enum Method {
|
||||||
|
#[serde(rename = "Feet follow hands")]
|
||||||
|
FeetFollowHands,
|
||||||
|
|
||||||
pub async fn parse() -> Result<MiniMoonboardProblems, Error> {
|
#[serde(rename = "Footless")]
|
||||||
let file_path = Utf8PathBuf::from(MINI_MOONBOARD_PROBLEMS);
|
Footless,
|
||||||
|
|
||||||
let content = tokio::fs::read_to_string(&file_path).await.map_err(|source| Error::Read {
|
#[serde(rename = "Footless + kickboard")]
|
||||||
|
FootlessPlusKickboard,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Move {
|
||||||
|
pub problem_id: u64,
|
||||||
|
pub description: MoveDescription,
|
||||||
|
pub is_start: bool,
|
||||||
|
pub is_end: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HoldSet {
|
||||||
|
pub description: String,
|
||||||
|
pub locations: Option<()>,
|
||||||
|
pub api_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||||
|
pub enum MoveDescription {
|
||||||
|
A12, B12, C12, D12, E12, F12, G12, H12, I12, J12, K12,
|
||||||
|
A11, B11, C11, D11, E11, F11, G11, H11, I11, J11, K11,
|
||||||
|
A10, B10, C10, D10, E10, F10, G10, H10, I10, J10, K10,
|
||||||
|
A9, B9, C9, D9, E9, F9, G9, H9, I9, J9, K9,
|
||||||
|
A8, B8, C8, D8, E8, F8, G8, H8, I8, J8, K8,
|
||||||
|
A7, B7, C7, D7, E7, F7, G7, H7, I7, J7, K7,
|
||||||
|
A6, B6, C6, D6, E6, F6, G6, H6, I6, J6, K6,
|
||||||
|
A5, B5, C5, D5, E5, F5, G5, H5, I5, J5, K5,
|
||||||
|
A4, B4, C4, D4, E4, F4, G4, H4, I4, J4, K4,
|
||||||
|
A3, B3, C3, D3, E3, F3, G3, H3, I3, J3, K3,
|
||||||
|
A2, B2, C2, D2, E2, F2, G2, H2, I2, J2, K2,
|
||||||
|
A1, B1, C1, D1, E1, F1, G1, H1, I1, J1, K1,
|
||||||
|
}
|
||||||
|
impl MoveDescription {
|
||||||
|
/// Convert to 0-indexed row (starting from top to bottom)
|
||||||
|
pub fn row(&self) -> u64 {
|
||||||
|
use MoveDescription::*;
|
||||||
|
match self {
|
||||||
|
A12 | B12 | C12 | D12 | E12 | F12 | G12 | H12 | I12 | J12 | K12 => 0,
|
||||||
|
A11 | B11 | C11 | D11 | E11 | F11 | G11 | H11 | I11 | J11 | K11 => 1,
|
||||||
|
A10 | B10 | C10 | D10 | E10 | F10 | G10 | H10 | I10 | J10 | K10 => 2,
|
||||||
|
A9 | B9 | C9 | D9 | E9 | F9 | G9 | H9 | I9 | J9 | K9 => 3,
|
||||||
|
A8 | B8 | C8 | D8 | E8 | F8 | G8 | H8 | I8 | J8 | K8 => 4,
|
||||||
|
A7 | B7 | C7 | D7 | E7 | F7 | G7 | H7 | I7 | J7 | K7 => 5,
|
||||||
|
A6 | B6 | C6 | D6 | E6 | F6 | G6 | H6 | I6 | J6 | K6 => 6,
|
||||||
|
A5 | B5 | C5 | D5 | E5 | F5 | G5 | H5 | I5 | J5 | K5 => 7,
|
||||||
|
A4 | B4 | C4 | D4 | E4 | F4 | G4 | H4 | I4 | J4 | K4 => 8,
|
||||||
|
A3 | B3 | C3 | D3 | E3 | F3 | G3 | H3 | I3 | J3 | K3 => 9,
|
||||||
|
A2 | B2 | C2 | D2 | E2 | F2 | G2 | H2 | I2 | J2 | K2 => 10,
|
||||||
|
A1 | B1 | C1 | D1 | E1 | F1 | G1 | H1 | I1 | J1 | K1 => 11,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert to 0-indexed column (starting from left to right)
|
||||||
|
pub fn column(&self) -> u64 {
|
||||||
|
use MoveDescription::*;
|
||||||
|
match self {
|
||||||
|
A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 => 0,
|
||||||
|
B1 | B2 | B3 | B4 | B5 | B6 | B7 | B8 | B9 | B10 | B11 | B12 => 1,
|
||||||
|
C1 | C2 | C3 | C4 | C5 | C6 | C7 | C8 | C9 | C10 | C11 | C12 => 2,
|
||||||
|
D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9 | D10 | D11 | D12 => 3,
|
||||||
|
E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10 | E11 | E12 => 4,
|
||||||
|
F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 => 5,
|
||||||
|
G1 | G2 | G3 | G4 | G5 | G6 | G7 | G8 | G9 | G10 | G11 | G12 => 6,
|
||||||
|
H1 | H2 | H3 | H4 | H5 | H6 | H7 | H8 | H9 | H10 | H11 | H12 => 7,
|
||||||
|
I1 | I2 | I3 | I4 | I5 | I6 | I7 | I8 | I9 | I10 | I11 | I12 => 8,
|
||||||
|
J1 | J2 | J3 | J4 | J5 | J6 | J7 | J8 | J9 | J10 | J11 | J12 => 9,
|
||||||
|
K1 | K2 | K3 | K4 | K5 | K6 | K7 | K8 | K9 | K10 | K11 | K12 => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn parse(file_path: &Path) -> Result<MiniMoonboard, Error> {
|
||||||
|
let content = tokio::fs::read_to_string(file_path).await.map_err(|source| Error::Read {
|
||||||
file_path: file_path.to_owned(),
|
file_path: file_path.to_owned(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let t: MiniMoonboardProblems = serde_json::from_str(&content).map_err(|source| Error::Deserialize {
|
let t: MiniMoonboard = serde_json::from_str(&content).map_err(|source| Error::Deserialize {
|
||||||
file_path: file_path.to_owned(),
|
file_path: file_path.to_owned(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
@@ -36,22 +153,25 @@ pub mod mini_moonboard {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_parse() {
|
async fn test_parse() {
|
||||||
parse().await.unwrap();
|
use std::path::PathBuf;
|
||||||
|
use type_toppings::ResultExt as _;
|
||||||
|
let file_path = PathBuf::from(MINI_MOONBOARD_PROBLEMS);
|
||||||
|
let mini_moonboard = parse(&file_path).await.unwrap_or_report();
|
||||||
|
assert_eq!(mini_moonboard.total, mini_moonboard.problems.len() as u64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
#[derive(Debug, derive_more::Error, derive_more::Display)]
|
||||||
#[display("Persistent state error: {_variant}")]
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[display("Failed to read file: {file_path}")]
|
#[display("Failed to read file: {}", file_path.display())]
|
||||||
Read { file_path: Utf8PathBuf, source: std::io::Error },
|
Read { file_path: PathBuf, source: std::io::Error },
|
||||||
|
|
||||||
#[display("Failed to deserialize state from file: {file_path}")]
|
#[display("Failed to deserialize file: {}", file_path.display())]
|
||||||
Deserialize { file_path: Utf8PathBuf, source: serde_json::Error },
|
Deserialize { file_path: PathBuf, source: serde_json::Error },
|
||||||
|
|
||||||
#[display("Failed to serialize state")]
|
#[display("Failed to serialize")]
|
||||||
Serialize { source: serde_json::Error },
|
Serialize { source: serde_json::Error },
|
||||||
|
|
||||||
#[display("Failed to write file: {file_path}")]
|
#[display("Failed to write file: {}", file_path.display())]
|
||||||
Write { file_path: Utf8PathBuf, source: std::io::Error },
|
Write { file_path: PathBuf, source: std::io::Error },
|
||||||
}
|
}
|
||||||
|
|||||||
3
justfile
3
justfile
@@ -24,7 +24,8 @@ run-release:
|
|||||||
LEPTOS_SITE_ROOT="site" LEPTOS_SITE_ADDR="127.0.0.1:1337" ./ascend serve
|
LEPTOS_SITE_ROOT="site" LEPTOS_SITE_ADDR="127.0.0.1:1337" ./ascend serve
|
||||||
|
|
||||||
reset-state:
|
reset-state:
|
||||||
cargo leptos serve -- reset-state
|
cargo run --features ssr -- reset-state
|
||||||
|
cargo run --features ssr -- import-mini-moonboard-problems "moonboard-problems/problems Mini MoonBoard 2020 40.json"
|
||||||
|
|
||||||
# Open firewall port for development
|
# Open firewall port for development
|
||||||
open-firewall:
|
open-firewall:
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"wall":{"rows":0,"cols":0,"holds":{},"routes":[]}}
|
|
||||||
Reference in New Issue
Block a user