feat: log payload fields in trace spans by redacting image data
Add ImageData newtype that wraps Option<String> with a custom Debug impl showing Some(<image>)/None instead of raw base64. Replace image fields on all 14 submission structs and remove payload from tracing skip lists so textual/numeric fields appear in spans.
This commit is contained in:
parent
56a1637d63
commit
4190fc2620
10 changed files with 132 additions and 80 deletions
|
|
@ -17,6 +17,7 @@ use crate::application::routes::support::{
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::bags::{BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
use crate::domain::bags::{BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
||||||
use crate::domain::ids::{BagId, RoastId};
|
use crate::domain::ids::{BagId, RoastId};
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::presentation::web::templates::BagListTemplate;
|
use crate::presentation::web::templates::BagListTemplate;
|
||||||
use crate::presentation::web::views::{BagView, ListNavigator, Paginated};
|
use crate::presentation::web::views::{BagView, ListNavigator, Paginated};
|
||||||
|
|
@ -53,7 +54,7 @@ pub(crate) async fn load_bag_page(
|
||||||
Ok(BagPageData { bags, navigator })
|
Ok(BagPageData { bags, navigator })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_bag(
|
pub(crate) async fn create_bag(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -139,7 +140,7 @@ pub(crate) struct UpdateBagSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateBagSubmission {
|
impl UpdateBagSubmission {
|
||||||
|
|
@ -153,7 +154,7 @@ impl UpdateBagSubmission {
|
||||||
finished_at: self.finished_at,
|
finished_at: self.finished_at,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +169,7 @@ impl_has_changes!(
|
||||||
created_at
|
created_at
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[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>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::domain::brews::{
|
||||||
};
|
};
|
||||||
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
|
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
|
||||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
|
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
|
||||||
use crate::presentation::web::templates::BrewListTemplate;
|
use crate::presentation::web::templates::BrewListTemplate;
|
||||||
use crate::presentation::web::views::{
|
use crate::presentation::web::views::{
|
||||||
|
|
@ -198,11 +199,11 @@ pub(crate) struct NewBrewSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewBrewSubmission {
|
impl NewBrewSubmission {
|
||||||
fn into_new_brew(self) -> Result<NewBrew, AppError> {
|
fn into_parts(self) -> Result<(NewBrew, Option<String>), AppError> {
|
||||||
if self.coffee_weight <= 0.0 {
|
if self.coffee_weight <= 0.0 {
|
||||||
return Err(AppError::validation("coffee weight must be positive"));
|
return Err(AppError::validation("coffee weight must be positive"));
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +224,8 @@ impl NewBrewSubmission {
|
||||||
return Err(AppError::validation("brew time must be positive"));
|
return Err(AppError::validation("brew time must be positive"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(NewBrew {
|
Ok((
|
||||||
|
NewBrew {
|
||||||
bag_id: self.bag_id,
|
bag_id: self.bag_id,
|
||||||
coffee_weight: self.coffee_weight,
|
coffee_weight: self.coffee_weight,
|
||||||
grinder_id: self.grinder_id,
|
grinder_id: self.grinder_id,
|
||||||
|
|
@ -235,11 +237,13 @@ impl NewBrewSubmission {
|
||||||
quick_notes: self.quick_notes,
|
quick_notes: self.quick_notes,
|
||||||
brew_time: self.brew_time,
|
brew_time: self.brew_time,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
})
|
},
|
||||||
|
self.image.into_inner(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_brew(
|
pub(crate) async fn create_brew(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -249,8 +253,7 @@ pub(crate) async fn create_brew(
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let (request, search) = query.into_request_and_search::<BrewSortKey>();
|
let (request, search) = query.into_request_and_search::<BrewSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let image_data_url = submission.image.clone();
|
let (new_brew, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||||
let new_brew = submission.into_new_brew().map_err(ApiError::from)?;
|
|
||||||
|
|
||||||
let enriched = state
|
let enriched = state
|
||||||
.brew_service
|
.brew_service
|
||||||
|
|
@ -350,7 +353,7 @@ pub(crate) struct UpdateBrewSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateBrewSubmission {
|
impl UpdateBrewSubmission {
|
||||||
|
|
@ -372,7 +375,7 @@ impl UpdateBrewSubmission {
|
||||||
brew_time: self.brew_time,
|
brew_time: self.brew_time,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -391,7 +394,7 @@ impl_has_changes!(
|
||||||
created_at
|
created_at
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_brew(
|
pub(crate) async fn update_brew(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use crate::application::routes::support::{
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
|
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
|
||||||
use crate::domain::ids::CafeId;
|
use crate::domain::ids::CafeId;
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::infrastructure::foursquare;
|
use crate::infrastructure::foursquare;
|
||||||
use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment};
|
use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment};
|
||||||
|
|
@ -71,7 +72,7 @@ pub(crate) struct NewCafeSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewCafeSubmission {
|
impl NewCafeSubmission {
|
||||||
|
|
@ -85,11 +86,11 @@ impl NewCafeSubmission {
|
||||||
website: self.website,
|
website: self.website,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(cafe, self.image)
|
(cafe, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_cafe(
|
pub(crate) async fn create_cafe(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -159,7 +160,7 @@ pub(crate) struct UpdateCafeSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateCafeSubmission {
|
impl UpdateCafeSubmission {
|
||||||
|
|
@ -173,7 +174,7 @@ impl UpdateCafeSubmission {
|
||||||
website: self.website,
|
website: self.website,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,7 +182,7 @@ impl_has_changes!(
|
||||||
UpdateCafe, name, city, country, latitude, longitude, website, created_at
|
UpdateCafe, name, city, country, latitude, longitude, website, created_at
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[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,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use crate::application::state::AppState;
|
||||||
use crate::domain::cafes::NewCafe;
|
use crate::domain::cafes::NewCafe;
|
||||||
use crate::domain::cups::NewCup;
|
use crate::domain::cups::NewCup;
|
||||||
use crate::domain::ids::{CafeId, RoastId};
|
use crate::domain::ids::{CafeId, RoastId};
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct CheckInSubmission {
|
pub(crate) struct CheckInSubmission {
|
||||||
|
|
@ -33,12 +34,12 @@ pub(crate) struct CheckInSubmission {
|
||||||
cafe_website: Option<String>,
|
cafe_website: Option<String>,
|
||||||
roast_id: String,
|
roast_id: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
cafe_image: Option<String>,
|
cafe_image: ImageData,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
cup_image: Option<String>,
|
cup_image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn submit_checkin(
|
pub(crate) async fn submit_checkin(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use crate::application::routes::support::{
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::presentation::web::templates::CupListTemplate;
|
use crate::presentation::web::templates::CupListTemplate;
|
||||||
use crate::presentation::web::views::{CupView, ListNavigator, Paginated};
|
use crate::presentation::web::views::{CupView, ListNavigator, Paginated};
|
||||||
|
|
@ -49,7 +50,7 @@ pub(crate) async fn load_cup_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_cup(
|
pub(crate) async fn create_cup(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -104,7 +105,7 @@ pub(crate) struct UpdateCupSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateCupSubmission {
|
impl UpdateCupSubmission {
|
||||||
|
|
@ -114,13 +115,13 @@ impl UpdateCupSubmission {
|
||||||
cafe_id: self.cafe_id,
|
cafe_id: self.cafe_id,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_has_changes!(UpdateCup, roast_id, cafe_id, created_at);
|
impl_has_changes!(UpdateCup, roast_id, cafe_id, created_at);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||||
pub(crate) async fn update_cup(
|
pub(crate) async fn update_cup(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ use crate::application::routes::support::{
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear, UpdateGear};
|
use crate::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear, UpdateGear};
|
||||||
use crate::domain::ids::GearId;
|
use crate::domain::ids::GearId;
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::presentation::web::templates::GearListTemplate;
|
use crate::presentation::web::templates::GearListTemplate;
|
||||||
use crate::presentation::web::views::{GearView, ListNavigator, Paginated};
|
use crate::presentation::web::views::{GearView, ListNavigator, Paginated};
|
||||||
|
|
@ -51,7 +52,7 @@ pub(crate) async fn load_gear_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_gear(
|
pub(crate) async fn create_gear(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -61,8 +62,7 @@ pub(crate) async fn create_gear(
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let image_data_url = submission.image.clone();
|
let (new_gear, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||||
let new_gear = submission.into_new_gear().map_err(ApiError::from)?;
|
|
||||||
|
|
||||||
let gear = state
|
let gear = state
|
||||||
.gear_service
|
.gear_service
|
||||||
|
|
@ -136,7 +136,7 @@ pub(crate) struct UpdateGearSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateGearSubmission {
|
impl UpdateGearSubmission {
|
||||||
|
|
@ -146,13 +146,13 @@ impl UpdateGearSubmission {
|
||||||
model: self.model,
|
model: self.model,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_has_changes!(UpdateGear, make, model, created_at);
|
impl_has_changes!(UpdateGear, make, model, created_at);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[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,
|
||||||
|
|
@ -210,11 +210,11 @@ pub(crate) struct NewGearSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewGearSubmission {
|
impl NewGearSubmission {
|
||||||
fn into_new_gear(self) -> Result<NewGear, AppError> {
|
fn into_parts(self) -> Result<(NewGear, Option<String>), AppError> {
|
||||||
let category = GearCategory::from_str(&self.category)
|
let category = GearCategory::from_str(&self.category)
|
||||||
.map_err(|()| AppError::validation("invalid category"))?;
|
.map_err(|()| AppError::validation("invalid category"))?;
|
||||||
|
|
||||||
|
|
@ -226,12 +226,15 @@ impl NewGearSubmission {
|
||||||
return Err(AppError::validation("model cannot be empty"));
|
return Err(AppError::validation("model cannot be empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(NewGear {
|
Ok((
|
||||||
|
NewGear {
|
||||||
category,
|
category,
|
||||||
make: self.make,
|
make: self.make,
|
||||||
model: self.model,
|
model: self.model,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
})
|
},
|
||||||
|
self.image.into_inner(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::application::routes::support::{
|
||||||
};
|
};
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::ids::RoasterId;
|
use crate::domain::ids::RoasterId;
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster};
|
use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster};
|
||||||
use crate::infrastructure::ai::{self, ExtractionInput};
|
use crate::infrastructure::ai::{self, ExtractionInput};
|
||||||
|
|
@ -72,7 +73,7 @@ pub(crate) struct NewRoasterSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewRoasterSubmission {
|
impl NewRoasterSubmission {
|
||||||
|
|
@ -84,11 +85,11 @@ impl NewRoasterSubmission {
|
||||||
homepage: self.homepage,
|
homepage: self.homepage,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(roaster, self.image)
|
(roaster, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_roaster(
|
pub(crate) async fn create_roaster(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -154,7 +155,7 @@ pub(crate) struct UpdateRoasterSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateRoasterSubmission {
|
impl UpdateRoasterSubmission {
|
||||||
|
|
@ -166,13 +167,13 @@ impl UpdateRoasterSubmission {
|
||||||
homepage: self.homepage,
|
homepage: self.homepage,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_has_changes!(UpdateRoaster, name, country, city, homepage, created_at);
|
impl_has_changes!(UpdateRoaster, name, country, city, homepage, created_at);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[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,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use crate::application::routes::support::{
|
||||||
};
|
};
|
||||||
use crate::application::state::AppState;
|
use crate::application::state::AppState;
|
||||||
use crate::domain::ids::{RoastId, RoasterId};
|
use crate::domain::ids::{RoastId, RoasterId};
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::listing::{ListRequest, SortDirection};
|
use crate::domain::listing::{ListRequest, SortDirection};
|
||||||
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
||||||
use crate::infrastructure::ai::{self, ExtractionInput};
|
use crate::infrastructure::ai::{self, ExtractionInput};
|
||||||
|
|
@ -49,7 +50,7 @@ pub(crate) async fn load_roast_page(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, query, payload))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn create_roast(
|
pub(crate) async fn create_roast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_auth_user: AuthenticatedUser,
|
_auth_user: AuthenticatedUser,
|
||||||
|
|
@ -59,8 +60,7 @@ pub(crate) async fn create_roast(
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let (request, search) = query.into_request_and_search::<RoastSortKey>();
|
let (request, search) = query.into_request_and_search::<RoastSortKey>();
|
||||||
let (submission, source) = payload.into_parts();
|
let (submission, source) = payload.into_parts();
|
||||||
let image_data_url = submission.image.clone();
|
let (new_roast, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||||
let new_roast = submission.into_new_roast().map_err(ApiError::from)?;
|
|
||||||
|
|
||||||
state
|
state
|
||||||
.roaster_repo
|
.roaster_repo
|
||||||
|
|
@ -196,7 +196,7 @@ pub(crate) struct UpdateRoastSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpdateRoastSubmission {
|
impl UpdateRoastSubmission {
|
||||||
|
|
@ -211,7 +211,7 @@ impl UpdateRoastSubmission {
|
||||||
process: self.process,
|
process: self.process,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
};
|
};
|
||||||
(update, self.image)
|
(update, self.image.into_inner())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,7 +227,7 @@ impl_has_changes!(
|
||||||
created_at
|
created_at
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
#[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,
|
||||||
|
|
@ -288,11 +288,11 @@ pub(crate) struct NewRoastSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
created_at: Option<DateTime<Utc>>,
|
created_at: Option<DateTime<Utc>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NewRoastSubmission {
|
impl NewRoastSubmission {
|
||||||
fn into_new_roast(self) -> Result<NewRoast, AppError> {
|
fn into_parts(self) -> Result<(NewRoast, Option<String>), AppError> {
|
||||||
fn require(field: &str, value: String) -> Result<String, AppError> {
|
fn require(field: &str, value: String) -> Result<String, AppError> {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
|
|
@ -318,7 +318,8 @@ impl NewRoastSubmission {
|
||||||
return Err(AppError::validation("tasting notes are required"));
|
return Err(AppError::validation("tasting notes are required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(NewRoast {
|
Ok((
|
||||||
|
NewRoast {
|
||||||
roaster_id,
|
roaster_id,
|
||||||
name,
|
name,
|
||||||
origin,
|
origin,
|
||||||
|
|
@ -327,7 +328,9 @@ impl NewRoastSubmission {
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
process,
|
process,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
})
|
},
|
||||||
|
self.image.into_inner(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ use crate::application::state::AppState;
|
||||||
use crate::domain::bags::NewBag;
|
use crate::domain::bags::NewBag;
|
||||||
use crate::domain::errors::RepositoryError;
|
use crate::domain::errors::RepositoryError;
|
||||||
use crate::domain::ids::RoastId;
|
use crate::domain::ids::RoastId;
|
||||||
|
use crate::domain::images::ImageData;
|
||||||
use crate::domain::roasters::NewRoaster;
|
use crate::domain::roasters::NewRoaster;
|
||||||
use crate::domain::roasts::NewRoast;
|
use crate::domain::roasts::NewRoast;
|
||||||
use crate::infrastructure::ai::{self, ExtractionInput, Usage};
|
use crate::infrastructure::ai::{self, ExtractionInput, Usage};
|
||||||
|
|
@ -158,7 +159,7 @@ fn default_tasting_notes() -> TastingNotesInput {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct BagScanSubmission {
|
pub(crate) struct BagScanSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
image: Option<String>,
|
image: ImageData,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
prompt: Option<String>,
|
prompt: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -186,7 +187,7 @@ pub(crate) struct BagScanSubmission {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
matched_roast_id: Option<String>,
|
matched_roast_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
scan_image: Option<String>,
|
scan_image: ImageData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|
@ -250,7 +251,7 @@ async fn extract_into_submission(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
#[tracing::instrument(skip(state, auth_user, headers, payload))]
|
#[tracing::instrument(skip(state, auth_user, headers))]
|
||||||
pub(crate) async fn submit_scan(
|
pub(crate) async fn submit_scan(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
auth_user: AuthenticatedUser,
|
auth_user: AuthenticatedUser,
|
||||||
|
|
@ -274,7 +275,7 @@ pub(crate) async fn submit_scan(
|
||||||
let scan_image = submission
|
let scan_image = submission
|
||||||
.scan_image
|
.scan_image
|
||||||
.take()
|
.take()
|
||||||
.or_else(|| submission.image.clone())
|
.or_else(|| submission.image.cloned())
|
||||||
.filter(|s| !s.is_empty());
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
if has_raw_input {
|
if has_raw_input {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
/// An image associated with an entity (roaster, roast, gear, or cafe).
|
/// An image associated with an entity (roaster, roast, gear, or cafe).
|
||||||
pub struct EntityImage {
|
pub struct EntityImage {
|
||||||
pub entity_type: String,
|
pub entity_type: String,
|
||||||
|
|
@ -6,3 +10,36 @@ pub struct EntityImage {
|
||||||
pub image_data: Vec<u8>,
|
pub image_data: Vec<u8>,
|
||||||
pub thumbnail_data: Vec<u8>,
|
pub thumbnail_data: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrapper for image data URLs that redacts content in `Debug` output,
|
||||||
|
/// allowing payloads to be traced without logging raw base64 image data.
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ImageData(Option<String>);
|
||||||
|
|
||||||
|
impl ImageData {
|
||||||
|
pub fn into_inner(self) -> Option<String> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_deref(&self) -> Option<&str> {
|
||||||
|
self.0.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take(&mut self) -> Option<String> {
|
||||||
|
self.0.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cloned(&self) -> Option<String> {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for ImageData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match &self.0 {
|
||||||
|
Some(_) => write!(f, "Some(<image>)"),
|
||||||
|
None => write!(f, "None"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue