ron codec
This commit is contained in:
@@ -9,6 +9,7 @@ publish = false
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
moonboard-parser = { workspace = true, optional = true }
|
||||
axum = { version = "0.7", optional = true }
|
||||
console_error_panic_hook = "0.1"
|
||||
leptos = { version = "0.7.3" }
|
||||
@@ -23,16 +24,17 @@ http = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
derive_more = { version = "1", features = ["display", "error", "from"] }
|
||||
clap = { version = "4.5.7", features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
camino = { version = "1.1", optional = true }
|
||||
type-toppings = { version = "0.2.1", features = ["result"] }
|
||||
|
||||
# Tracing
|
||||
tracing = { version = "0.1", optional = true }
|
||||
tracing-subscriber = { version = "0.3.18", features = [
|
||||
"env-filter",
|
||||
], optional = true }
|
||||
ron = { version = "0.8" }
|
||||
|
||||
[dev-dependencies.serde_json]
|
||||
version = "1"
|
||||
|
||||
[features]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
@@ -43,7 +45,7 @@ ssr = [
|
||||
"dep:tower-http",
|
||||
"dep:leptos_axum",
|
||||
"dep:camino",
|
||||
"dep:serde_json",
|
||||
"dep:moonboard-parser",
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
|
||||
@@ -20,11 +20,12 @@ pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[leptos::component]
|
||||
#[component]
|
||||
pub fn App() -> impl leptos::IntoView {
|
||||
use leptos_meta::Stylesheet;
|
||||
use leptos_meta::Title;
|
||||
|
||||
// TODO: look at tracing-subscriber-wasm
|
||||
#[cfg(feature = "ssr")]
|
||||
tracing::debug!("Rendering root component");
|
||||
|
||||
@@ -43,7 +44,9 @@ pub fn App() -> impl leptos::IntoView {
|
||||
}
|
||||
}
|
||||
|
||||
#[leptos::component]
|
||||
#[component]
|
||||
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")]
|
||||
pub mod server;
|
||||
|
||||
pub mod models;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
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 ron_codec::RonCodec;
|
||||
|
||||
#[leptos::component]
|
||||
#[component]
|
||||
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))
|
||||
.map(|i| {
|
||||
let i = i.to_string();
|
||||
view! {
|
||||
<div class="aspect-square rounded border-2 border-dashed border-sky-500 bg-indigo-100">
|
||||
{i}
|
||||
</div>
|
||||
}
|
||||
view! { <Cell label=i/> }
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
leptos::view! {
|
||||
<Await future=random_problem let:foo>
|
||||
<p>"Got a problem!"</p>
|
||||
</Await>
|
||||
|
||||
<div class="container mx-auto border">
|
||||
<div class="grid grid-rows-4 grid-cols-12 gap-4">{cells}</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
|
||||
|
||||
use crate::models;
|
||||
use models::HoldPosition;
|
||||
use models::HoldRole;
|
||||
use models::Problem;
|
||||
use persistence::Persistent;
|
||||
use state::PersistentState;
|
||||
use state::State;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use type_toppings::ResultExt;
|
||||
|
||||
pub mod cli {
|
||||
//! Server CLI interface
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
@@ -22,6 +31,10 @@ pub mod cli {
|
||||
|
||||
/// Resets state, replacing it with defaults
|
||||
ResetState,
|
||||
|
||||
ImportMiniMoonboardProblems {
|
||||
file_path: PathBuf,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +42,7 @@ pub mod state {
|
||||
//! Server state
|
||||
|
||||
use super::persistence::Persistent;
|
||||
use crate::models;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -46,59 +60,25 @@ pub mod state {
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct Wall {
|
||||
pub rows: u32,
|
||||
pub cols: u32,
|
||||
pub holds: BTreeMap<HoldPosition, Hold>,
|
||||
pub routes: BTreeSet<route::Route>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct HoldPosition {
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub rows: u64,
|
||||
pub cols: u64,
|
||||
pub holds: BTreeMap<models::HoldPosition, Hold>,
|
||||
pub problems: BTreeSet<models::Problem>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct Hold {
|
||||
pub position: HoldPosition,
|
||||
pub position: models::HoldPosition,
|
||||
pub image: Option<Image>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
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 const STATE_FILE: &str = "state.json";
|
||||
pub const STATE_FILE: &str = "state.ron";
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn main() {
|
||||
@@ -120,6 +100,17 @@ pub async fn main() {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
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 => {
|
||||
let s = PersistentState::default();
|
||||
let p = camino::Utf8Path::new(STATE_FILE);
|
||||
@@ -133,7 +124,6 @@ pub async fn main() {
|
||||
async fn serve() -> Result<(), Error> {
|
||||
use crate::app::App;
|
||||
use crate::app::shell;
|
||||
use crate::server::state::State;
|
||||
use axum::Router;
|
||||
use leptos::prelude::*;
|
||||
use leptos_axum::LeptosRoutes;
|
||||
@@ -149,10 +139,7 @@ async fn serve() -> Result<(), Error> {
|
||||
let addr = leptos_options.site_addr;
|
||||
let routes = generate_route_list(App);
|
||||
|
||||
tracing::info!("Loading state");
|
||||
let server_state = State {
|
||||
persistent: Persistent::<PersistentState>::load(STATE_FILE.into()).await?,
|
||||
};
|
||||
let server_state = load_state().await?;
|
||||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
@@ -170,9 +157,46 @@ async fn serve() -> Result<(), Error> {
|
||||
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)]
|
||||
#[display("Server crash")]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
Persistence(persistence::Error),
|
||||
Parser(moonboard_parser::Error),
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl<T> Persistent<T> {
|
||||
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(),
|
||||
source,
|
||||
})?;
|
||||
@@ -43,6 +43,7 @@ impl<T> Persistent<T> {
|
||||
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
|
||||
@@ -53,7 +54,7 @@ impl<T> Persistent<T> {
|
||||
state.clone()
|
||||
}
|
||||
|
||||
/// Returns state
|
||||
/// Returns state passed through given function
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn with<F, R>(&self, f: F) -> R
|
||||
where
|
||||
@@ -96,7 +97,8 @@ impl<T> Persistent<T> {
|
||||
where
|
||||
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 {
|
||||
file_path: file_path.to_owned(),
|
||||
source,
|
||||
@@ -112,10 +114,13 @@ pub enum Error {
|
||||
Read { file_path: Utf8PathBuf, source: std::io::Error },
|
||||
|
||||
#[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")]
|
||||
Serialize { source: serde_json::Error },
|
||||
Serialize { source: ron::Error },
|
||||
|
||||
#[display("Failed to write file: {file_path}")]
|
||||
Write { file_path: Utf8PathBuf, source: std::io::Error },
|
||||
|
||||
Reference in New Issue
Block a user