This commit is contained in:
2025-02-04 16:27:38 +01:00
parent 51ada6c9bd
commit 73abd96f10
7 changed files with 319 additions and 142 deletions

View File

@@ -45,6 +45,8 @@ 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 }
serde_json = { version = "1" }
codee = { version = "0.3" }
[dev-dependencies.serde_json]
version = "1"

View File

@@ -1,10 +1,11 @@
use crate::codec::ron::RonCodec;
use crate::codec::ron::RonEncoded;
use crate::models;
use crate::pages;
use leptos::prelude::*;
use leptos_router::components::*;
use leptos_router::path;
use std::sync::Arc;
use type_toppings::ResultExt;
pub fn shell(options: LeptosOptions) -> impl IntoView {
use leptos_meta::MetaTags;
@@ -60,7 +61,13 @@ pub fn Home() -> impl leptos::IntoView {
let action = Action::new(|()| async move {
tracing::debug!("running action");
let walls = get_walls().await.unwrap().into_inner();
let walls = get_walls()
.await
.inspect_err(|e| {
dbg!(e);
})
.expect("failed to get walls")
.into_inner();
let wall = walls.first();
if let Some(wall) = wall {
@@ -81,7 +88,7 @@ pub fn Home() -> impl leptos::IntoView {
#[server]
#[tracing::instrument(skip_all, err)]
async fn get_walls() -> Result<RonCodec<Vec<models::Wall>>, ServerFnError> {
async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
use redb::ReadableTable;
tracing::debug!("get walls");
@@ -104,5 +111,5 @@ async fn get_walls() -> Result<RonCodec<Vec<models::Wall>>, ServerFnError> {
})
.await??;
Ok(RonCodec::new(walls))
Ok(RonEncoded::new(walls))
}

View File

@@ -1,59 +1,159 @@
pub mod ron {
//! Wrap T in RonCodec<T> that when serialized, always serializes to a [ron] string.
#[derive(Debug, Clone)]
pub struct RonCodec<T> {
t: T,
use codee::Decoder;
use codee::Encoder;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeOwned;
use server_fn::ServerFnError;
use server_fn::codec::Encoding;
use server_fn::codec::FromReq;
use server_fn::codec::FromRes;
use server_fn::codec::IntoReq;
use server_fn::codec::IntoRes;
use server_fn::request::ClientReq;
use server_fn::request::Req;
use server_fn::response::ClientRes;
use server_fn::response::Res;
pub struct Ron;
impl<T> Encoder<T> for Ron
where
T: Serialize,
{
type Encoded = String;
type Error = ron::Error;
fn encode(val: &T) -> Result<Self::Encoded, Self::Error> {
ron::to_string(val)
}
}
impl<T> RonCodec<T> {
impl<T> Decoder<T> for Ron
where
for<'de> T: Deserialize<'de>,
{
type Encoded = str;
type Error = ron::error::SpannedError;
fn decode(val: &Self::Encoded) -> Result<T, Self::Error> {
ron::from_str(val)
}
}
impl Encoding for Ron {
const CONTENT_TYPE: &'static str = "application/ron";
const METHOD: http::Method = http::Method::POST;
}
#[derive(Debug, Clone)]
pub struct RonEncoded<T>(pub T);
impl<T> RonEncoded<T> {
pub fn into_inner(self) -> T {
self.t
self.0
}
pub fn new(t: T) -> Self {
Self { t }
Self(t)
}
}
impl<T> std::ops::Deref for RonCodec<T> {
impl<T> std::ops::Deref for RonEncoded<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.t
&self.0
}
}
impl<T> serde::Serialize for RonCodec<T>
// impl<T> serde::Serialize for RonEncoded<T>
// where
// T: serde::Serialize,
// {
// #[tracing::instrument(skip_all, err)]
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: serde::Serializer,
// {
// let serialized = ron::to_string(&self.0).map_err(serde::ser::Error::custom)?;
// serializer.serialize_str(&serialized)
// }
// }
// impl<'de, T> serde::Deserialize<'de> for RonEncoded<T>
// where
// T: serde::de::DeserializeOwned + 'static,
// {
// #[tracing::instrument(skip_all, err)]
// 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))
// }
// }
// IntoReq
impl<T, Request, Err> IntoReq<Ron, Request, Err> for RonEncoded<T>
where
T: serde::Serialize,
Request: ClientReq<Err>,
T: 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)
fn into_req(self, path: &str, accepts: &str) -> Result<Request, ServerFnError<Err>> {
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Request::try_new_post(path, Ron::CONTENT_TYPE, accepts, data)
}
}
impl<'de, T> serde::Deserialize<'de> for RonCodec<T>
// FromReq
impl<T, Request, Err> FromReq<Ron, Request, Err> for RonEncoded<T>
where
T: serde::de::DeserializeOwned + 'static,
Request: Req<Err> + Send,
T: DeserializeOwned,
{
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 })
async fn from_req(req: Request) -> Result<Self, ServerFnError<Err>> {
let data = req.try_into_string().await?;
Ron::decode(&data).map(RonEncoded).map_err(|e| ServerFnError::Args(e.to_string()))
}
}
// IntoRes
impl<CustErr, T, Response> IntoRes<Ron, Response, CustErr> for RonEncoded<T>
where
Response: Res<CustErr>,
T: Serialize + Send,
{
async fn into_res(self) -> Result<Response, ServerFnError<CustErr>> {
let data = Ron::encode(&self.0).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
Response::try_from_string(Ron::CONTENT_TYPE, data)
}
}
// FromRes
impl<T, Response, Err> FromRes<Ron, Response, Err> for RonEncoded<T>
where
Response: ClientRes<Err> + Send,
T: DeserializeOwned,
{
async fn from_res(res: Response) -> Result<Self, ServerFnError<Err>> {
let data = res.try_into_string().await?;
Ron::decode(&data)
.map(RonEncoded)
.map_err(|e| ServerFnError::Deserialization(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::RonCodec;
use super::Ron;
use super::RonEncoded;
use codee::Decoder;
use codee::Encoder;
use serde::Deserialize;
use serde::Serialize;
@@ -63,25 +163,36 @@ pub mod ron {
value: i32,
}
// #[test]
// fn test_ron_wrapper() {
// let original = TestStruct {
// name: "Test".to_string(),
// value: 42,
// };
// // Wrap in RonCodec
// let wrapped = RonEncoded::new(original.clone());
// // Serialize
// let serialized = serde_json::to_string(&wrapped).expect("Serialization failed");
// println!("Serialized: {}", serialized);
// // Deserialize
// let deserialized: RonEncoded<TestStruct> = serde_json::from_str(&serialized).expect("Deserialization failed");
// // Compare
// assert_eq!(deserialized.into_inner(), original);
// }
#[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);
let enc = Ron::encode(&original).unwrap();
let dec: TestStruct = Ron::decode(&enc).unwrap();
assert_eq!(dec, original);
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::codec::ron::RonCodec;
use crate::codec::ron::RonEncoded;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
@@ -150,7 +150,7 @@ pub struct Image {
}
#[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
async fn load_initial_data() -> Result<RonEncoded<InitialData>, ServerFnError> {
todo!()
// let wall = state.persistent.with(|s| s.wall.clone()).await;
// Ok(RonCodec::new(InitialData { wall }))

View File

@@ -1,4 +1,4 @@
use crate::codec::ron::RonCodec;
use crate::codec::ron::RonEncoded;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
@@ -63,7 +63,7 @@ pub struct InitialData {
}
#[server]
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
async fn load_initial_data() -> Result<RonEncoded<InitialData>, ServerFnError> {
todo!()
// let state = expect_context::<State>();

View File

@@ -1,10 +1,12 @@
use crate::codec::ron::RonCodec;
use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::components::button::Button;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::components::header::StyledHeader;
use crate::models;
use crate::models::HoldRole;
use crate::models::WallUid;
use leptos::Params;
use leptos::prelude::*;
use leptos::reactive::graph::ReactiveNode;
@@ -17,21 +19,37 @@ struct WallParams {
}
#[component]
#[tracing::instrument(skip_all)]
pub fn Wall() -> impl leptos::IntoView {
tracing::debug!("Enter");
let params = leptos_router::hooks::use_params::<WallParams>();
let wall = Resource::new(
// TODO
let wall = Resource::<models::Wall, Ron>::new_with_options(
move || params.get().unwrap().id,
move |wall_id| async move {
if let Some(wall_id) = wall_id {
let wall = get_wall(wall_id).await.unwrap().into_inner();
Some(wall)
} else {
None
{
move |wall_uid: Option<WallUid>| async move {
let wall_uid = wall_uid.unwrap();
let wall = get_wall(wall_uid).await.unwrap().into_inner();
wall
}
},
false,
);
// // TODO
// let wall = Resource::new(
// move || params.get().unwrap().id,
// move |wall_id| async move {
// if let Some(wall_id) = wall_id {
// let wall = get_wall(wall_id).await.unwrap();
// Some(wall)
// } else {
// None
// }
// },
// );
let header_items = HeaderItems {
left: vec![],
middle: vec![HeaderItem {
@@ -57,10 +75,10 @@ pub fn Wall() -> impl leptos::IntoView {
<div class="m-2">
<Suspense fallback=move || view! {<p>"Loading..."</p>}>
{move || Suspend::new(async move{
let wall: Option<Option<models::Wall>> = wall.get();
let wall: Option<Option<RonEncoded<models::Wall>>> = wall.get();
wall.map(|wall|{let wall = wall.unwrap();
view! {
<Ready wall />
<Ready wall=wall.into_inner() />
}
})
})}
@@ -95,6 +113,7 @@ fn Ready(wall: models::Wall) -> impl leptos::IntoView {
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols);
tracing::debug!("view");
view! {
<div class="grid grid-cols-[auto,1fr] gap-8">
// Render the wall
@@ -114,7 +133,9 @@ fn Ready(wall: models::Wall) -> impl leptos::IntoView {
}
#[component]
#[tracing::instrument(skip_all)]
fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
tracing::trace!("Enter");
let class = move || {
let role_classes = match role.get() {
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
@@ -137,13 +158,18 @@ fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> imp
view! { <img class="object-cover w-full h-full" src=src /> }
});
tracing::trace!("view");
view! { <div class=class>{img}</div> }
}
#[server]
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_wall(wall_id: models::WallUid) -> Result<RonCodec<models::Wall>, ServerFnError> {
let db = expect_context::<Arc<redb::Database>>();
async fn get_wall(wall_uid: models::WallUid) -> Result<RonEncoded<models::Wall>, ServerFnError> {
tracing::debug!("Enter");
#[derive(Debug, derive_more::Error, derive_more::Display)]
enum Error {
@@ -151,23 +177,43 @@ async fn get_wall(wall_id: models::WallUid) -> Result<RonCodec<models::Wall>, Se
NotFound(#[error(not(source))] models::WallUid),
}
let db = expect_context::<Arc<redb::Database>>();
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();
let wall = walls_table.get(wall_uid)?.ok_or(Error::NotFound(wall_uid))?.value();
Ok(wall)
})
.await??;
Ok(RonCodec::new(wall))
tracing::debug!("ok");
Ok(RonEncoded::new(wall))
}
#[server]
#[server(
input = Ron,
output = Ron,
custom = RonEncoded
)]
#[tracing::instrument(skip_all, err)]
async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, ServerFnError> {
Ok(RonCodec::new(None))
async fn get_problem(wall_uid: models::WallUid, problem_uid: models::ProblemUid) -> Result<RonEncoded<Option<models::Problem>>, ServerFnError> {
tracing::debug!("Enter");
let db = expect_context::<Arc<redb::Database>>();
let problem = tokio::task::spawn_blocking(move || -> Result<Option<models::Problem>, ServerFnError> {
let read_txn = db.begin_read()?;
let table = read_txn.open_table(crate::server::db::current::TABLE_PROBLEMS)?;
let problem = table.get((wall_uid, problem_uid))?.map(|guard| guard.value());
Ok(problem)
})
.await??;
// use rand::seq::IteratorRandom;
@@ -184,5 +230,5 @@ async fn get_random_problem() -> Result<RonCodec<Option<models::Problem>>, Serve
// tracing::debug!("Returning randomized problem: {problem:?}");
// Ok(RonCodec::new(problem))
Ok(RonEncoded::new(problem))
}