diff --git a/src/application/routes/bags.rs b/src/application/routes/bags.rs index 97e8eac..3225b4d 100644 --- a/src/application/routes/bags.rs +++ b/src/application/routes/bags.rs @@ -4,6 +4,7 @@ use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Redirect, Response}; use serde::Deserialize; +use super::macros::{define_delete_handler, define_get_handler}; use crate::application::auth::AuthenticatedUser; use crate::application::errors::{ApiError, AppError, map_app_error}; use crate::application::routes::render_html; @@ -184,14 +185,7 @@ pub(crate) async fn list_bags( Ok(Json(bags)) } -#[tracing::instrument(skip(state))] -pub(crate) async fn get_bag( - State(state): State, - Path(id): Path, -) -> Result, ApiError> { - let bag = state.bag_repo.get(id).await.map_err(AppError::from)?; - Ok(Json(bag)) -} +define_get_handler!(get_bag, BagId, Bag, bag_repo); #[tracing::instrument(skip(state, _auth_user, headers, query))] pub(crate) async fn update_bag( @@ -265,25 +259,13 @@ pub(crate) async fn update_bag( } } -#[tracing::instrument(skip(state, _auth_user, headers, query))] -pub(crate) async fn delete_bag( - State(state): State, - _auth_user: AuthenticatedUser, - headers: HeaderMap, - Path(id): Path, - Query(query): Query, -) -> Result { - let request = query.into_request::(); - state.bag_repo.delete(id).await.map_err(AppError::from)?; - - if is_datastar_request(&headers) { - render_bag_list_fragment(state, request, true) - .await - .map_err(ApiError::from) - } else { - Ok(StatusCode::NO_CONTENT.into_response()) - } -} +define_delete_handler!( + delete_bag, + BagId, + BagSortKey, + bag_repo, + render_bag_list_fragment +); #[derive(Debug, Deserialize)] pub struct BagsQuery { diff --git a/src/application/routes/macros.rs b/src/application/routes/macros.rs new file mode 100644 index 0000000..cd452a9 --- /dev/null +++ b/src/application/routes/macros.rs @@ -0,0 +1,80 @@ +/// Generates a GET-by-ID handler that retrieves an entity from a repository. +/// +/// # Arguments +/// * `$fn_name` - Name of the generated handler function +/// * `$id_type` - Type of the ID path parameter (e.g., `RoasterId`) +/// * `$entity_type` - Type of the entity returned as JSON (e.g., `Roaster`) +/// * `$repo_field` - Name of the repository field on `AppState` (e.g., `roaster_repo`) +/// +/// # Example +/// ```ignore +/// define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo); +/// ``` +macro_rules! define_get_handler { + ($fn_name:ident, $id_type:ty, $entity_type:ty, $repo_field:ident) => { + #[tracing::instrument(skip(state))] + pub(crate) async fn $fn_name( + axum::extract::State(state): axum::extract::State, + axum::extract::Path(id): axum::extract::Path<$id_type>, + ) -> Result, crate::application::errors::ApiError> { + let entity = state + .$repo_field + .get(id) + .await + .map_err(crate::application::errors::AppError::from)?; + Ok(axum::Json(entity)) + } + }; +} + +/// Generates a DELETE handler with Datastar fragment re-rendering support. +/// +/// # Arguments +/// * `$fn_name` - Name of the generated handler function +/// * `$id_type` - Type of the ID path parameter (e.g., `RoasterId`) +/// * `$sort_key` - Sort key type for list requests (e.g., `RoasterSortKey`) +/// * `$repo_field` - Name of the repository field on `AppState` (e.g., `roaster_repo`) +/// * `$render_fragment` - Path to the fragment render function +/// +/// # Example +/// ```ignore +/// define_delete_handler!( +/// delete_roaster, +/// RoasterId, +/// RoasterSortKey, +/// roaster_repo, +/// render_roaster_list_fragment +/// ); +/// ``` +macro_rules! define_delete_handler { + ($fn_name:ident, $id_type:ty, $sort_key:ty, $repo_field:ident, $render_fragment:path) => { + #[tracing::instrument(skip(state, _auth_user, headers, query))] + pub(crate) async fn $fn_name( + axum::extract::State(state): axum::extract::State, + _auth_user: crate::application::auth::AuthenticatedUser, + headers: axum::http::HeaderMap, + axum::extract::Path(id): axum::extract::Path<$id_type>, + axum::extract::Query(query): axum::extract::Query< + crate::application::routes::support::ListQuery, + >, + ) -> Result { + let request = query.into_request::<$sort_key>(); + state + .$repo_field + .delete(id) + .await + .map_err(crate::application::errors::AppError::from)?; + + if crate::application::routes::support::is_datastar_request(&headers) { + $render_fragment(state, request, true) + .await + .map_err(crate::application::errors::ApiError::from) + } else { + Ok(axum::http::StatusCode::NO_CONTENT.into_response()) + } + } + }; +} + +pub(super) use define_delete_handler; +pub(super) use define_get_handler; diff --git a/src/application/routes/mod.rs b/src/application/routes/mod.rs index a1213ac..557de98 100644 --- a/src/application/routes/mod.rs +++ b/src/application/routes/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod bags; +mod macros; pub mod roasters; pub mod roasts; pub mod support; diff --git a/src/application/routes/roasters.rs b/src/application/routes/roasters.rs index f75b711..6c51f40 100644 --- a/src/application/routes/roasters.rs +++ b/src/application/routes/roasters.rs @@ -3,6 +3,7 @@ use axum::extract::{Path, Query, State}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{Html, IntoResponse, Redirect, Response}; +use super::macros::{define_delete_handler, define_get_handler}; use crate::application::auth::AuthenticatedUser; use crate::application::errors::{ApiError, AppError, map_app_error}; use crate::application::routes::render_html; @@ -148,14 +149,7 @@ pub(crate) async fn create_roaster( } } -#[tracing::instrument(skip(state))] -pub(crate) async fn get_roaster( - State(state): State, - Path(id): Path, -) -> Result, ApiError> { - let roaster = state.roaster_repo.get(id).await.map_err(AppError::from)?; - Ok(Json(roaster)) -} +define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo); #[tracing::instrument(skip(state, _auth_user))] pub(crate) async fn update_roaster( @@ -182,29 +176,13 @@ pub(crate) async fn update_roaster( Ok(Json(roaster)) } -#[tracing::instrument(skip(state, _auth_user, headers, query))] -pub(crate) async fn delete_roaster( - State(state): State, - _auth_user: AuthenticatedUser, - headers: HeaderMap, - Path(id): Path, - Query(query): Query, -) -> Result { - let request = query.into_request::(); - state - .roaster_repo - .delete(id) - .await - .map_err(AppError::from)?; - - if is_datastar_request(&headers) { - render_roaster_list_fragment(state, request, true) - .await - .map_err(ApiError::from) - } else { - Ok(StatusCode::NO_CONTENT.into_response()) - } -} +define_delete_handler!( + delete_roaster, + RoasterId, + RoasterSortKey, + roaster_repo, + render_roaster_list_fragment +); async fn render_roaster_list_fragment( state: AppState, diff --git a/src/application/routes/roasts.rs b/src/application/routes/roasts.rs index 91e1c73..1a975a9 100644 --- a/src/application/routes/roasts.rs +++ b/src/application/routes/roasts.rs @@ -4,6 +4,7 @@ use axum::http::{HeaderMap, StatusCode}; use axum::response::{Html, IntoResponse, Redirect, Response}; use serde::Deserialize; +use super::macros::{define_delete_handler, define_get_handler}; use crate::application::auth::AuthenticatedUser; use crate::application::errors::{ApiError, AppError, map_app_error}; use crate::application::routes::render_html; @@ -205,34 +206,15 @@ pub(crate) async fn list_roasts( } } -#[tracing::instrument(skip(state))] -pub(crate) async fn get_roast( - State(state): State, - Path(id): Path, -) -> Result, ApiError> { - let roast = state.roast_repo.get(id).await.map_err(AppError::from)?; - Ok(Json(roast)) -} +define_get_handler!(get_roast, RoastId, Roast, roast_repo); -#[tracing::instrument(skip(state, _auth_user, headers, query))] -pub(crate) async fn delete_roast( - State(state): State, - _auth_user: AuthenticatedUser, - headers: HeaderMap, - Path(id): Path, - Query(query): Query, -) -> Result { - let request = query.into_request::(); - state.roast_repo.delete(id).await.map_err(AppError::from)?; - - if is_datastar_request(&headers) { - render_roast_list_fragment(state, request, true) - .await - .map_err(ApiError::from) - } else { - Ok(StatusCode::NO_CONTENT.into_response()) - } -} +define_delete_handler!( + delete_roast, + RoastId, + RoastSortKey, + roast_repo, + render_roast_list_fragment +); #[derive(Debug, Deserialize)] pub struct RoastsQuery {