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        let status = response.status();
77        (status, headers, html).into_response()
78    } else {
79        response
80    }
81}
82
83pub fn htmlify<
84    I: 'static
85        + Send
86        + Sync
87        + Clone
88        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
89>() -> impl 'static
90+ Send
91+ Sync
92+ Clone
93+ Layer<
94    I,
95    Service: 'static
96                 + Send
97                 + Sync
98                 + Clone
99                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
100> {
101    middleware::from_fn(axum_htmlify)
102}
103
104pub fn catify<
105    I: 'static
106        + Send
107        + Sync
108        + Clone
109        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
110>() -> impl 'static
111+ Send
112+ Sync
113+ Clone
114+ Layer<
115    I,
116    Service: 'static
117                 + Send
118                 + Sync
119                 + Clone
120                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
121> {
122    middleware::from_fn(axum_catify)
123}
124
125pub fn htmlify_catify<
126    I: 'static
127        + Send
128        + Sync
129        + Clone
130        + Service<Request, Response: IntoResponse, Error = Infallible, Future: 'static + Send>,
131>() -> impl 'static
132+ Send
133+ Sync
134+ Clone
135+ Layer<
136    I,
137    Service: 'static
138                 + Send
139                 + Sync
140                 + Clone
141                 + Service<Request, Response = Response, Error = Infallible, Future: 'static + Send>,
142> {
143    Stack::new(catify(), htmlify())
144}