http-cat-tower 0.0.0

Replaces text responses with https://http.cat/
Documentation
use std::convert::Infallible;

use axum::{
    Extension,
    extract::{Request, rejection::ExtensionRejection},
    http::HeaderMap,
    middleware::{self, Next},
    response::{IntoResponse, Response},
};
use maud::DOCTYPE;
use tower::{Layer, Service, layer::util::Stack};

#[derive(Clone)]
pub struct AcceptHtml;

async fn axum_htmlify(headers: HeaderMap, mut request: Request, next: Next) -> Response {
    if let Some(accept) = headers.get("accept")
        && let Ok(accept) = accept.to_str()
        && accept.split(',').any(|s| s == "text/html")
    {
        request.extensions_mut().insert(AcceptHtml);
    }
    next.run(request).await
}

#[derive(Clone)]
pub struct NoCats;

async fn axum_catify(
    extension: Result<Extension<AcceptHtml>, ExtensionRejection>,
    request: Request,
    next: Next,
) -> Response {
    let html = extension.is_ok();
    let response = next.run(request).await;
    if html
        && response
            .headers()
            .get("content-type")
            .is_none_or(|ty| ty == "text/plain; charset=utf-8")
        && response.extensions().get::<NoCats>().is_none()
    {
        let code = response.status();
        let number = code.as_u16();
        let src = format!("https://http.cat/{number}");
        let mut headers = response.headers().clone();
        headers.remove("content-type");
        let html = maud::html! {
            (DOCTYPE)
            html {
                head {
                    title { (code) }
                    meta name="viewport" content="width=device-width, initial-scale=1";
                    meta charset="utf-8";
                    style {
                        r#"
html, body {
    margin: 0;
    background: black;
    display: flex;
    width: 100vw;
    height: 100vh;
}

img {
    margin: auto;
}
"#
                    }
                }
                body {
                    img src=(src);
                }
            }
        };
        let status = response.status();
        (status, headers, html).into_response()
    } else {
        response
    }
}

pub fn htmlify<
    I: 'static
        + Send
        + Sync
        + Clone
        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
>() -> impl 'static
+ Send
+ Sync
+ Clone
+ Layer<
    I,
    Service: 'static
                 + Send
                 + Sync
                 + Clone
                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
> {
    middleware::from_fn(axum_htmlify)
}

pub fn catify<
    I: 'static
        + Send
        + Sync
        + Clone
        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
>() -> impl 'static
+ Send
+ Sync
+ Clone
+ Layer<
    I,
    Service: 'static
                 + Send
                 + Sync
                 + Clone
                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
> {
    middleware::from_fn(axum_catify)
}

pub fn htmlify_catify<
    I: 'static
        + Send
        + Sync
        + Clone
        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
>() -> impl 'static
+ Send
+ Sync
+ Clone
+ Layer<
    I,
    Service: 'static
                 + Send
                 + Sync
                 + Clone
                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
> {
    Stack::new(catify(), htmlify())
}