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::domain::bags::{BagFilter, BagSortKey, BagWithRoast, NewBag, UpdateBag};
|
||||
use crate::domain::ids::{BagId, RoastId};
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::presentation::web::templates::BagListTemplate;
|
||||
use crate::presentation::web::views::{BagView, ListNavigator, Paginated};
|
||||
|
|
@ -53,7 +54,7 @@ pub(crate) async fn load_bag_page(
|
|||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -139,7 +140,7 @@ pub(crate) struct UpdateBagSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateBagSubmission {
|
||||
|
|
@ -153,7 +154,7 @@ impl UpdateBagSubmission {
|
|||
finished_at: self.finished_at,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +169,7 @@ impl_has_changes!(
|
|||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use crate::domain::brews::{
|
|||
};
|
||||
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
|
||||
use crate::domain::ids::{BagId, BrewId, GearId};
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
|
||||
use crate::presentation::web::templates::BrewListTemplate;
|
||||
use crate::presentation::web::views::{
|
||||
|
|
@ -198,11 +199,11 @@ pub(crate) struct NewBrewSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
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 {
|
||||
return Err(AppError::validation("coffee weight must be positive"));
|
||||
}
|
||||
|
|
@ -223,23 +224,26 @@ impl NewBrewSubmission {
|
|||
return Err(AppError::validation("brew time must be positive"));
|
||||
}
|
||||
|
||||
Ok(NewBrew {
|
||||
bag_id: self.bag_id,
|
||||
coffee_weight: self.coffee_weight,
|
||||
grinder_id: self.grinder_id,
|
||||
grind_setting: self.grind_setting,
|
||||
brewer_id: self.brewer_id,
|
||||
filter_paper_id: self.filter_paper_id,
|
||||
water_volume: self.water_volume,
|
||||
water_temp: self.water_temp,
|
||||
quick_notes: self.quick_notes,
|
||||
brew_time: self.brew_time,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
Ok((
|
||||
NewBrew {
|
||||
bag_id: self.bag_id,
|
||||
coffee_weight: self.coffee_weight,
|
||||
grinder_id: self.grinder_id,
|
||||
grind_setting: self.grind_setting,
|
||||
brewer_id: self.brewer_id,
|
||||
filter_paper_id: self.filter_paper_id,
|
||||
water_volume: self.water_volume,
|
||||
water_temp: self.water_temp,
|
||||
quick_notes: self.quick_notes,
|
||||
brew_time: self.brew_time,
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -249,8 +253,7 @@ pub(crate) async fn create_brew(
|
|||
) -> Result<Response, ApiError> {
|
||||
let (request, search) = query.into_request_and_search::<BrewSortKey>();
|
||||
let (submission, source) = payload.into_parts();
|
||||
let image_data_url = submission.image.clone();
|
||||
let new_brew = submission.into_new_brew().map_err(ApiError::from)?;
|
||||
let (new_brew, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||
|
||||
let enriched = state
|
||||
.brew_service
|
||||
|
|
@ -350,7 +353,7 @@ pub(crate) struct UpdateBrewSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateBrewSubmission {
|
||||
|
|
@ -372,7 +375,7 @@ impl UpdateBrewSubmission {
|
|||
brew_time: self.brew_time,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -391,7 +394,7 @@ impl_has_changes!(
|
|||
created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||
pub(crate) async fn update_brew(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use crate::application::routes::support::{
|
|||
use crate::application::state::AppState;
|
||||
use crate::domain::cafes::{Cafe, CafeSortKey, NewCafe, UpdateCafe};
|
||||
use crate::domain::ids::CafeId;
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::infrastructure::foursquare;
|
||||
use crate::presentation::web::templates::{CafeListTemplate, NearbyCafesFragment};
|
||||
|
|
@ -71,7 +72,7 @@ pub(crate) struct NewCafeSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl NewCafeSubmission {
|
||||
|
|
@ -85,11 +86,11 @@ impl NewCafeSubmission {
|
|||
website: self.website,
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -159,7 +160,7 @@ pub(crate) struct UpdateCafeSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateCafeSubmission {
|
||||
|
|
@ -173,7 +174,7 @@ impl UpdateCafeSubmission {
|
|||
website: self.website,
|
||||
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
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||
pub(crate) async fn update_cafe(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use crate::application::state::AppState;
|
|||
use crate::domain::cafes::NewCafe;
|
||||
use crate::domain::cups::NewCup;
|
||||
use crate::domain::ids::{CafeId, RoastId};
|
||||
use crate::domain::images::ImageData;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct CheckInSubmission {
|
||||
|
|
@ -33,12 +34,12 @@ pub(crate) struct CheckInSubmission {
|
|||
cafe_website: Option<String>,
|
||||
roast_id: String,
|
||||
#[serde(default)]
|
||||
cafe_image: Option<String>,
|
||||
cafe_image: ImageData,
|
||||
#[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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ use crate::application::routes::support::{
|
|||
use crate::application::state::AppState;
|
||||
use crate::domain::cups::{CupFilter, CupSortKey, CupWithDetails, NewCup, UpdateCup};
|
||||
use crate::domain::ids::{CafeId, CupId, RoastId};
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::presentation::web::templates::CupListTemplate;
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -104,7 +105,7 @@ pub(crate) struct UpdateCupSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateCupSubmission {
|
||||
|
|
@ -114,13 +115,13 @@ impl UpdateCupSubmission {
|
|||
cafe_id: self.cafe_id,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ use crate::application::routes::support::{
|
|||
use crate::application::state::AppState;
|
||||
use crate::domain::gear::{Gear, GearCategory, GearFilter, GearSortKey, NewGear, UpdateGear};
|
||||
use crate::domain::ids::GearId;
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::presentation::web::templates::GearListTemplate;
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -61,8 +62,7 @@ pub(crate) async fn create_gear(
|
|||
) -> Result<Response, ApiError> {
|
||||
let (request, search) = query.into_request_and_search::<GearSortKey>();
|
||||
let (submission, source) = payload.into_parts();
|
||||
let image_data_url = submission.image.clone();
|
||||
let new_gear = submission.into_new_gear().map_err(ApiError::from)?;
|
||||
let (new_gear, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||
|
||||
let gear = state
|
||||
.gear_service
|
||||
|
|
@ -136,7 +136,7 @@ pub(crate) struct UpdateGearSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateGearSubmission {
|
||||
|
|
@ -146,13 +146,13 @@ impl UpdateGearSubmission {
|
|||
model: self.model,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -210,11 +210,11 @@ pub(crate) struct NewGearSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
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)
|
||||
.map_err(|()| AppError::validation("invalid category"))?;
|
||||
|
||||
|
|
@ -226,12 +226,15 @@ impl NewGearSubmission {
|
|||
return Err(AppError::validation("model cannot be empty"));
|
||||
}
|
||||
|
||||
Ok(NewGear {
|
||||
category,
|
||||
make: self.make,
|
||||
model: self.model,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
Ok((
|
||||
NewGear {
|
||||
category,
|
||||
make: self.make,
|
||||
model: self.model,
|
||||
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::domain::ids::RoasterId;
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::domain::roasters::{NewRoaster, Roaster, RoasterSortKey, UpdateRoaster};
|
||||
use crate::infrastructure::ai::{self, ExtractionInput};
|
||||
|
|
@ -72,7 +73,7 @@ pub(crate) struct NewRoasterSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl NewRoasterSubmission {
|
||||
|
|
@ -84,11 +85,11 @@ impl NewRoasterSubmission {
|
|||
homepage: self.homepage,
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -154,7 +155,7 @@ pub(crate) struct UpdateRoasterSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateRoasterSubmission {
|
||||
|
|
@ -166,13 +167,13 @@ impl UpdateRoasterSubmission {
|
|||
homepage: self.homepage,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use crate::application::routes::support::{
|
|||
};
|
||||
use crate::application::state::AppState;
|
||||
use crate::domain::ids::{RoastId, RoasterId};
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::listing::{ListRequest, SortDirection};
|
||||
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
|
||||
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(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -59,8 +60,7 @@ pub(crate) async fn create_roast(
|
|||
) -> Result<Response, ApiError> {
|
||||
let (request, search) = query.into_request_and_search::<RoastSortKey>();
|
||||
let (submission, source) = payload.into_parts();
|
||||
let image_data_url = submission.image.clone();
|
||||
let new_roast = submission.into_new_roast().map_err(ApiError::from)?;
|
||||
let (new_roast, image_data_url) = submission.into_parts().map_err(ApiError::from)?;
|
||||
|
||||
state
|
||||
.roaster_repo
|
||||
|
|
@ -196,7 +196,7 @@ pub(crate) struct UpdateRoastSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
impl UpdateRoastSubmission {
|
||||
|
|
@ -211,7 +211,7 @@ impl UpdateRoastSubmission {
|
|||
process: self.process,
|
||||
created_at: self.created_at,
|
||||
};
|
||||
(update, self.image)
|
||||
(update, self.image.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ impl_has_changes!(
|
|||
created_at
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, payload))]
|
||||
#[tracing::instrument(skip(state, _auth_user, headers))]
|
||||
pub(crate) async fn update_roast(
|
||||
State(state): State<AppState>,
|
||||
_auth_user: AuthenticatedUser,
|
||||
|
|
@ -288,11 +288,11 @@ pub(crate) struct NewRoastSubmission {
|
|||
#[serde(default)]
|
||||
created_at: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
}
|
||||
|
||||
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> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
|
@ -318,16 +318,19 @@ impl NewRoastSubmission {
|
|||
return Err(AppError::validation("tasting notes are required"));
|
||||
}
|
||||
|
||||
Ok(NewRoast {
|
||||
roaster_id,
|
||||
name,
|
||||
origin,
|
||||
region,
|
||||
producer,
|
||||
tasting_notes,
|
||||
process,
|
||||
created_at: self.created_at,
|
||||
})
|
||||
Ok((
|
||||
NewRoast {
|
||||
roaster_id,
|
||||
name,
|
||||
origin,
|
||||
region,
|
||||
producer,
|
||||
tasting_notes,
|
||||
process,
|
||||
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::errors::RepositoryError;
|
||||
use crate::domain::ids::RoastId;
|
||||
use crate::domain::images::ImageData;
|
||||
use crate::domain::roasters::NewRoaster;
|
||||
use crate::domain::roasts::NewRoast;
|
||||
use crate::infrastructure::ai::{self, ExtractionInput, Usage};
|
||||
|
|
@ -158,7 +159,7 @@ fn default_tasting_notes() -> TastingNotesInput {
|
|||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct BagScanSubmission {
|
||||
#[serde(default)]
|
||||
image: Option<String>,
|
||||
image: ImageData,
|
||||
#[serde(default)]
|
||||
prompt: Option<String>,
|
||||
#[serde(default)]
|
||||
|
|
@ -186,7 +187,7 @@ pub(crate) struct BagScanSubmission {
|
|||
#[serde(default)]
|
||||
matched_roast_id: Option<String>,
|
||||
#[serde(default)]
|
||||
scan_image: Option<String>,
|
||||
scan_image: ImageData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -250,7 +251,7 @@ async fn extract_into_submission(
|
|||
}
|
||||
|
||||
#[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(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthenticatedUser,
|
||||
|
|
@ -274,7 +275,7 @@ pub(crate) async fn submit_scan(
|
|||
let scan_image = submission
|
||||
.scan_image
|
||||
.take()
|
||||
.or_else(|| submission.image.clone())
|
||||
.or_else(|| submission.image.cloned())
|
||||
.filter(|s| !s.is_empty());
|
||||
|
||||
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).
|
||||
pub struct EntityImage {
|
||||
pub entity_type: String,
|
||||
|
|
@ -6,3 +10,36 @@ pub struct EntityImage {
|
|||
pub image_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