1use std::convert::Infallible;
5
6use askama::Template;
7use axum::extract::FromRequestParts;
8use axum::http::request::Parts;
9use axum::http::{HeaderMap, HeaderName, HeaderValue};
10use axum::response::Html;
11
12use crate::error::AppError;
13
14pub fn html<T: Template>(t: &T) -> Result<Html<String>, AppError> {
16 Ok(Html(t.render()?))
17}
18
19pub fn is_htmx(headers: &HeaderMap) -> bool {
20 headers.get("hx-request").is_some_and(|v| v == "true")
21}
22
23#[derive(Debug, Clone, Default)]
26pub struct Hx {
27 pub request: bool,
28 pub boosted: bool,
29 pub target: Option<String>,
30}
31
32impl Hx {
33 pub fn partial(&self) -> bool {
36 self.request && !self.boosted
37 }
38}
39
40impl<S: Send + Sync> FromRequestParts<S> for Hx {
41 type Rejection = Infallible;
42
43 async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Hx, Infallible> {
44 let h = &parts.headers;
45 Ok(Hx {
46 request: is_htmx(h),
47 boosted: h.get("hx-boosted").is_some_and(|v| v == "true"),
48 target: h
49 .get("hx-target")
50 .and_then(|v| v.to_str().ok())
51 .map(str::to_owned),
52 })
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum ToastKind {
58 Ok,
59 Info,
60 Error,
61}
62
63impl ToastKind {
64 fn as_str(self) -> &'static str {
65 match self {
66 ToastKind::Ok => "ok",
67 ToastKind::Info => "info",
68 ToastKind::Error => "error",
69 }
70 }
71}
72
73pub fn toast(kind: ToastKind, message: &str) -> (HeaderName, HeaderValue) {
76 let payload = serde_json::json!({ "toast": { "kind": kind.as_str(), "message": message } });
77 (
78 HeaderName::from_static("hx-trigger"),
79 HeaderValue::from_str(&payload.to_string())
80 .unwrap_or_else(|_| HeaderValue::from_static("{}")),
81 )
82}
83
84pub fn hx_redirect(to: &str) -> (HeaderName, HeaderValue) {
86 (
87 HeaderName::from_static("hx-redirect"),
88 HeaderValue::from_str(to).unwrap_or_else(|_| HeaderValue::from_static("/")),
89 )
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn toast_header_is_json() {
98 let (name, value) = toast(ToastKind::Ok, "Crawl \"queued\"");
99 assert_eq!(name, "hx-trigger");
100 let v: serde_json::Value = serde_json::from_str(value.to_str().unwrap()).unwrap();
101 assert_eq!(v["toast"]["kind"], "ok");
102 assert_eq!(v["toast"]["message"], "Crawl \"queued\"");
103 }
104}