feat: upgrade update handlers to FlexiblePayload with three-way response
All five existing update handlers (roaster, roast, cafe, gear, bag) now accept FlexiblePayload with UpdateSubmission types that separate image data from domain structs. Each returns Datastar redirect scripts, form redirects, or JSON depending on request type. Image save support added to all update paths.
This commit is contained in:
parent
77cb61f91b
commit
bc449e520e
6 changed files with 306 additions and 58 deletions
|
|
@ -8,6 +8,7 @@ use tracing::info;
|
||||||
|
|
||||||
use crate::application::auth::AuthenticatedUser;
|
use crate::application::auth::AuthenticatedUser;
|
||||||
use crate::application::errors::{ApiError, AppError};
|
use crate::application::errors::{ApiError, AppError};
|
||||||
|
use crate::application::routes::api::images::save_deferred_image;
|
||||||
use crate::application::routes::api::macros::{define_delete_handler, define_enriched_get_handler};
|
use crate::application::routes::api::macros::{define_delete_handler, define_enriched_get_handler};
|
||||||
use crate::application::routes::support::{
|
use crate::application::routes::support::{
|
||||||
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
FlexiblePayload, ListQuery, PayloadSource, is_datastar_request,
|
||||||
|
|
@ -120,6 +121,41 @@ pub(crate) async fn list_bags(
|
||||||
|
|
||||||
define_enriched_get_handler!(get_bag, BagId, BagWithRoast, bag_repo, get_with_roast);
|
define_enriched_get_handler!(get_bag, BagId, BagWithRoast, bag_repo, get_with_roast);
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateBagSubmission {
|
||||||
|
#[serde(default)]
|
||||||
|
roast_id: Option<RoastId>,
|
||||||
|
#[serde(default)]
|
||||||
|
roast_date: Option<chrono::NaiveDate>,
|
||||||
|
#[serde(default)]
|
||||||
|
amount: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
remaining: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
closed: Option<bool>,
|
||||||
|
#[serde(default)]
|
||||||
|
finished_at: Option<chrono::NaiveDate>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateBagSubmission {
|
||||||
|
fn into_parts(self) -> (UpdateBag, Option<String>) {
|
||||||
|
let update = UpdateBag {
|
||||||
|
roast_id: self.roast_id,
|
||||||
|
roast_date: self.roast_date,
|
||||||
|
amount: self.amount,
|
||||||
|
remaining: self.remaining,
|
||||||
|
closed: self.closed,
|
||||||
|
finished_at: self.finished_at,
|
||||||
|
created_at: self.created_at,
|
||||||
|
};
|
||||||
|
(update, self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[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(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -128,11 +164,11 @@ pub(crate) async fn update_bag(
|
||||||
Path(id): Path<BagId>,
|
Path(id): Path<BagId>,
|
||||||
Query(query): Query<ListQuery>,
|
Query(query): Query<ListQuery>,
|
||||||
Query(update_params): Query<UpdateBag>,
|
Query(update_params): Query<UpdateBag>,
|
||||||
payload: Option<Json<UpdateBag>>,
|
payload: FlexiblePayload<UpdateBagSubmission>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let (request, search) = query.into_request_and_search::<BagSortKey>();
|
let (request, search) = query.into_request_and_search::<BagSortKey>();
|
||||||
|
let (submission, source) = payload.into_parts();
|
||||||
let body_update = payload.map_or(UpdateBag::default(), |Json(p)| p);
|
let (body_update, image_data_url) = submission.into_parts();
|
||||||
|
|
||||||
let update = UpdateBag {
|
let update = UpdateBag {
|
||||||
roast_id: body_update.roast_id.or(update_params.roast_id),
|
roast_id: body_update.roast_id.or(update_params.roast_id),
|
||||||
|
|
@ -161,6 +197,8 @@ pub(crate) async fn update_bag(
|
||||||
info!(%id, closed = ?update.closed, "bag updated");
|
info!(%id, closed = ?update.closed, "bag updated");
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
|
|
||||||
|
save_deferred_image(&state, "bag", i64::from(bag.id), image_data_url.as_deref()).await;
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
let from_bag_page = headers
|
let from_bag_page = headers
|
||||||
.get("referer")
|
.get("referer")
|
||||||
|
|
@ -176,6 +214,9 @@ pub(crate) async fn update_bag(
|
||||||
crate::application::routes::support::render_redirect_script(&detail_url)
|
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
}
|
}
|
||||||
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
|
let detail_url = format!("/bags/{id}");
|
||||||
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
} else {
|
} else {
|
||||||
let enriched = state
|
let enriched = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
|
|
|
||||||
|
|
@ -140,20 +140,60 @@ pub(crate) async fn create_cafe(
|
||||||
|
|
||||||
define_get_handler!(get_cafe, CafeId, Cafe, cafe_repo);
|
define_get_handler!(get_cafe, CafeId, Cafe, cafe_repo);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user))]
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateCafeSubmission {
|
||||||
|
#[serde(default)]
|
||||||
|
name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
city: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
country: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
latitude: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
longitude: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
website: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateCafeSubmission {
|
||||||
|
fn into_parts(self) -> (UpdateCafe, Option<String>) {
|
||||||
|
let update = UpdateCafe {
|
||||||
|
name: self.name,
|
||||||
|
city: self.city,
|
||||||
|
country: self.country,
|
||||||
|
latitude: self.latitude,
|
||||||
|
longitude: self.longitude,
|
||||||
|
website: self.website,
|
||||||
|
created_at: self.created_at,
|
||||||
|
};
|
||||||
|
(update, self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_cafe(
|
pub(crate) async fn update_cafe(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
headers: HeaderMap,
|
||||||
Path(id): Path<CafeId>,
|
Path(id): Path<CafeId>,
|
||||||
Json(payload): Json<UpdateCafe>,
|
payload: FlexiblePayload<UpdateCafeSubmission>,
|
||||||
) -> Result<Json<Cafe>, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let has_changes = payload.name.is_some()
|
let (submission, source) = payload.into_parts();
|
||||||
|| payload.city.is_some()
|
let (update, image_data_url) = submission.into_parts();
|
||||||
|| payload.country.is_some()
|
|
||||||
|| payload.latitude.is_some()
|
let has_changes = update.name.is_some()
|
||||||
|| payload.longitude.is_some()
|
|| update.city.is_some()
|
||||||
|| payload.website.is_some()
|
|| update.country.is_some()
|
||||||
|| payload.created_at.is_some();
|
|| update.latitude.is_some()
|
||||||
|
|| update.longitude.is_some()
|
||||||
|
|| update.website.is_some()
|
||||||
|
|| update.created_at.is_some()
|
||||||
|
|| image_data_url.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
@ -161,12 +201,29 @@ pub(crate) async fn update_cafe(
|
||||||
|
|
||||||
let cafe = state
|
let cafe = state
|
||||||
.cafe_repo
|
.cafe_repo
|
||||||
.update(id, payload)
|
.update(id, update)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
info!(%id, "cafe updated");
|
info!(%id, "cafe updated");
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
Ok(Json(cafe))
|
|
||||||
|
save_deferred_image(
|
||||||
|
&state,
|
||||||
|
"cafe",
|
||||||
|
i64::from(cafe.id),
|
||||||
|
image_data_url.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let detail_url = format!("/cafes/{}", cafe.slug);
|
||||||
|
|
||||||
|
if is_datastar_request(&headers) {
|
||||||
|
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||||
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
|
} else {
|
||||||
|
Ok(Json(cafe).into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_delete_handler!(
|
define_delete_handler!(
|
||||||
|
|
|
||||||
|
|
@ -125,30 +125,72 @@ pub(crate) async fn list_gear(
|
||||||
|
|
||||||
define_get_handler!(get_gear, GearId, Gear, gear_repo);
|
define_get_handler!(get_gear, GearId, Gear, gear_repo);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateGearSubmission {
|
||||||
|
#[serde(default)]
|
||||||
|
make: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
model: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateGearSubmission {
|
||||||
|
fn into_parts(self) -> (UpdateGear, Option<String>) {
|
||||||
|
let update = UpdateGear {
|
||||||
|
make: self.make,
|
||||||
|
model: self.model,
|
||||||
|
created_at: self.created_at,
|
||||||
|
};
|
||||||
|
(update, self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_gear(
|
pub(crate) async fn update_gear(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(id): Path<GearId>,
|
Path(id): Path<GearId>,
|
||||||
Query(query): Query<ListQuery>,
|
payload: FlexiblePayload<UpdateGearSubmission>,
|
||||||
payload: Json<UpdateGear>,
|
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
let (submission, source) = payload.into_parts();
|
||||||
|
let (update, image_data_url) = submission.into_parts();
|
||||||
|
|
||||||
|
let has_changes = update.make.is_some()
|
||||||
|
|| update.model.is_some()
|
||||||
|
|| update.created_at.is_some()
|
||||||
|
|| image_data_url.is_some();
|
||||||
|
|
||||||
|
if !has_changes {
|
||||||
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
}
|
||||||
|
|
||||||
let gear = state
|
let gear = state
|
||||||
.gear_repo
|
.gear_repo
|
||||||
.update(id, payload.0)
|
.update(id, update)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
info!(%id, "gear updated");
|
info!(%id, "gear updated");
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
|
|
||||||
|
save_deferred_image(
|
||||||
|
&state,
|
||||||
|
"gear",
|
||||||
|
i64::from(gear.id),
|
||||||
|
image_data_url.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let detail_url = format!("/gear/{}", gear.id);
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_gear_list_fragment(state, request, search, true)
|
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||||
.await
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
.map_err(ApiError::from)
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(Json(gear).into_response())
|
Ok(Json(gear).into_response())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,19 +139,53 @@ pub(crate) async fn create_roaster(
|
||||||
|
|
||||||
define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo);
|
define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user))]
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateRoasterSubmission {
|
||||||
|
#[serde(default)]
|
||||||
|
name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
country: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
city: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
homepage: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateRoasterSubmission {
|
||||||
|
fn into_parts(self) -> (UpdateRoaster, Option<String>) {
|
||||||
|
let update = UpdateRoaster {
|
||||||
|
name: self.name,
|
||||||
|
country: self.country,
|
||||||
|
city: self.city,
|
||||||
|
homepage: self.homepage,
|
||||||
|
created_at: self.created_at,
|
||||||
|
};
|
||||||
|
(update, self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_roaster(
|
pub(crate) async fn update_roaster(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
headers: HeaderMap,
|
||||||
Path(id): Path<RoasterId>,
|
Path(id): Path<RoasterId>,
|
||||||
Json(payload): Json<UpdateRoaster>,
|
payload: FlexiblePayload<UpdateRoasterSubmission>,
|
||||||
) -> Result<Json<Roaster>, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let payload = payload.normalize();
|
let (submission, source) = payload.into_parts();
|
||||||
let has_changes = payload.name.is_some()
|
let (update, image_data_url) = submission.into_parts();
|
||||||
|| payload.country.is_some()
|
let update = update.normalize();
|
||||||
|| payload.city.is_some()
|
|
||||||
|| payload.homepage.is_some()
|
let has_changes = update.name.is_some()
|
||||||
|| payload.created_at.is_some();
|
|| update.country.is_some()
|
||||||
|
|| update.city.is_some()
|
||||||
|
|| update.homepage.is_some()
|
||||||
|
|| update.created_at.is_some()
|
||||||
|
|| image_data_url.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
@ -159,12 +193,29 @@ pub(crate) async fn update_roaster(
|
||||||
|
|
||||||
let roaster = state
|
let roaster = state
|
||||||
.roaster_repo
|
.roaster_repo
|
||||||
.update(id, payload)
|
.update(id, update)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
info!(%id, "roaster updated");
|
info!(%id, "roaster updated");
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
Ok(Json(roaster))
|
|
||||||
|
save_deferred_image(
|
||||||
|
&state,
|
||||||
|
"roaster",
|
||||||
|
i64::from(roaster.id),
|
||||||
|
image_data_url.as_deref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let detail_url = format!("/roasters/{}", roaster.slug);
|
||||||
|
|
||||||
|
if is_datastar_request(&headers) {
|
||||||
|
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||||
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
|
} else {
|
||||||
|
Ok(Json(roaster).into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
define_delete_handler!(
|
define_delete_handler!(
|
||||||
|
|
|
||||||
|
|
@ -176,21 +176,64 @@ define_delete_handler!(
|
||||||
image_type: "roast"
|
image_type: "roast"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user))]
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateRoastSubmission {
|
||||||
|
#[serde(default)]
|
||||||
|
roaster_id: Option<RoasterId>,
|
||||||
|
#[serde(default)]
|
||||||
|
name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
origin: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
region: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
producer: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
tasting_notes: Option<TastingNotesInput>,
|
||||||
|
#[serde(default)]
|
||||||
|
process: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
created_at: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
image: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateRoastSubmission {
|
||||||
|
fn into_parts(self) -> (UpdateRoast, Option<String>) {
|
||||||
|
let update = UpdateRoast {
|
||||||
|
roaster_id: self.roaster_id,
|
||||||
|
name: self.name,
|
||||||
|
origin: self.origin,
|
||||||
|
region: self.region,
|
||||||
|
producer: self.producer,
|
||||||
|
tasting_notes: self.tasting_notes.map(TastingNotesInput::into_vec),
|
||||||
|
process: self.process,
|
||||||
|
created_at: self.created_at,
|
||||||
|
};
|
||||||
|
(update, self.image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_roast(
|
pub(crate) async fn update_roast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
headers: HeaderMap,
|
||||||
Path(id): Path<RoastId>,
|
Path(id): Path<RoastId>,
|
||||||
Json(payload): Json<UpdateRoast>,
|
payload: FlexiblePayload<UpdateRoastSubmission>,
|
||||||
) -> Result<Json<RoastWithRoaster>, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let has_changes = payload.roaster_id.is_some()
|
let (submission, source) = payload.into_parts();
|
||||||
|| payload.name.is_some()
|
let (update, image_data_url) = submission.into_parts();
|
||||||
|| payload.origin.is_some()
|
|
||||||
|| payload.region.is_some()
|
let has_changes = update.roaster_id.is_some()
|
||||||
|| payload.producer.is_some()
|
|| update.name.is_some()
|
||||||
|| payload.tasting_notes.is_some()
|
|| update.origin.is_some()
|
||||||
|| payload.process.is_some()
|
|| update.region.is_some()
|
||||||
|| payload.created_at.is_some();
|
|| update.producer.is_some()
|
||||||
|
|| update.tasting_notes.is_some()
|
||||||
|
|| update.process.is_some()
|
||||||
|
|| update.created_at.is_some()
|
||||||
|
|| image_data_url.is_some();
|
||||||
|
|
||||||
if !has_changes {
|
if !has_changes {
|
||||||
return Err(AppError::validation("no changes provided").into());
|
return Err(AppError::validation("no changes provided").into());
|
||||||
|
|
@ -198,20 +241,33 @@ pub(crate) async fn update_roast(
|
||||||
|
|
||||||
state
|
state
|
||||||
.roast_repo
|
.roast_repo
|
||||||
.update(id, payload)
|
.update(id, update)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
info!(%id, "roast updated");
|
info!(%id, "roast updated");
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
|
|
||||||
|
save_deferred_image(&state, "roast", i64::from(id), image_data_url.as_deref()).await;
|
||||||
|
|
||||||
let enriched = state
|
let enriched = state
|
||||||
.roast_repo
|
.roast_repo
|
||||||
.get_with_roaster(id)
|
.get_with_roaster(id)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
Ok(Json(enriched))
|
let detail_url = format!(
|
||||||
|
"/roasters/{}/roasts/{}",
|
||||||
|
enriched.roaster_slug, enriched.roast.slug
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_datastar_request(&headers) {
|
||||||
|
render_redirect_script(&detail_url).map_err(ApiError::from)
|
||||||
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
|
} else {
|
||||||
|
Ok(Json(enriched).into_response())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
//! Datastar headers when the `datastar-request: true` header is present.
|
//! Datastar headers when the `datastar-request: true` header is present.
|
||||||
|
|
||||||
use crate::helpers::{
|
use crate::helpers::{
|
||||||
TestApp, assert_datastar_headers, assert_full_page, assert_html_fragment, create_default_bag,
|
TestApp, assert_datastar_headers, assert_datastar_headers_with_mode, assert_full_page,
|
||||||
create_default_cafe, create_default_gear, create_default_roast, create_default_roaster,
|
assert_html_fragment, create_default_bag, create_default_cafe, create_default_gear,
|
||||||
spawn_app_with_auth,
|
create_default_roast, create_default_roaster, spawn_app_with_auth,
|
||||||
};
|
};
|
||||||
use crate::test_macros::define_datastar_entity_tests;
|
use crate::test_macros::define_datastar_entity_tests;
|
||||||
use brewlog::domain::bags::UpdateBag;
|
use brewlog::domain::bags::UpdateBag;
|
||||||
|
|
@ -459,7 +459,7 @@ async fn gear_create_without_datastar_header_returns_json() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn gear_update_with_datastar_header_returns_fragment() {
|
async fn gear_update_with_datastar_header_returns_redirect_script() {
|
||||||
let app = spawn_app_with_auth().await;
|
let app = spawn_app_with_auth().await;
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
|
||||||
|
|
@ -479,11 +479,9 @@ async fn gear_update_with_datastar_header_returns_fragment() {
|
||||||
|
|
||||||
let gear: brewlog::domain::gear::Gear = create_response.json().await.unwrap();
|
let gear: brewlog::domain::gear::Gear = create_response.json().await.unwrap();
|
||||||
|
|
||||||
let update = brewlog::domain::gear::UpdateGear {
|
let update = serde_json::json!({
|
||||||
make: Some("Updated Make".to_string()),
|
"make": "Updated Make",
|
||||||
model: None,
|
});
|
||||||
created_at: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let response = client
|
let response = client
|
||||||
.put(app.api_url(&format!("/gear/{}", gear.id)))
|
.put(app.api_url(&format!("/gear/{}", gear.id)))
|
||||||
|
|
@ -495,10 +493,13 @@ async fn gear_update_with_datastar_header_returns_fragment() {
|
||||||
.expect("failed to update gear");
|
.expect("failed to update gear");
|
||||||
|
|
||||||
assert_eq!(response.status(), 200);
|
assert_eq!(response.status(), 200);
|
||||||
assert_datastar_headers(&response, "#gear-list");
|
assert_datastar_headers_with_mode(&response, "body", "append");
|
||||||
|
|
||||||
let body = response.text().await.expect("failed to read body");
|
let body = response.text().await.expect("failed to read body");
|
||||||
assert_html_fragment(&body);
|
assert!(
|
||||||
|
body.contains("window.location"),
|
||||||
|
"Expected redirect script in body"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue