diff --git a/src/components/mod.rs b/src/components/mod.rs index 26a9632..c1da939 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -7,7 +7,7 @@ pub mod crud { #[async_trait] pub trait Create: Sized { - type Id: Sized + Send + Sync + 'static; + type Id: Sized + Send + Sync + Copy + 'static; type Filter: Sized + Send + Sync + 'static; type Info: Sized + Send + Sync + 'static; @@ -22,7 +22,7 @@ pub mod crud { #[async_trait] pub trait Read: Sized { type Filter; - type Id; + type Id: Copy; async fn findall( ctx: &Context, @@ -40,7 +40,7 @@ pub mod crud { #[async_trait] pub trait Update: Sized { - type Id; + type Id: Copy; type Filter; type UpdateElement; @@ -99,6 +99,20 @@ pub mod crud { Ok(true) } } + + #[async_trait] + pub trait Toggle: Sized { + type Id: Sized + Send + Sync + Copy + 'static; + type Filter: Sized + Send + Sync + 'static; + + async fn set( + ctx: &Context, + pool: &sqlite::Pool, + filter: Self::Filter, + id: Self::Id, + value: bool, + ) -> Result<(), crate::Error>; + } } pub mod view { @@ -192,8 +206,8 @@ pub mod route { } #[async_trait] - pub trait ToggleFallback: Send + Sync + Sized + 'static { - type UrlParams: Clone + Copy + Send + Sync + Sized + 'static; + pub trait ToggleFallback: super::crud::Toggle { + type UrlParams: Send + Sync + 'static; const URL_TRUE: &'static str; const URL_FALSE: &'static str; @@ -212,7 +226,7 @@ pub mod route { headers: HeaderMap, Path(path): Path, ) -> Result, crate::Error> { - Self::set(user, state, headers, path, true).await + ::set(user, state, headers, path, true).await } async fn set_false( @@ -221,7 +235,7 @@ pub mod route { headers: HeaderMap, Path(path): Path, ) -> Result, crate::Error> { - Self::set(user, state, headers, path, false).await + ::set(user, state, headers, path, false).await } fn router() -> axum::Router @@ -232,7 +246,9 @@ pub mod route { } #[async_trait] - pub trait ToggleHtmx { + pub trait ToggleHtmx: super::crud::Toggle { + type Id: Send + Sync + Copy + 'static + From; + type Filter: Send + Sync + 'static + From; type UrlParams: Send + Sync + 'static; const URL_TRUE: &'static str; @@ -243,22 +259,33 @@ pub mod route { state: AppState, params: Self::UrlParams, value: bool, + ) -> Result<(crate::Context, AppState, Self::UrlParams, bool), crate::Error>; + + async fn response( + ctx: &crate::Context, + state: AppState, + params: Self::UrlParams, + value: bool, ) -> Result, crate::Error>; - async fn set_true( + async fn on( Extension(user): Extension, State(state): State, Path(path): Path, ) -> Result, crate::Error> { - Self::set(user, state, path, true).await + let (ctx, state, params, value) = + ::set(user, state, path, true).await?; + ::response(&ctx, state, params, value).await } - async fn set_false( + async fn off( Extension(user): Extension, State(state): State, Path(path): Path, ) -> Result, crate::Error> { - Self::set(user, state, path, false).await + let (ctx, state, params, value) = + ::set(user, state, path, false).await?; + ::response(&ctx, state, params, value).await } fn router() -> axum::Router @@ -296,7 +323,7 @@ pub mod route { } pub trait Router: Create + Delete { - fn get() -> axum::Router + fn router() -> axum::Router where B: HttpBody + Send + 'static, ::Data: Send, diff --git a/src/components/trips/todos/mod.rs b/src/components/trips/todos/mod.rs index 82fbdb5..020cceb 100644 --- a/src/components/trips/todos/mod.rs +++ b/src/components/trips/todos/mod.rs @@ -17,8 +17,9 @@ use uuid::Uuid; use crate::{ components::{ + self, crud::{self, Read, Update}, - route, + route::{self, Toggle}, view::{self, View}, }, htmx, @@ -86,6 +87,27 @@ pub struct Filter { pub trip_id: Uuid, } +impl From<(Uuid, Uuid)> for Filter { + fn from((trip_id, _todo_id): (Uuid, Uuid)) -> Self { + Self { trip_id } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Id(pub Uuid); + +impl std::fmt::Display for Id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From<(Uuid, Uuid)> for Id { + fn from((_trip_id, todo_id): (Uuid, Uuid)) -> Self { + Self(todo_id) + } +} + impl Todo { pub fn is_done(&self) -> bool { self.state == State::Done @@ -95,7 +117,7 @@ impl Todo { #[async_trait] impl crud::Read for Todo { type Filter = Filter; - type Id = Uuid; + type Id = Id; async fn findall( ctx: &Context, @@ -138,10 +160,10 @@ impl crud::Read for Todo { ctx: &Context, pool: &sqlite::Pool, filter: Filter, - todo_id: Uuid, + todo_id: Id, ) -> Result, Error> { let trip_id_param = filter.trip_id.to_string(); - let todo_id_param = todo_id.to_string(); + let todo_id_param = todo_id.0.to_string(); let user_id = ctx.user.id.to_string(); crate::query_one!( &sqlite::QueryClassification { @@ -178,7 +200,7 @@ pub struct TodoNew { #[async_trait] impl crud::Create for Todo { - type Id = Uuid; + type Id = Id; type Filter = Filter; type Info = TodoNew; @@ -215,7 +237,7 @@ impl crud::Create for Todo { ) .await?; - Ok(id) + Ok(components::trips::todos::Id(id)) } } @@ -255,7 +277,7 @@ pub enum UpdateElement { #[async_trait] impl crud::Update for Todo { - type Id = Uuid; + type Id = Id; type Filter = Filter; type UpdateElement = UpdateElement; @@ -344,15 +366,15 @@ impl crud::Update for Todo { #[async_trait] impl crud::Delete for Todo { - type Id = Uuid; + type Id = Id; type Filter = Filter; #[tracing::instrument] - async fn delete<'c, T>(ctx: &Context, db: T, filter: &Filter, id: Uuid) -> Result + async fn delete<'c, T>(ctx: &Context, db: T, filter: &Filter, id: Id) -> Result where T: sqlx::Acquire<'c, Database = sqlx::Sqlite> + Send + std::fmt::Debug, { - let id_param = id.to_string(); + let id_param = id.0.to_string(); let user_id = ctx.user.id.to_string(); let trip_id_param = filter.trip_id.to_string(); @@ -663,7 +685,7 @@ impl route::Delete for Todo { &ctx, &state.database_pool, &Filter { trip_id }, - todo_id, + components::trips::todos::Id(todo_id), ) .await?; @@ -692,7 +714,7 @@ impl route::Delete for Todo { } impl route::Router for Todo { - fn get() -> axum::Router + fn router() -> axum::Router where B: HttpBody + Send + 'static, ::Data: Send, @@ -704,6 +726,7 @@ impl route::Router for Todo { "/:id/delete", axum::routing::post(::delete), ) + .merge(StateUpdate::router()) } } @@ -719,7 +742,7 @@ pub async fn trip_todo_done( &ctx, &state.database_pool, Filter { trip_id }, - todo_id, + Id(todo_id), UpdateElement::State(State::Done.into()), ) .await?; @@ -738,12 +761,12 @@ pub async fn trip_todo_undone_htmx( &ctx, &state.database_pool, Filter { trip_id }, - todo_id, + Id(todo_id), UpdateElement::State(State::Todo.into()), ) .await?; - let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id) + let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)) .await? .ok_or_else(|| { crate::Error::Request(RequestError::NotFound { @@ -769,7 +792,7 @@ pub async fn trip_todo_undone( &ctx, &state.database_pool, Filter { trip_id }, - todo_id, + Id(todo_id), UpdateElement::State(State::Todo.into()), ) .await?; @@ -792,7 +815,7 @@ pub async fn trip_todo_edit( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id).await?; + let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)).await?; match todo_item { None => Err(crate::Error::Request(RequestError::NotFound { @@ -820,7 +843,7 @@ pub async fn trip_todo_edit_save( &ctx, &state.database_pool, Filter { trip_id }, - todo_id, + Id(todo_id), UpdateElement::Description(form.description.into()), ) .await?; @@ -852,7 +875,7 @@ pub async fn trip_todo_edit_cancel( Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, ) -> Result { let ctx = Context::build(current_user); - let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id).await?; + let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)).await?; match todo_item { None => Err(crate::Error::Request(RequestError::NotFound { @@ -867,6 +890,23 @@ pub async fn trip_todo_edit_cancel( } } +#[async_trait] +impl crud::Toggle for StateUpdate { + type Id = Id; + type Filter = Filter; + + async fn set( + ctx: &Context, + pool: &sqlite::Pool, + filter: Self::Filter, + id: Self::Id, + value: bool, + ) -> Result<(), crate::Error> { + Todo::update(&ctx, &pool, filter, id, UpdateElement::State(value.into())).await?; + Ok(()) + } +} + #[async_trait] impl route::ToggleFallback for StateUpdate { type UrlParams = (Uuid, Uuid); @@ -882,12 +922,12 @@ impl route::ToggleFallback for StateUpdate { value: bool, ) -> Result, crate::Error> { let ctx = Context::build(current_user); - Todo::update( + ::set( &ctx, &state.database_pool, Filter { trip_id }, - todo_id, - UpdateElement::State(value.into()), + Id(todo_id), + value, ) .await?; @@ -908,6 +948,8 @@ impl route::ToggleFallback for StateUpdate { #[async_trait] impl route::ToggleHtmx for StateUpdate { + type Id = Id; + type Filter = Filter; type UrlParams = (Uuid, Uuid); const URL_TRUE: &'static str = "/:id/done/htmx/true"; @@ -916,20 +958,29 @@ impl route::ToggleHtmx for StateUpdate { async fn set( current_user: User, state: AppState, - (trip_id, todo_id): (Uuid, Uuid), + params: Self::UrlParams, value: bool, - ) -> Result, crate::Error> { + ) -> Result<(crate::Context, AppState, Self::UrlParams, bool), crate::Error> { let ctx = Context::build(current_user); - Todo::update( + ::set( &ctx, &state.database_pool, - Filter { trip_id }, - todo_id, - UpdateElement::State(value.into()), + params.into(), + params.into(), + value, ) .await?; - let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id) + Ok((ctx, state, params, value)) + } + + async fn response( + ctx: &Context, + state: AppState, + (trip_id, todo_id): (Uuid, Uuid), + value: bool, + ) -> Result, crate::Error> { + let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)) .await? .ok_or_else(|| { crate::Error::Request(RequestError::NotFound { @@ -952,8 +1003,8 @@ impl route::ToggleHtmx for StateUpdate { ::Error: std::error::Error + Sync + Send, { axum::Router::new() - .route(Self::URL_TRUE, post(Self::set_true)) - .route(Self::URL_FALSE, post(Self::set_false)) + .route(Self::URL_TRUE, post(Self::on)) + .route(Self::URL_FALSE, post(Self::off)) } } diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 8f669a3..af3ae6d 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -13,10 +13,7 @@ use std::{fmt, time::Duration}; use tower::{timeout::TimeoutLayer, ServiceBuilder}; use crate::{ - components::{ - self, - route::{Router as _, Toggle}, - }, + components::{self, route::Router as _}, AppState, Error, RequestError, TopLevelPage, }; @@ -168,11 +165,7 @@ pub fn router(state: AppState) -> Router { "/:id/todo/:id/edit/cancel", post(components::trips::todos::trip_todo_edit_cancel), ) - .nest( - "/:id/todo/", - components::trips::todos::Todo::get() - .merge(::router()), - ), + .nest("/:id/todo/", components::trips::todos::Todo::router()), ) .nest( (&TopLevelPage::Inventory.path()).into(), diff --git a/src/routing/routes.rs b/src/routing/routes.rs index a97715e..70b26a2 100644 --- a/src/routing/routes.rs +++ b/src/routing/routes.rs @@ -441,7 +441,7 @@ pub async fn trip( &ctx, &state.database_pool, &todos::Filter { trip_id: id }, - delete_todo, + components::trips::todos::Id(delete_todo), ) .await?;