1use axum::body::Body;
11use axum::http::{StatusCode, header};
12use axum::response::Response;
13
14const FRONTEND_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets");
15
16const EMBEDDED_INDEX: &str = include_str!("../assets/index.html");
17const EMBEDDED_CSS: &str = include_str!("../assets/style.css");
18const EMBEDDED_JS: &str = include_str!("../assets/app.js");
19
20pub fn frontend_dir() -> std::path::PathBuf {
24 std::env::var_os("PHRONA_FRONTEND_DIR")
25 .map(std::path::PathBuf::from)
26 .unwrap_or_else(|| std::path::PathBuf::from(FRONTEND_DIR))
27}
28
29fn embedded_asset(name: &str) -> Option<&'static [u8]> {
30 match name {
31 "index.html" => Some(EMBEDDED_INDEX.as_bytes()),
32 "style.css" => Some(EMBEDDED_CSS.as_bytes()),
33 "app.js" => Some(EMBEDDED_JS.as_bytes()),
34 _ => None,
35 }
36}
37
38fn ok_response(body: Vec<u8>, mime: &'static str) -> Response {
39 Response::builder()
40 .status(StatusCode::OK)
41 .header(header::CONTENT_TYPE, mime)
42 .body(Body::from(body))
43 .unwrap_or_else(|_| Response::new(Body::empty()))
44}
45
46fn serve(name: &str, mime: &'static str) -> Response {
47 match std::fs::read(frontend_dir().join(name)) {
48 Ok(body) => ok_response(body, mime),
49 Err(_) => match embedded_asset(name) {
50 Some(asset) => ok_response(asset.to_vec(), mime),
51 None => Response::builder()
52 .status(StatusCode::NOT_FOUND)
53 .body(Body::from("not found"))
54 .unwrap(),
55 },
56 }
57}
58
59pub async fn index(req: axum::extract::Request) -> Response {
62 let p = req.uri().path().trim_start_matches('/');
63 match p {
64 "" => serve("index.html", "text/html; charset=utf-8"),
65 "style.css" => serve("style.css", "text/css; charset=utf-8"),
66 "app.js" => serve("app.js", "text/javascript; charset=utf-8"),
67 _ => serve("index.html", "text/html; charset=utf-8"),
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn embedded_assets_cover_the_ui() {
77 assert!(!EMBEDDED_INDEX.is_empty());
78 assert!(EMBEDDED_INDEX.contains("<html") || EMBEDDED_INDEX.contains("<!doctype"));
79 assert!(!EMBEDDED_CSS.is_empty());
80 assert!(!EMBEDDED_JS.is_empty());
81 for name in ["index.html", "style.css", "app.js"] {
82 assert!(embedded_asset(name).is_some(), "{name} missing");
83 }
84 assert!(embedded_asset("favicon.ico").is_none());
85 }
86
87 #[tokio::test]
88 async fn fallback_serves_embedded_when_dir_missing() {
89 let dir = frontend_dir();
90 let gone = dir.join("__definitely_missing__");
91 assert!(std::fs::read(&gone).is_err());
92 let resp = serve("app.js", "text/javascript; charset=utf-8");
93 assert_eq!(resp.status(), StatusCode::OK);
94 let body = axum::body::to_bytes(resp.into_body(), 1 << 20)
95 .await
96 .unwrap();
97 assert!(!body.is_empty());
98 assert_eq!(body, EMBEDDED_JS.as_bytes());
99 }
100}