chore: refactored server/mod.rs
188d345b
2 file(s) · +143 −141
| 137 | 137 | }; |
|
| 138 | 138 | ||
| 139 | 139 | let input = db::WineInput { |
|
| 140 | - | name: &data.name, |
|
| 141 | - | origin: &data.origin, |
|
| 142 | - | grape: &data.grape, |
|
| 143 | - | notes: &data.notes, |
|
| 144 | - | sweetness: data.sweetness, |
|
| 145 | - | acidity: data.acidity, |
|
| 146 | - | tannin: data.tannin, |
|
| 147 | - | alcohol: data.alcohol, |
|
| 148 | - | body: data.body, |
|
| 149 | - | clarity: data.clarity, |
|
| 150 | - | color_intensity: data.color_intensity, |
|
| 151 | - | aroma_intensity: data.aroma_intensity, |
|
| 152 | - | nose_complexity: data.nose_complexity, |
|
| 153 | - | background: &data.background, |
|
| 140 | + | name: &data.base.name, |
|
| 141 | + | origin: &data.base.origin, |
|
| 142 | + | grape: &data.base.grape, |
|
| 143 | + | notes: &data.base.notes, |
|
| 144 | + | sweetness: data.scores.sweetness, |
|
| 145 | + | acidity: data.scores.acidity, |
|
| 146 | + | tannin: data.scores.tannin, |
|
| 147 | + | alcohol: data.scores.alcohol, |
|
| 148 | + | body: data.scores.body, |
|
| 149 | + | clarity: data.scores.clarity, |
|
| 150 | + | color_intensity: data.scores.color_intensity, |
|
| 151 | + | aroma_intensity: data.scores.aroma_intensity, |
|
| 152 | + | nose_complexity: data.scores.nose_complexity, |
|
| 153 | + | background: &data.base.background, |
|
| 154 | 154 | }; |
|
| 155 | 155 | match db::create_wine(&state.db, &input, false) { |
|
| 156 | 156 | Ok(wine) => { |
|
| 157 | - | if let (Some(image), Some(mime)) = (&data.image, &data.image_mime) { |
|
| 157 | + | if let (Some(image), Some(mime)) = (&data.base.image, &data.base.image_mime) { |
|
| 158 | 158 | if let Err(e) = db::update_wine_image(&state.db, &wine.short_id, image, mime) { |
|
| 159 | 159 | tracing::error!("Failed to set wine image: {}", e); |
|
| 160 | 160 | } |
|
| 187 | 187 | }; |
|
| 188 | 188 | ||
| 189 | 189 | let input = db::WineInput { |
|
| 190 | - | name: &data.name, |
|
| 191 | - | origin: &data.origin, |
|
| 192 | - | grape: &data.grape, |
|
| 193 | - | notes: &data.notes, |
|
| 194 | - | sweetness: data.sweetness, |
|
| 195 | - | acidity: data.acidity, |
|
| 196 | - | tannin: data.tannin, |
|
| 197 | - | alcohol: data.alcohol, |
|
| 198 | - | body: data.body, |
|
| 199 | - | clarity: data.clarity, |
|
| 200 | - | color_intensity: data.color_intensity, |
|
| 201 | - | aroma_intensity: data.aroma_intensity, |
|
| 202 | - | nose_complexity: data.nose_complexity, |
|
| 203 | - | background: &data.background, |
|
| 190 | + | name: &data.base.name, |
|
| 191 | + | origin: &data.base.origin, |
|
| 192 | + | grape: &data.base.grape, |
|
| 193 | + | notes: &data.base.notes, |
|
| 194 | + | sweetness: data.scores.sweetness, |
|
| 195 | + | acidity: data.scores.acidity, |
|
| 196 | + | tannin: data.scores.tannin, |
|
| 197 | + | alcohol: data.scores.alcohol, |
|
| 198 | + | body: data.scores.body, |
|
| 199 | + | clarity: data.scores.clarity, |
|
| 200 | + | color_intensity: data.scores.color_intensity, |
|
| 201 | + | aroma_intensity: data.scores.aroma_intensity, |
|
| 202 | + | nose_complexity: data.scores.nose_complexity, |
|
| 203 | + | background: &data.base.background, |
|
| 204 | 204 | }; |
|
| 205 | 205 | match db::update_wine(&state.db, &short_id, &input) { |
|
| 206 | 206 | Ok(Some(_)) => { |
|
| 207 | - | if let Some(image) = &data.image { |
|
| 208 | - | if let Some(mime) = &data.image_mime { |
|
| 207 | + | if let Some(image) = &data.base.image { |
|
| 208 | + | if let Some(mime) = &data.base.image_mime { |
|
| 209 | 209 | if let Err(e) = db::update_wine_image(&state.db, &short_id, image, mime) { |
|
| 210 | 210 | tracing::error!("Failed to update wine image: {}", e); |
|
| 211 | 211 | } |
|
| 1 | 1 | use askama::Template; |
|
| 2 | 2 | use axum::{ |
|
| 3 | - | extract::{DefaultBodyLimit, Multipart}, |
|
| 3 | + | extract::{multipart::Field, DefaultBodyLimit, Multipart}, |
|
| 4 | 4 | routing::{get, post}, |
|
| 5 | 5 | Router, |
|
| 6 | 6 | }; |
|
| 320 | 320 | ||
| 321 | 321 | // --- Multipart parsing --- |
|
| 322 | 322 | ||
| 323 | - | struct WineFormData { |
|
| 323 | + | #[derive(Default)] |
|
| 324 | + | struct WineBase { |
|
| 324 | 325 | name: String, |
|
| 325 | 326 | origin: String, |
|
| 326 | 327 | grape: String, |
|
| 328 | 329 | background: String, |
|
| 329 | 330 | image: Option<Vec<u8>>, |
|
| 330 | 331 | image_mime: Option<String>, |
|
| 332 | + | } |
|
| 333 | + | ||
| 334 | + | impl WineBase { |
|
| 335 | + | fn owns(field_name: &str) -> bool { |
|
| 336 | + | matches!( |
|
| 337 | + | field_name, |
|
| 338 | + | "image" | "name" | "origin" | "grape" | "notes" | "background" |
|
| 339 | + | ) |
|
| 340 | + | } |
|
| 341 | + | ||
| 342 | + | async fn apply_field(&mut self, field_name: &str, field: Field<'_>) -> Result<(), String> { |
|
| 343 | + | match field_name { |
|
| 344 | + | "image" => { |
|
| 345 | + | let bytes = field.bytes().await.map_err(|e| format!("Failed to read image: {}", e))?; |
|
| 346 | + | if !bytes.is_empty() { |
|
| 347 | + | self.image = Some(process_image(&bytes)?); |
|
| 348 | + | self.image_mime = Some("image/jpeg".to_string()); |
|
| 349 | + | } |
|
| 350 | + | } |
|
| 351 | + | "name" => self.name = field.text().await.unwrap_or_default(), |
|
| 352 | + | "origin" => self.origin = field.text().await.unwrap_or_default(), |
|
| 353 | + | "grape" => self.grape = field.text().await.unwrap_or_default(), |
|
| 354 | + | "notes" => self.notes = field.text().await.unwrap_or_default(), |
|
| 355 | + | "background" => self.background = field.text().await.unwrap_or_default(), |
|
| 356 | + | _ => {} |
|
| 357 | + | } |
|
| 358 | + | Ok(()) |
|
| 359 | + | } |
|
| 360 | + | ||
| 361 | + | fn finalize(&mut self) -> Result<(), String> { |
|
| 362 | + | if self.name.trim().is_empty() { |
|
| 363 | + | return Err("Name is required".to_string()); |
|
| 364 | + | } |
|
| 365 | + | self.name = self.name.trim().to_string(); |
|
| 366 | + | self.origin = self.origin.trim().to_string(); |
|
| 367 | + | self.grape = self.grape.trim().to_string(); |
|
| 368 | + | self.notes = self.notes.trim().to_string(); |
|
| 369 | + | self.background = self.background.trim().to_string(); |
|
| 370 | + | Ok(()) |
|
| 371 | + | } |
|
| 372 | + | } |
|
| 373 | + | ||
| 374 | + | type WishlistFormData = WineBase; |
|
| 375 | + | ||
| 376 | + | struct WineScores { |
|
| 331 | 377 | sweetness: i32, |
|
| 332 | 378 | acidity: i32, |
|
| 333 | 379 | tannin: i32, |
|
| 339 | 385 | nose_complexity: i32, |
|
| 340 | 386 | } |
|
| 341 | 387 | ||
| 342 | - | async fn parse_wine_multipart(mut multipart: Multipart) -> Result<WineFormData, String> { |
|
| 343 | - | let mut name = String::new(); |
|
| 344 | - | let mut origin = String::new(); |
|
| 345 | - | let mut grape = String::new(); |
|
| 346 | - | let mut notes = String::new(); |
|
| 347 | - | let mut background = String::new(); |
|
| 348 | - | let mut image: Option<Vec<u8>> = None; |
|
| 349 | - | let mut image_mime: Option<String> = None; |
|
| 350 | - | let mut sweetness = 3; |
|
| 351 | - | let mut acidity = 3; |
|
| 352 | - | let mut tannin = 3; |
|
| 353 | - | let mut alcohol = 3; |
|
| 354 | - | let mut body = 3; |
|
| 355 | - | let mut clarity = 3; |
|
| 356 | - | let mut color_intensity = 3; |
|
| 357 | - | let mut aroma_intensity = 3; |
|
| 358 | - | let mut nose_complexity = 3; |
|
| 359 | - | ||
| 360 | - | while let Ok(Some(field)) = multipart.next_field().await { |
|
| 361 | - | let field_name = field.name().unwrap_or("").to_string(); |
|
| 362 | - | match field_name.as_str() { |
|
| 363 | - | "image" => { |
|
| 364 | - | let bytes = field.bytes().await.map_err(|e| format!("Failed to read image: {}", e))?; |
|
| 365 | - | if !bytes.is_empty() { |
|
| 366 | - | let processed = process_image(&bytes)?; |
|
| 367 | - | image = Some(processed); |
|
| 368 | - | image_mime = Some("image/jpeg".to_string()); |
|
| 369 | - | } |
|
| 370 | - | } |
|
| 371 | - | "name" => name = field.text().await.unwrap_or_default(), |
|
| 372 | - | "origin" => origin = field.text().await.unwrap_or_default(), |
|
| 373 | - | "grape" => grape = field.text().await.unwrap_or_default(), |
|
| 374 | - | "notes" => notes = field.text().await.unwrap_or_default(), |
|
| 375 | - | "background" => background = field.text().await.unwrap_or_default(), |
|
| 376 | - | "sweetness" => sweetness = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 377 | - | "acidity" => acidity = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 378 | - | "tannin" => tannin = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 379 | - | "alcohol" => alcohol = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 380 | - | "body" => body = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 381 | - | "clarity" => clarity = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 382 | - | "color_intensity" => color_intensity = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 383 | - | "aroma_intensity" => aroma_intensity = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 384 | - | "nose_complexity" => nose_complexity = field.text().await.unwrap_or_default().parse().unwrap_or(3), |
|
| 385 | - | _ => {} |
|
| 388 | + | impl Default for WineScores { |
|
| 389 | + | fn default() -> Self { |
|
| 390 | + | Self { |
|
| 391 | + | sweetness: 3, |
|
| 392 | + | acidity: 3, |
|
| 393 | + | tannin: 3, |
|
| 394 | + | alcohol: 3, |
|
| 395 | + | body: 3, |
|
| 396 | + | clarity: 3, |
|
| 397 | + | color_intensity: 3, |
|
| 398 | + | aroma_intensity: 3, |
|
| 399 | + | nose_complexity: 3, |
|
| 386 | 400 | } |
|
| 387 | 401 | } |
|
| 402 | + | } |
|
| 388 | 403 | ||
| 389 | - | if name.trim().is_empty() { |
|
| 390 | - | return Err("Name is required".to_string()); |
|
| 404 | + | impl WineScores { |
|
| 405 | + | fn slot(&mut self, field_name: &str) -> Option<&mut i32> { |
|
| 406 | + | Some(match field_name { |
|
| 407 | + | "sweetness" => &mut self.sweetness, |
|
| 408 | + | "acidity" => &mut self.acidity, |
|
| 409 | + | "tannin" => &mut self.tannin, |
|
| 410 | + | "alcohol" => &mut self.alcohol, |
|
| 411 | + | "body" => &mut self.body, |
|
| 412 | + | "clarity" => &mut self.clarity, |
|
| 413 | + | "color_intensity" => &mut self.color_intensity, |
|
| 414 | + | "aroma_intensity" => &mut self.aroma_intensity, |
|
| 415 | + | "nose_complexity" => &mut self.nose_complexity, |
|
| 416 | + | _ => return None, |
|
| 417 | + | }) |
|
| 391 | 418 | } |
|
| 392 | 419 | ||
| 393 | - | let clamp = |v: i32| v.max(1).min(5); |
|
| 394 | - | Ok(WineFormData { |
|
| 395 | - | name: name.trim().to_string(), |
|
| 396 | - | origin: origin.trim().to_string(), |
|
| 397 | - | grape: grape.trim().to_string(), |
|
| 398 | - | notes: notes.trim().to_string(), |
|
| 399 | - | background: background.trim().to_string(), |
|
| 400 | - | image, |
|
| 401 | - | image_mime, |
|
| 402 | - | sweetness: clamp(sweetness), |
|
| 403 | - | acidity: clamp(acidity), |
|
| 404 | - | tannin: clamp(tannin), |
|
| 405 | - | alcohol: clamp(alcohol), |
|
| 406 | - | body: clamp(body), |
|
| 407 | - | clarity: clamp(clarity), |
|
| 408 | - | color_intensity: clamp(color_intensity), |
|
| 409 | - | aroma_intensity: clamp(aroma_intensity), |
|
| 410 | - | nose_complexity: clamp(nose_complexity), |
|
| 411 | - | }) |
|
| 420 | + | fn clamp_all(&mut self) { |
|
| 421 | + | for v in [ |
|
| 422 | + | &mut self.sweetness, |
|
| 423 | + | &mut self.acidity, |
|
| 424 | + | &mut self.tannin, |
|
| 425 | + | &mut self.alcohol, |
|
| 426 | + | &mut self.body, |
|
| 427 | + | &mut self.clarity, |
|
| 428 | + | &mut self.color_intensity, |
|
| 429 | + | &mut self.aroma_intensity, |
|
| 430 | + | &mut self.nose_complexity, |
|
| 431 | + | ] { |
|
| 432 | + | *v = (*v).clamp(1, 5); |
|
| 433 | + | } |
|
| 434 | + | } |
|
| 412 | 435 | } |
|
| 413 | 436 | ||
| 414 | - | struct WishlistFormData { |
|
| 415 | - | name: String, |
|
| 416 | - | origin: String, |
|
| 417 | - | grape: String, |
|
| 418 | - | notes: String, |
|
| 419 | - | background: String, |
|
| 420 | - | image: Option<Vec<u8>>, |
|
| 421 | - | image_mime: Option<String>, |
|
| 437 | + | struct WineFormData { |
|
| 438 | + | base: WineBase, |
|
| 439 | + | scores: WineScores, |
|
| 422 | 440 | } |
|
| 423 | 441 | ||
| 424 | - | async fn parse_wishlist_multipart(mut multipart: Multipart) -> Result<WishlistFormData, String> { |
|
| 425 | - | let mut name = String::new(); |
|
| 426 | - | let mut origin = String::new(); |
|
| 427 | - | let mut grape = String::new(); |
|
| 428 | - | let mut notes = String::new(); |
|
| 429 | - | let mut background = String::new(); |
|
| 430 | - | let mut image: Option<Vec<u8>> = None; |
|
| 431 | - | let mut image_mime: Option<String> = None; |
|
| 442 | + | async fn parse_wine_multipart(mut multipart: Multipart) -> Result<WineFormData, String> { |
|
| 443 | + | let mut base = WineBase::default(); |
|
| 444 | + | let mut scores = WineScores::default(); |
|
| 432 | 445 | ||
| 433 | 446 | while let Ok(Some(field)) = multipart.next_field().await { |
|
| 434 | 447 | let field_name = field.name().unwrap_or("").to_string(); |
|
| 435 | - | match field_name.as_str() { |
|
| 436 | - | "image" => { |
|
| 437 | - | let bytes = field.bytes().await.map_err(|e| format!("Failed to read image: {}", e))?; |
|
| 438 | - | if !bytes.is_empty() { |
|
| 439 | - | let processed = process_image(&bytes)?; |
|
| 440 | - | image = Some(processed); |
|
| 441 | - | image_mime = Some("image/jpeg".to_string()); |
|
| 442 | - | } |
|
| 443 | - | } |
|
| 444 | - | "name" => name = field.text().await.unwrap_or_default(), |
|
| 445 | - | "origin" => origin = field.text().await.unwrap_or_default(), |
|
| 446 | - | "grape" => grape = field.text().await.unwrap_or_default(), |
|
| 447 | - | "notes" => notes = field.text().await.unwrap_or_default(), |
|
| 448 | - | "background" => background = field.text().await.unwrap_or_default(), |
|
| 449 | - | _ => {} |
|
| 448 | + | if WineBase::owns(&field_name) { |
|
| 449 | + | base.apply_field(&field_name, field).await?; |
|
| 450 | + | } else if let Some(slot) = scores.slot(&field_name) { |
|
| 451 | + | *slot = field.text().await.unwrap_or_default().parse().unwrap_or(3); |
|
| 450 | 452 | } |
|
| 451 | 453 | } |
|
| 452 | 454 | ||
| 453 | - | if name.trim().is_empty() { |
|
| 454 | - | return Err("Name is required".to_string()); |
|
| 455 | + | base.finalize()?; |
|
| 456 | + | scores.clamp_all(); |
|
| 457 | + | Ok(WineFormData { base, scores }) |
|
| 458 | + | } |
|
| 459 | + | ||
| 460 | + | async fn parse_wishlist_multipart(mut multipart: Multipart) -> Result<WishlistFormData, String> { |
|
| 461 | + | let mut base = WineBase::default(); |
|
| 462 | + | while let Ok(Some(field)) = multipart.next_field().await { |
|
| 463 | + | let field_name = field.name().unwrap_or("").to_string(); |
|
| 464 | + | base.apply_field(&field_name, field).await?; |
|
| 455 | 465 | } |
|
| 456 | - | ||
| 457 | - | Ok(WishlistFormData { |
|
| 458 | - | name: name.trim().to_string(), |
|
| 459 | - | origin: origin.trim().to_string(), |
|
| 460 | - | grape: grape.trim().to_string(), |
|
| 461 | - | notes: notes.trim().to_string(), |
|
| 462 | - | background: background.trim().to_string(), |
|
| 463 | - | image, |
|
| 464 | - | image_mime, |
|
| 465 | - | }) |
|
| 466 | + | base.finalize()?; |
|
| 467 | + | Ok(base) |
|
| 466 | 468 | } |
|
| 467 | 469 | ||
| 468 | 470 | // --- Router --- |
|