Use less opaque return types for handlers

This commit is contained in:
Juno Takano 2025-12-12 00:46:14 -03:00
commit 501096c851

View file

@ -1,7 +1,8 @@
use axum::{ use axum::{
body::Body,
extract::Path, extract::Path,
http::{header, StatusCode}, http::{ header, HeaderValue, Response, StatusCode },
response::{IntoResponse, Redirect}, response::{ Redirect },
routing::get, routing::get,
Form, Form,
Router, Router,
@ -36,12 +37,16 @@ async fn main() {
.route("/", get(index).post(query)) .route("/", get(index).post(query))
.route("/graph/toml", get(toml_graph)) .route("/graph/toml", get(toml_graph))
.route("/graph/json", get(json_graph)) .route("/graph/json", get(json_graph))
.route("/static/style.css", get(stylesheet)) .route("/static/style.css", get(
.route("/static/favicon.svg", get(favicon)) || { 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("/node/{node_id}", get(node_view).post(node_view))
.route("/tree", get(tree)) .route("/tree", get(tree))
.route("/about", get(|| static_template_handler("about.html"))) .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) .fallback(not_found)
; ;
@ -61,7 +66,7 @@ async fn main() {
fn make_body( fn make_body(
name: &str, name: &str,
context: &tera::Context, context: &tera::Context,
error_message: Option<&str>, error_message: Option<String>,
) -> (String, u16) { ) -> (String, u16) {
let tera = match tera::Tera::new( 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<Body> {
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( fn template_handler(
name: &str, name: &str,
context: tera::Context, context: &tera::Context,
error_code: u16, error_code: u16,
error_message: Option<String>, error_message: Option<String>,
is_error: bool, is_error: bool,
) -> impl IntoResponse { ) -> Response<Body> {
let (body, render_status) = make_body( let (body, render_status) = make_body(
name, &context, error_message.as_deref()); name, context, error_message);
let status_code = if render_status != 200 { let status_code = if is_error { error_code } else { render_status };
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 };
( make_response(&body, status_code,
status_code, &[(header::CONTENT_TYPE, "text/html")])
[(header::CONTENT_TYPE, "text/html")],
body.clone(),
)
} }
async fn node_view(Path(id): Path<String>) -> impl IntoResponse { async fn node_view(Path(id): Path<String>) -> Response<Body> {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
@ -151,10 +175,11 @@ async fn node_view(Path(id): Path<String>) -> impl IntoResponse {
context.insert("incoming", &graph.incoming.get(&id)); context.insert("incoming", &graph.incoming.get(&id));
let not_found = node.clone() == empty_node; let not_found = node.clone() == empty_node;
let template_name = "node.html".to_string();
template_handler( template_handler(
"node.html", &template_name,
context, &context,
if not_found { 404 } else { 500 }, if not_found { 404 } else { 500 },
Some(format!( Some(format!(
"Failed to generate page for node {} (ID {}).\n\ "Failed to generate page for node {} (ID {}).\n\
@ -165,7 +190,7 @@ async fn node_view(Path(id): Path<String>) -> impl IntoResponse {
) )
} }
async fn index() -> impl IntoResponse { async fn index() -> Response<Body> {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
@ -175,10 +200,10 @@ async fn index() -> impl IntoResponse {
context.insert("nodes", &nodes); context.insert("nodes", &nodes);
context.insert("root_node", &root_node); 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<Body> {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
@ -188,12 +213,38 @@ async fn tree() -> impl IntoResponse {
context.insert("nodes", &nodes); context.insert("nodes", &nodes);
context.insert("root_node", &root_node); 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)] #[expect(clippy::unused_async)]
async fn static_template_handler(name: &str) -> impl IntoResponse { async fn static_template_handler(name: &str) -> Response<Body> {
template_handler(name, tera::Context::new(), 500, None, false) template_handler(name, &tera::Context::new(), 500, None, false)
}
#[expect(clippy::unused_async)]
async fn file_handler(
file_path: &str,
content_type: &str,
) -> Response<Body> {
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)] #[derive(serde::Deserialize)]
@ -203,36 +254,18 @@ async fn query(Form(query): Form<Query>) -> Redirect {
Redirect::permanent(format!("/node/{}", query.node).as_str()) Redirect::permanent(format!("/node/{}", query.node).as_str())
} }
async fn json_graph() -> impl IntoResponse { async fn json_graph() -> Response<Body> {
let graph = populate_graph(); let graph = populate_graph();
let body = serialize_graph(&Format::Json, &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<Body> {
let graph = populate_graph(); let graph = populate_graph();
let body = serialize_graph(&Format::Toml, &graph); let body = serialize_graph(&Format::Toml, &graph);
([(header::CONTENT_TYPE, "text/plain")], body) make_response(&body, 200, &[(header::CONTENT_TYPE, "text/plain")])
}
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)
} }
fn make_error_body( fn make_error_body(
@ -252,28 +285,27 @@ fn make_error_body(
make_body("error.html", &context, Some(&format!( make_body("error.html", &context, Some(&format!(
"Failed to render template for Error {out_code}: {out_message}" "Failed to render template for Error {out_code}: {out_message}"
))).0 )).cloned()).0
} }
fn make_error_response( fn make_error_response(
code: Option<u16>, code: Option<u16>,
message: Option<&str>, message: Option<&str>,
) -> impl IntoResponse { ) -> Response<Body> {
let out_code = code.unwrap_or(500); let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error"); let out_message = &message.unwrap_or("Unknown error");
let body = make_error_body(Some(out_code), Some(out_message)); let body = make_error_body(Some(out_code), Some(out_message));
( make_response(
StatusCode::from_u16(out_code) &body,
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), out_code,
[(header::CONTENT_TYPE, "text/html")], &[(header::CONTENT_TYPE, "text/html")],
body.clone(),
) )
} }
async fn not_found() -> impl IntoResponse { async fn not_found() -> Response<Body> {
make_error_response( make_error_response(
Some(404), Some(404),
Some("The page you tried to access could not be found."), Some("The page you tried to access could not be found."),