Skip to main content

http_cat_tower/
lib.rs

1use std::convert::Infallible;
2
3use axum::{
4    Extension,
5    extract::{Request, rejection::ExtensionRejection},
6    http::HeaderMap,
7    middleware::{self, Next},
8    response::{IntoResponse, Response},
9};
10use maud::DOCTYPE;
11use tower::{Layer, Service, layer::util::Stack};
12
13#[derive(Clone)]
14pub struct AcceptHtml;
15
16async fn axum_htmlify(headers: HeaderMap, mut request: Request, next: Next) -> Response {
17    if let Some(accept) = headers.get("accept")
18        && let Ok(accept) = accept.to_str()
19        && accept.split(',').any(|s| s == "text/html")
20    {
21        request.extensions_mut().insert(AcceptHtml);
22    }
23    next.run(request).await
24}
25
26#[derive(Clone)]
27pub struct NoCats;
28
29async fn axum_catify(
30    extension: Result<Extension<AcceptHtml>, ExtensionRejection>,
31    request: Request,
32    next: Next,
33) -> Response {
34    let html = extension.is_ok();
35    let response = next.run(request).await;
36    if html
37        && response
38            .headers()
39            .get("content-type")
40            .is_none_or(|ty| ty == "text/plain; charset=utf-8")
41        && response.extensions().get::<NoCats>().is_none()
42    {
43        let code = response.status();
44        let number = code.as_u16();
45        let src = format!("https://http.cat/{number}");
46        let mut headers = response.headers().clone();
47        headers.remove("content-type");
48        let html = maud::html! {
49            (DOCTYPE)
50            html {
51                head {
52                    title { (code) }
53                    meta name="viewport" content="width=device-width, initial-scale=1";
54                    meta charset="utf-8";
55                    style {
56                        r#"
57html, body {
58    margin: 0;
59    background: black;
60    display: flex;
61    width: 100vw;
62    height: 100vh;
63}
64
65img {
66    margin: auto;
67}
68"#
69                    }
70                }
71                body {
72                    img src=(src);
73                }
74            }
75        };
76        (code, headers, html).into_response()
77    } else {
78        response
79    }
80}
81
82pub fn htmlify<
83    I: 'static
84        + Send
85        + Sync
86        + Clone
87        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
88>() -> impl 'static
89+ Send
90+ Sync
91+ Clone
92+ Layer<
93    I,
94    Service: 'static
95                 + Send
96                 + Sync
97                 + Clone
98                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
99> {
100    middleware::from_fn(axum_htmlify)
101}
102
103pub fn catify<
104    I: 'static
105        + Send
106        + Sync
107        + Clone
108        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
109>() -> impl 'static
110+ Send
111+ Sync
112+ Clone
113+ Layer<
114    I,
115    Service: 'static
116                 + Send
117                 + Sync
118                 + Clone
119                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
120> {
121    middleware::from_fn(axum_catify)
122}
123
124pub fn htmlify_catify<
125    I: 'static
126        + Send
127        + Sync
128        + Clone
129        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
130>() -> impl 'static
131+ Send
132+ Sync
133+ Clone
134+ Layer<
135    I,
136    Service: 'static
137                 + Send
138                 + Sync
139                 + Clone
140                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
141> {
142    Stack::new(catify(), htmlify())
143}