eclaw 0.0.17

Personal AI assistant powered by Elph
use std::time::{Duration, Instant};

use axum::{
    body::Body,
    http::{Method, Request, StatusCode},
    middleware::Next,
    response::{IntoResponse, Redirect, Response},
};

pub async fn request_logger(request: Request<Body>, next: Next) -> Response {
    let method = request.method().clone();
    let path = request.uri().path().to_owned();
    let query = request.uri().query().unwrap_or("").to_owned();
    let start = Instant::now();
    let response = next.run(request).await;
    log_request(&method, &path, &query, response.status(), start.elapsed());
    response
}

fn request_outcome(status: u16) -> &'static str {
    match status {
        500.. => "failed",
        400..=499 => "rejected",
        _ => "completed",
    }
}

fn log_request(method: &Method, path: &str, query: &str, status: StatusCode, latency: Duration) {
    let status_code = status.as_u16();
    let latency_ms = latency.as_millis() as u64;
    let outcome = request_outcome(status_code);

    match status_code {
        500.. => tracing::error!(
            %method,
            path,
            query,
            status = status_code,
            latency_ms,
            outcome,
            "request failed"
        ),
        400..=499 => tracing::warn!(
            %method,
            path,
            query,
            status = status_code,
            latency_ms,
            outcome,
            "request rejected"
        ),
        _ => tracing::info!(
            %method,
            path,
            query,
            status = status_code,
            latency_ms,
            outcome,
            "request completed"
        ),
    }
}

pub async fn trim_trailing_slash(request: Request<Body>, next: Next) -> Response {
    let path = request.uri().path();
    if path.len() > 1 && path.ends_with('/') {
        let trimmed = &path[..path.len() - 1];
        let location = match request.uri().query() {
            Some(query) => format!("{trimmed}?{query}"),
            None => trimmed.to_string(),
        };
        return Redirect::permanent(&location).into_response();
    }

    next.run(request).await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn maps_status_to_outcome() {
        assert_eq!(request_outcome(200), "completed");
        assert_eq!(request_outcome(404), "rejected");
        assert_eq!(request_outcome(500), "failed");
    }
}