diff --git a/src/main.rs b/src/main.rs index 9e236dd..d0a0f35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use axum::{ + body::Body, extract::Path, - http::{header, StatusCode}, - response::{IntoResponse, Redirect}, + http::{ header, HeaderValue, Response, StatusCode }, + response::{ Redirect }, routing::get, Form, Router, @@ -36,12 +37,16 @@ async fn main() { .route("/", get(index).post(query)) .route("/graph/toml", get(toml_graph)) .route("/graph/json", get(json_graph)) - .route("/static/style.css", get(stylesheet)) - .route("/static/favicon.svg", get(favicon)) + .route("/static/style.css", get( + || { file_handler("./static/style.css", "text/css") })) + .route("/static/favicon.svg", get( + || { file_handler("./static/favicon.svg", "image/svg+xml") })) .route("/node/{node_id}", get(node_view).post(node_view)) .route("/tree", get(tree)) .route("/about", get(|| static_template_handler("about.html"))) - .route("/acknowledgments", get(|| static_template_handler("acknowledgments.html"))) + .route("/acknowledgments", get(|| { + static_template_handler("acknowledgments.html") + })) .fallback(not_found) ; @@ -61,7 +66,7 @@ async fn main() { fn make_body( name: &str, context: &tera::Context, - error_message: Option<&str>, + error_message: Option, ) -> (String, u16) { let tera = match tera::Tera::new( @@ -106,33 +111,52 @@ fn make_body( } } +fn make_response( + body: &str, + status_code: u16, + headers: &[(header::HeaderName, &str)] +) -> Response { + + let mut response = Response::new(Body::from(body.to_owned())); + + *response.status_mut() = StatusCode::from_u16(status_code) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + for header in headers { + if let Ok(wrapped) = HeaderValue::from_str(header.1) { + if let Some(overwritten) = response.headers_mut().insert( + header.0.clone(), + wrapped, + ) { eprintln!("[make_response] Overwrote header {overwritten:?} \ + because another for key {} already existed", header.0); + } + } else { + eprintln!("[make_response] Failed to wrap header value {}", + header.1); + } + } + + response +} + fn template_handler( name: &str, - context: tera::Context, + context: &tera::Context, error_code: u16, error_message: Option, is_error: bool, -) -> impl IntoResponse { +) -> Response { let (body, render_status) = make_body( - name, &context, error_message.as_deref()); + name, context, error_message); - let status_code = if render_status != 200 { - StatusCode::from_u16(render_status) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) - } else if is_error { - StatusCode::from_u16(error_code) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) - } else { StatusCode::OK }; + let status_code = if is_error { error_code } else { render_status }; - ( - status_code, - [(header::CONTENT_TYPE, "text/html")], - body.clone(), - ) + make_response(&body, status_code, + &[(header::CONTENT_TYPE, "text/html")]) } -async fn node_view(Path(id): Path) -> impl IntoResponse { +async fn node_view(Path(id): Path) -> Response { let mut context = tera::Context::new(); @@ -151,10 +175,11 @@ async fn node_view(Path(id): Path) -> impl IntoResponse { context.insert("incoming", &graph.incoming.get(&id)); let not_found = node.clone() == empty_node; + let template_name = "node.html".to_string(); template_handler( - "node.html", - context, + &template_name, + &context, if not_found { 404 } else { 500 }, Some(format!( "Failed to generate page for node {} (ID {}).\n\ @@ -165,7 +190,7 @@ async fn node_view(Path(id): Path) -> impl IntoResponse { ) } -async fn index() -> impl IntoResponse { +async fn index() -> Response { let mut context = tera::Context::new(); let graph = populate_graph(); @@ -175,10 +200,10 @@ async fn index() -> impl IntoResponse { context.insert("nodes", &nodes); context.insert("root_node", &root_node); - template_handler("index.html", context.clone(), 500, None, false) + template_handler("index.html", &context, 500, None, false) } -async fn tree() -> impl IntoResponse { +async fn tree() -> Response { let mut context = tera::Context::new(); let graph = populate_graph(); @@ -188,12 +213,38 @@ async fn tree() -> impl IntoResponse { context.insert("nodes", &nodes); context.insert("root_node", &root_node); - template_handler("tree.html", context, 500, None, false) + template_handler("tree.html", &context, 500, None, false) } #[expect(clippy::unused_async)] -async fn static_template_handler(name: &str) -> impl IntoResponse { - template_handler(name, tera::Context::new(), 500, None, false) +async fn static_template_handler(name: &str) -> Response { + template_handler(name, &tera::Context::new(), 500, None, false) +} + +#[expect(clippy::unused_async)] +async fn file_handler( + file_path: &str, + content_type: &str, +) -> Response { + + let content = match std::fs::read(file_path) { + Ok(s) => s, + Err(e) => panic!("[static_file_handler] Failed to read file contents: {e}"), + }; + + let mut response = Response::new(Body::from(content)); + *response.status_mut() = StatusCode::OK; + let header = header::CONTENT_TYPE; + + if let Ok(header_value) = HeaderValue::from_str(content_type) { + if let Some(h) = response.headers_mut().insert(header, header_value) { + eprintln!("[static_file_handler] Overwrote existing header {h:?} \ + because a header for the same key existed"); + } + } else { eprintln!("[static_file_handler] Failed to create content type \ + header value from {content_type}"); } + + response } #[derive(serde::Deserialize)] @@ -203,36 +254,18 @@ async fn query(Form(query): Form) -> Redirect { Redirect::permanent(format!("/node/{}", query.node).as_str()) } -async fn json_graph() -> impl IntoResponse { +async fn json_graph() -> Response { let graph = populate_graph(); let body = serialize_graph(&Format::Json, &graph); - ([(header::CONTENT_TYPE, "application/json")], body) + make_response(&body, 200, &[(header::CONTENT_TYPE, "application/json")]) } -async fn toml_graph() -> impl IntoResponse { +async fn toml_graph() -> Response { let graph = populate_graph(); let body = serialize_graph(&Format::Toml, &graph); - ([(header::CONTENT_TYPE, "text/plain")], body) -} - -async fn stylesheet() -> impl IntoResponse { - let content = match std::fs::read_to_string("./static/style.css") { - Ok(s) => s, - Err(e) => format!("Error: {e}"), - }; - - ([(header::CONTENT_TYPE, "text/css")], content) -} - -async fn favicon() -> impl IntoResponse { - let content = match std::fs::read("./static/favicon.svg") { - Ok(b) => b, - Err(e) => { eprintln!("Error: {e}"); vec![] } - }; - - ([(header::CONTENT_TYPE, "image/svg+xml")], content) + make_response(&body, 200, &[(header::CONTENT_TYPE, "text/plain")]) } fn make_error_body( @@ -252,28 +285,27 @@ fn make_error_body( make_body("error.html", &context, Some(&format!( "Failed to render template for Error {out_code}: {out_message}" - ))).0 + )).cloned()).0 } fn make_error_response( code: Option, message: Option<&str>, -) -> impl IntoResponse { +) -> Response { let out_code = code.unwrap_or(500); let out_message = &message.unwrap_or("Unknown error"); let body = make_error_body(Some(out_code), Some(out_message)); - ( - StatusCode::from_u16(out_code) - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), - [(header::CONTENT_TYPE, "text/html")], - body.clone(), + make_response( + &body, + out_code, + &[(header::CONTENT_TYPE, "text/html")], ) } -async fn not_found() -> impl IntoResponse { +async fn not_found() -> Response { make_error_response( Some(404), Some("The page you tried to access could not be found."),