refactor(routes): add macros to reduce route handler boilerplate
- Create define_get_handler! macro for GET-by-ID endpoints - Create define_delete_handler! macro for DELETE endpoints with Datastar support - Apply macros to roasters, roasts, and bags route modules - Reduces 6 handlers from ~78 lines to ~12 lines total
This commit is contained in:
parent
9f70aa0892
commit
2d183e6955
5 changed files with 108 additions and 85 deletions
|
|
@ -4,6 +4,7 @@ use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::macros::{define_delete_handler, define_get_handler};
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||||
use crate::application::routes::render_html;
|
use crate::application::routes::render_html;
|
||||||
|
|
@ -184,14 +185,7 @@ pub(crate) async fn list_bags(
|
||||||
Ok(Json(bags))
|
Ok(Json(bags))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
define_get_handler!(get_bag, BagId, Bag, bag_repo);
|
||||||
pub(crate) async fn get_bag(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Path(id): Path<BagId>,
|
|
||||||
) -> Result<Json<Bag>, ApiError> {
|
|
||||||
let bag = state.bag_repo.get(id).await.map_err(AppError::from)?;
|
|
||||||
Ok(Json(bag))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn update_bag(
|
pub(crate) async fn update_bag(
|
||||||
|
|
@ -265,25 +259,13 @@ pub(crate) async fn update_bag(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
define_delete_handler!(
|
||||||
pub(crate) async fn delete_bag(
|
delete_bag,
|
||||||
State(state): State<AppState>,
|
BagId,
|
||||||
_auth_user: AuthenticatedUser,
|
BagSortKey,
|
||||||
headers: HeaderMap,
|
bag_repo,
|
||||||
Path(id): Path<BagId>,
|
render_bag_list_fragment
|
||||||
Query(query): Query<ListQuery>,
|
);
|
||||||
) -> Result<Response, ApiError> {
|
|
||||||
let request = query.into_request::<BagSortKey>();
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct BagsQuery {
|
pub struct BagsQuery {
|
||||||
|
|
|
||||||
80
src/application/routes/macros.rs
Normal file
80
src/application/routes/macros.rs
Normal file
|
|
@ -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<crate::application::server::AppState>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<$id_type>,
|
||||||
|
) -> Result<axum::Json<$entity_type>, 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<crate::application::server::AppState>,
|
||||||
|
_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<axum::response::Response, crate::application::errors::ApiError> {
|
||||||
|
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;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod bags;
|
pub mod bags;
|
||||||
|
mod macros;
|
||||||
pub mod roasters;
|
pub mod roasters;
|
||||||
pub mod roasts;
|
pub mod roasts;
|
||||||
pub mod support;
|
pub mod support;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use axum::extract::{Path, Query, State};
|
||||||
use axum::http::{HeaderMap, StatusCode};
|
use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||||
|
|
||||||
|
use super::macros::{define_delete_handler, define_get_handler};
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||||
use crate::application::routes::render_html;
|
use crate::application::routes::render_html;
|
||||||
|
|
@ -148,14 +149,7 @@ pub(crate) async fn create_roaster(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo);
|
||||||
pub(crate) async fn get_roaster(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Path(id): Path<RoasterId>,
|
|
||||||
) -> Result<Json<Roaster>, ApiError> {
|
|
||||||
let roaster = state.roaster_repo.get(id).await.map_err(AppError::from)?;
|
|
||||||
Ok(Json(roaster))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user))]
|
#[tracing::instrument(skip(state, _auth_user))]
|
||||||
pub(crate) async fn update_roaster(
|
pub(crate) async fn update_roaster(
|
||||||
|
|
@ -182,29 +176,13 @@ pub(crate) async fn update_roaster(
|
||||||
Ok(Json(roaster))
|
Ok(Json(roaster))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
define_delete_handler!(
|
||||||
pub(crate) async fn delete_roaster(
|
delete_roaster,
|
||||||
State(state): State<AppState>,
|
RoasterId,
|
||||||
_auth_user: AuthenticatedUser,
|
RoasterSortKey,
|
||||||
headers: HeaderMap,
|
roaster_repo,
|
||||||
Path(id): Path<RoasterId>,
|
render_roaster_list_fragment
|
||||||
Query(query): Query<ListQuery>,
|
);
|
||||||
) -> Result<Response, ApiError> {
|
|
||||||
let request = query.into_request::<RoasterSortKey>();
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_roaster_list_fragment(
|
async fn render_roaster_list_fragment(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use axum::http::{HeaderMap, StatusCode};
|
||||||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::macros::{define_delete_handler, define_get_handler};
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||||
use crate::application::routes::render_html;
|
use crate::application::routes::render_html;
|
||||||
|
|
@ -205,34 +206,15 @@ pub(crate) async fn list_roasts(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state))]
|
define_get_handler!(get_roast, RoastId, Roast, roast_repo);
|
||||||
pub(crate) async fn get_roast(
|
|
||||||
State(state): State<AppState>,
|
|
||||||
Path(id): Path<RoastId>,
|
|
||||||
) -> Result<Json<Roast>, ApiError> {
|
|
||||||
let roast = state.roast_repo.get(id).await.map_err(AppError::from)?;
|
|
||||||
Ok(Json(roast))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
define_delete_handler!(
|
||||||
pub(crate) async fn delete_roast(
|
delete_roast,
|
||||||
State(state): State<AppState>,
|
RoastId,
|
||||||
_auth_user: AuthenticatedUser,
|
RoastSortKey,
|
||||||
headers: HeaderMap,
|
roast_repo,
|
||||||
Path(id): Path<RoastId>,
|
render_roast_list_fragment
|
||||||
Query(query): Query<ListQuery>,
|
);
|
||||||
) -> Result<Response, ApiError> {
|
|
||||||
let request = query.into_request::<RoastSortKey>();
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RoastsQuery {
|
pub struct RoastsQuery {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue