2.6 KiB · text History aecd85b
use axum::http::StatusCode;
use axum::response::Html;
use tracing::error;

use crate::AppState;

pub mod areas;
pub mod auth;
pub mod cuisines;
pub mod friends;
pub mod lunch;
pub mod members;
pub mod outings;
pub mod places;
pub mod restaurants;

pub use auth::{
    handle_create_invite, handle_login, handle_logout, handle_register, show_invites, show_login,
    show_profile, show_register,
};

/// Render a named template with the given context, mapping failures to a 500
/// plus a log line. Templates are data rather than code, so a typo in one only
/// surfaces here at render time — hence the logging rather than a bare `?`.
pub(crate) fn render(
    state: &AppState,
    name: &str,
    ctx: minijinja::Value,
) -> Result<Html<String>, StatusCode> {
    let template = state.env.get_template(name).map_err(|e| {
        error!(template = name, error = %e, "load template failed");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    let body = template.render(ctx).map_err(|e| {
        error!(template = name, error = %e, "render template failed");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;
    Ok(Html(body))
}

/// Log a database (or other backend) error and turn it into a 500.
pub(crate) fn internal(what: &'static str) -> impl FnOnce(sea_orm::DbErr) -> StatusCode {
    move |e| {
        error!(error = %e, "{what}");
        StatusCode::INTERNAL_SERVER_ERROR
    }
}

/// Trim a form field, treating blank as absent.
pub(crate) fn non_empty(s: Option<String>) -> Option<String> {
    s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}

/// Forms with repeated fields (`attendee=a&attendee=b`) arrive as raw pairs:
/// axum's `Form` can't put repeated keys into a struct, but it can into a list
/// of pairs.
pub(crate) struct Fields(pub Vec<(String, String)>);

impl Fields {
    pub fn one(&self, key: &str) -> Option<&str> {
        self.0.iter().find(|(k, _)| k == key).map(|(_, v)| v.trim()).filter(|v| !v.is_empty())
    }

    /// A time of day, as `<input type=time>` sends it (`12:30`, or with
    /// seconds).
    pub fn time(&self, key: &str) -> Option<chrono::NaiveTime> {
        let v = self.one(key)?;
        chrono::NaiveTime::parse_from_str(v, "%H:%M")
            .or_else(|_| chrono::NaiveTime::parse_from_str(v, "%H:%M:%S"))
            .ok()
    }

    /// Every parseable UUID under `key`, de-duplicated.
    pub fn uuids(&self, key: &str) -> Vec<uuid::Uuid> {
        let set: std::collections::HashSet<uuid::Uuid> =
            self.0.iter().filter(|(k, _)| k == key).filter_map(|(_, v)| v.parse().ok()).collect();
        set.into_iter().collect()
    }
}