chore: refactored server/mod.rs 188d345b
Steve · 2026-04-15 19:31 2 file(s) · +143 −141
apps/cellar/src/server/handlers/admin.rs +31 −31
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
                    }
apps/cellar/src/server/mod.rs +112 −110
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 ---