refactor(routes): extract form option helpers and fragment renderer macro
- Add load_roaster_options, load_roast_options, load_cafe_options helpers to support.rs, replacing duplicated inline loading in roasts.rs, bags.rs, and cups.rs - Add define_list_fragment_renderer! macro for the 5 structurally identical render_*_list_fragment functions - Fix gear.rs to return navigator from load_gear_page like all other loaders instead of discarding and reconstructing it
This commit is contained in:
parent
4b55d1b65c
commit
714713827d
8 changed files with 132 additions and 135 deletions
|
|
@ -9,16 +9,15 @@ use crate::application::auth::AuthenticatedUser;
|
|||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, load_roaster_options,
|
||||
};
|
||||
use crate::application::server::AppState;
|
||||
use crate::domain::bags::{BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
||||
use crate::domain::ids::{BagId, RoastId};
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::domain::roasters::RoasterSortKey;
|
||||
use crate::domain::timeline::{NewTimelineEvent, TimelineEventDetail};
|
||||
use crate::presentation::web::templates::{BagListTemplate, BagsTemplate};
|
||||
use crate::presentation::web::views::{BagView, ListNavigator, Paginated, RoasterOptionView};
|
||||
use crate::presentation::web::views::{BagView, ListNavigator, Paginated};
|
||||
|
||||
const BAG_PAGE_PATH: &str = "/bags";
|
||||
const BAG_FRAGMENT_PATH: &str = "/bags#bag-list";
|
||||
|
|
@ -85,13 +84,7 @@ pub(crate) async fn bags_page(
|
|||
.map_err(map_app_error);
|
||||
}
|
||||
|
||||
let roasters = state
|
||||
.roaster_repo
|
||||
.list_all_sorted(RoasterSortKey::Name, SortDirection::Asc)
|
||||
.await
|
||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||
|
||||
let roaster_options = roasters.into_iter().map(RoasterOptionView::from).collect();
|
||||
let roaster_options = load_roaster_options(&state).await.map_err(map_app_error)?;
|
||||
|
||||
let BagPageData {
|
||||
open_bags,
|
||||
|
|
|
|||
|
|
@ -4,7 +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 super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer};
|
||||
use crate::application::auth::AuthenticatedUser;
|
||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
|
|
@ -177,22 +177,13 @@ define_delete_handler!(
|
|||
render_cafe_list_fragment
|
||||
);
|
||||
|
||||
async fn render_cafe_list_fragment(
|
||||
state: AppState,
|
||||
request: ListRequest<CafeSortKey>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let (cafes, navigator) = load_cafe_page(&state, request, search.as_deref()).await?;
|
||||
|
||||
let template = CafeListTemplate {
|
||||
is_authenticated,
|
||||
cafes,
|
||||
navigator,
|
||||
};
|
||||
|
||||
crate::application::routes::support::render_fragment(template, "#cafe-list")
|
||||
}
|
||||
define_list_fragment_renderer!(
|
||||
render_cafe_list_fragment,
|
||||
CafeSortKey,
|
||||
load_cafe_page,
|
||||
CafeListTemplate { cafes },
|
||||
"#cafe-list"
|
||||
);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NearbyQuery {
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@ use axum::extract::{Path, Query, State};
|
|||
use axum::http::{HeaderMap, StatusCode};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
|
||||
use super::macros::{define_delete_handler, define_enriched_get_handler};
|
||||
use super::macros::{
|
||||
define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::auth::AuthenticatedUser;
|
||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, load_cafe_options,
|
||||
load_roast_options,
|
||||
};
|
||||
use crate::application::server::AppState;
|
||||
use crate::domain::cafes::CafeSortKey;
|
||||
use crate::domain::cups::{Cup, CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::ids::CupId;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::presentation::web::templates::{CupListTemplate, CupsTemplate};
|
||||
use crate::presentation::web::views::{
|
||||
CafeOptionView, CupView, ListNavigator, Paginated, RoastOptionView,
|
||||
};
|
||||
use crate::presentation::web::views::{CupView, ListNavigator, Paginated};
|
||||
|
||||
const CUP_PAGE_PATH: &str = "/cups";
|
||||
const CUP_FRAGMENT_PATH: &str = "/cups#cup-list";
|
||||
|
|
@ -67,23 +67,9 @@ pub(crate) async fn cups_page(
|
|||
|
||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||
|
||||
let roast_options: Vec<RoastOptionView> = state
|
||||
.roast_repo
|
||||
.list_all()
|
||||
.await
|
||||
.map_err(|e| map_app_error(AppError::from(e)))?
|
||||
.into_iter()
|
||||
.map(RoastOptionView::from)
|
||||
.collect();
|
||||
let roast_options = load_roast_options(&state).await.map_err(map_app_error)?;
|
||||
|
||||
let cafe_options: Vec<CafeOptionView> = state
|
||||
.cafe_repo
|
||||
.list_all_sorted(CafeSortKey::Name, SortDirection::Asc)
|
||||
.await
|
||||
.map_err(|e| map_app_error(AppError::from(e)))?
|
||||
.into_iter()
|
||||
.map(CafeOptionView::from)
|
||||
.collect();
|
||||
let cafe_options = load_cafe_options(&state).await.map_err(map_app_error)?;
|
||||
|
||||
let template = CupsTemplate {
|
||||
nav_active: "cups",
|
||||
|
|
@ -183,19 +169,10 @@ define_delete_handler!(
|
|||
render_cup_list_fragment
|
||||
);
|
||||
|
||||
async fn render_cup_list_fragment(
|
||||
state: AppState,
|
||||
request: ListRequest<CupSortKey>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let (cups, navigator) = load_cup_page(&state, request, search.as_deref()).await?;
|
||||
|
||||
let template = CupListTemplate {
|
||||
is_authenticated,
|
||||
cups,
|
||||
navigator,
|
||||
};
|
||||
|
||||
crate::application::routes::support::render_fragment(template, "#cup-list")
|
||||
}
|
||||
define_list_fragment_renderer!(
|
||||
render_cup_list_fragment,
|
||||
CupSortKey,
|
||||
load_cup_page,
|
||||
CupListTemplate { cups },
|
||||
"#cup-list"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,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 super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer};
|
||||
use crate::application::auth::AuthenticatedUser;
|
||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
|
|
@ -29,23 +29,21 @@ async fn load_gear_page(
|
|||
state: &AppState,
|
||||
request: ListRequest<GearSortKey>,
|
||||
search: Option<&str>,
|
||||
) -> Result<Paginated<GearView>, AppError> {
|
||||
) -> Result<(Paginated<GearView>, ListNavigator<GearSortKey>), AppError> {
|
||||
let page = state
|
||||
.gear_repo
|
||||
.list(GearFilter::all(), &request, search)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
let (gear, _navigator) = crate::application::routes::support::build_page_view(
|
||||
Ok(crate::application::routes::support::build_page_view(
|
||||
page,
|
||||
request,
|
||||
GearView::from_domain,
|
||||
GEAR_PAGE_PATH,
|
||||
GEAR_FRAGMENT_PATH,
|
||||
search.map(String::from),
|
||||
);
|
||||
|
||||
Ok(gear)
|
||||
))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, cookies, headers, query))]
|
||||
|
|
@ -64,12 +62,11 @@ pub(crate) async fn gear_page(
|
|||
.map_err(map_app_error);
|
||||
}
|
||||
|
||||
let gear = load_gear_page(&state, request, search.as_deref())
|
||||
let (gear, navigator) = load_gear_page(&state, request, search.as_deref())
|
||||
.await
|
||||
.map_err(map_app_error)?;
|
||||
|
||||
let is_authenticated = super::is_authenticated(&state, &cookies).await;
|
||||
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search);
|
||||
|
||||
let template = GearTemplate {
|
||||
nav_active: "gear",
|
||||
|
|
@ -231,20 +228,10 @@ impl NewGearSubmission {
|
|||
}
|
||||
}
|
||||
|
||||
async fn render_gear_list_fragment(
|
||||
state: AppState,
|
||||
request: ListRequest<GearSortKey>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let gear = load_gear_page(&state, request, search.as_deref()).await?;
|
||||
let navigator = ListNavigator::new(GEAR_PAGE_PATH, GEAR_FRAGMENT_PATH, request, search);
|
||||
|
||||
let template = GearListTemplate {
|
||||
is_authenticated,
|
||||
gear,
|
||||
navigator,
|
||||
};
|
||||
|
||||
crate::application::routes::support::render_fragment(template, "#gear-list")
|
||||
}
|
||||
define_list_fragment_renderer!(
|
||||
render_gear_list_fragment,
|
||||
GearSortKey,
|
||||
load_gear_page,
|
||||
GearListTemplate { gear },
|
||||
"#gear-list"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -106,6 +106,48 @@ macro_rules! define_delete_handler {
|
|||
};
|
||||
}
|
||||
|
||||
/// Generates a list-fragment renderer for Datastar partial updates.
|
||||
///
|
||||
/// Produces a function that loads a page via `$loader`, builds the given list
|
||||
/// template, and returns it as a Datastar fragment targeting `$selector`.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `$fn_name` - Name of the generated function
|
||||
/// * `$sort_key` - Sort key type (e.g., `RoasterSortKey`)
|
||||
/// * `$loader` - Page-loader function returning `(Paginated<V>, ListNavigator<K>)`
|
||||
/// * `$template { $field }` - List template type and its items field name
|
||||
/// * `$selector` - CSS selector for Datastar patching (e.g., `"#roaster-list"`)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// define_list_fragment_renderer!(
|
||||
/// render_roaster_list_fragment,
|
||||
/// RoasterSortKey,
|
||||
/// load_roaster_page,
|
||||
/// RoasterListTemplate { roasters },
|
||||
/// "#roaster-list"
|
||||
/// );
|
||||
/// ```
|
||||
macro_rules! define_list_fragment_renderer {
|
||||
($fn_name:ident, $sort_key:ty, $loader:ident, $template:ident { $field:ident }, $selector:literal) => {
|
||||
async fn $fn_name(
|
||||
state: crate::application::server::AppState,
|
||||
request: crate::domain::listing::ListRequest<$sort_key>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<axum::response::Response, crate::application::errors::AppError> {
|
||||
let (items, navigator) = $loader(&state, request, search.as_deref()).await?;
|
||||
let template = $template {
|
||||
is_authenticated,
|
||||
$field: items,
|
||||
navigator,
|
||||
};
|
||||
crate::application::routes::support::render_fragment(template, $selector)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(super) use define_delete_handler;
|
||||
pub(super) use define_enriched_get_handler;
|
||||
pub(super) use define_get_handler;
|
||||
pub(super) use define_list_fragment_renderer;
|
||||
|
|
|
|||
|
|
@ -3,7 +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 super::macros::{define_delete_handler, define_get_handler, define_list_fragment_renderer};
|
||||
use crate::application::auth::AuthenticatedUser;
|
||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
|
|
@ -183,19 +183,10 @@ define_delete_handler!(
|
|||
render_roaster_list_fragment
|
||||
);
|
||||
|
||||
async fn render_roaster_list_fragment(
|
||||
state: AppState,
|
||||
request: ListRequest<RoasterSortKey>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let (roasters, navigator) = load_roaster_page(&state, request, search.as_deref()).await?;
|
||||
|
||||
let template = RoasterListTemplate {
|
||||
is_authenticated,
|
||||
roasters,
|
||||
navigator,
|
||||
};
|
||||
|
||||
crate::application::routes::support::render_fragment(template, "#roaster-list")
|
||||
}
|
||||
define_list_fragment_renderer!(
|
||||
render_roaster_list_fragment,
|
||||
RoasterSortKey,
|
||||
load_roaster_page,
|
||||
RoasterListTemplate { roasters },
|
||||
"#roaster-list"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,23 +4,24 @@ use axum::http::{HeaderMap, StatusCode};
|
|||
use axum::response::{Html, IntoResponse, Redirect, Response};
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::macros::{define_delete_handler, define_enriched_get_handler};
|
||||
use super::macros::{
|
||||
define_delete_handler, define_enriched_get_handler, define_list_fragment_renderer,
|
||||
};
|
||||
use crate::application::auth::AuthenticatedUser;
|
||||
use crate::application::errors::{ApiError, AppError, map_app_error};
|
||||
use crate::application::routes::render_html;
|
||||
use crate::application::routes::support::{
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request, load_roaster_options,
|
||||
};
|
||||
use crate::application::server::AppState;
|
||||
use crate::domain::bags::{BagFilter, BagSortKey};
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::domain::roasters::RoasterSortKey;
|
||||
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
||||
use crate::presentation::web::templates::{
|
||||
RoastDetailTemplate, RoastListTemplate, RoastOptionsTemplate, RoastsTemplate,
|
||||
};
|
||||
use crate::presentation::web::views::{ListNavigator, Paginated, RoastView, RoasterOptionView};
|
||||
use crate::presentation::web::views::{ListNavigator, Paginated, RoastView};
|
||||
|
||||
const ROAST_PAGE_PATH: &str = "/roasts";
|
||||
const ROAST_FRAGMENT_PATH: &str = "/roasts#roast-list";
|
||||
|
|
@ -63,13 +64,7 @@ pub(crate) async fn roasts_page(
|
|||
.map_err(map_app_error);
|
||||
}
|
||||
|
||||
let roasters = state
|
||||
.roaster_repo
|
||||
.list_all_sorted(RoasterSortKey::Name, SortDirection::Asc)
|
||||
.await
|
||||
.map_err(|err| map_app_error(AppError::from(err)))?;
|
||||
|
||||
let roaster_options = roasters.into_iter().map(RoasterOptionView::from).collect();
|
||||
let roaster_options = load_roaster_options(&state).await.map_err(map_app_error)?;
|
||||
|
||||
let (roasts, navigator) = load_roast_page(&state, request, search.as_deref())
|
||||
.await
|
||||
|
|
@ -343,19 +338,10 @@ impl TastingNotesInput {
|
|||
}
|
||||
}
|
||||
|
||||
async fn render_roast_list_fragment(
|
||||
state: AppState,
|
||||
request: ListRequest<RoastSortKey>,
|
||||
search: Option<String>,
|
||||
is_authenticated: bool,
|
||||
) -> Result<Response, AppError> {
|
||||
let (roasts, navigator) = load_roast_page(&state, request, search.as_deref()).await?;
|
||||
|
||||
let template = RoastListTemplate {
|
||||
is_authenticated,
|
||||
roasts,
|
||||
navigator,
|
||||
};
|
||||
|
||||
crate::application::routes::support::render_fragment(template, "#roast-list")
|
||||
}
|
||||
define_list_fragment_renderer!(
|
||||
render_roast_list_fragment,
|
||||
RoastSortKey,
|
||||
load_roast_page,
|
||||
RoastListTemplate { roasts },
|
||||
"#roast-list"
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ use axum::response::{Html, IntoResponse, Response};
|
|||
use serde::Deserialize;
|
||||
|
||||
use crate::application::errors::{ApiError, AppError};
|
||||
use crate::application::server::AppState;
|
||||
use crate::domain::listing::{
|
||||
DEFAULT_PAGE_SIZE, ListRequest, Page, PageSize, SortDirection, SortKey,
|
||||
};
|
||||
use crate::presentation::web::views::{ListNavigator, Paginated};
|
||||
use crate::presentation::web::views::{
|
||||
CafeOptionView, ListNavigator, Paginated, RoastOptionView, RoasterOptionView,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum PayloadSource {
|
||||
|
|
@ -202,6 +205,33 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) async fn load_roaster_options(
|
||||
state: &AppState,
|
||||
) -> Result<Vec<RoasterOptionView>, AppError> {
|
||||
use crate::domain::roasters::RoasterSortKey;
|
||||
let roasters = state
|
||||
.roaster_repo
|
||||
.list_all_sorted(RoasterSortKey::Name, SortDirection::Asc)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(roasters.into_iter().map(RoasterOptionView::from).collect())
|
||||
}
|
||||
|
||||
pub(super) async fn load_roast_options(state: &AppState) -> Result<Vec<RoastOptionView>, AppError> {
|
||||
let roasts = state.roast_repo.list_all().await.map_err(AppError::from)?;
|
||||
Ok(roasts.into_iter().map(RoastOptionView::from).collect())
|
||||
}
|
||||
|
||||
pub(super) async fn load_cafe_options(state: &AppState) -> Result<Vec<CafeOptionView>, AppError> {
|
||||
use crate::domain::cafes::CafeSortKey;
|
||||
let cafes = state
|
||||
.cafe_repo
|
||||
.list_all_sorted(CafeSortKey::Name, SortDirection::Asc)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
Ok(cafes.into_iter().map(CafeOptionView::from).collect())
|
||||
}
|
||||
|
||||
pub fn is_datastar_request(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get("datastar-request")
|
||||
|
|
|
|||
Loading…
Reference in a new issue