Heavy refactor and extraction of most code to handler submodules

This commit is contained in:
Juno Takano 2025-12-12 04:13:32 -03:00
commit ab6e90b6b8
9 changed files with 313 additions and 277 deletions

View file

@ -6,7 +6,9 @@ It works by ingesting a TOML file containing your node specification and serving
## Roadmap ## Roadmap
- [ ] Automatic anchors - [ ] Anchor rendering
- [ ] Automatic anchors
- [ ] Connection kinds
- [ ] Reduce O(n) calls in the formats module - [ ] Reduce O(n) calls in the formats module
- [ ] Add tests - [ ] Add tests
- [x] Array syntax for lightweight connections - [x] Array syntax for lightweight connections

6
src/handlers.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod graph;
pub mod template;
pub mod raw;
pub mod navigation;
pub mod fixed;
pub mod error;

53
src/handlers/error.rs Normal file
View file

@ -0,0 +1,53 @@
use axum::{
body::Body,
http::{Response, StatusCode, header},
};
use crate::handlers;
pub fn by_code(code: Option<u16>, message: Option<&str>) -> Response<Body> {
let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error");
let body = make_body(Some(out_code), Some(out_message));
handlers::raw::make_response(
&body,
out_code,
&[(header::CONTENT_TYPE, "text/html")],
)
}
fn make_body(code: Option<u16>, message: Option<&str>) -> String {
let mut context = tera::Context::new();
let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error");
context.insert(
"title",
&StatusCode::from_u16(out_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
.to_string(),
);
context.insert("message", out_message);
context.insert("status_code", &out_code.to_string());
handlers::template::render(
"error.html",
&context,
Some(&format!(
"Failed to render template for Error {out_code}: {out_message}"
))
.cloned(),
)
.0
}
pub async fn not_found() -> Response<Body> {
by_code(
Some(404),
Some("The page you tried to access could not be found."),
)
}

54
src/handlers/fixed.rs Normal file
View file

@ -0,0 +1,54 @@
use axum::{
body::Body,
http::{Response, StatusCode, header, HeaderValue},
};
use crate::formats::{Format, populate_graph, serialize_graph};
use crate::handlers;
pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
let content = match std::fs::read(file_path) {
Ok(s) => s,
Err(e) => {
panic!("[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!(
"[file_handler] Overwrote existing header {h:?} \
because a header for the same key existed"
);
}
} else {
eprintln!(
"[file_handler] Failed to create content type \
header value from {content_type}"
);
}
response
}
pub async fn serial(format: &Format) -> Response<Body> {
let graph = populate_graph();
let body = serialize_graph(format, &graph);
match *format {
Format::Toml => handlers::raw::make_response(
&body,
200,
&[(header::CONTENT_TYPE, "text/plain")],
),
Format::Json => handlers::raw::make_response(
&body,
200,
&[(header::CONTENT_TYPE, "application/json")],
),
}
}

38
src/handlers/graph.rs Normal file
View file

@ -0,0 +1,38 @@
use axum::{body::Body, extract::Path, http::Response};
use crate::{formats::populate_graph, types::Node, handlers};
pub async fn node(Path(id): Path<String>) -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
let nodes = graph.nodes;
let empty_node =
Node::new(Some(format!("Could not find node with ID {id}.")));
let node: &Node = nodes.get(&id).unwrap_or(&empty_node);
context.insert("id", &id);
context.insert("title", &node.title);
context.insert("text", &node.text);
context.insert("connections", &node.connections.clone());
context.insert("incoming", &graph.incoming.get(&id));
let not_found = node.clone() == empty_node;
let template_name = "node.html".to_string();
handlers::template::by_filename(
&template_name,
&context,
if not_found { 404 } else { 500 },
Some(
format!(
"Failed to generate page for node {} (ID {}).\n\
Node struct: <pre>{:#?}</pre>",
node.title, id, node
)
.to_owned(),
),
not_found,
)
}

View file

@ -0,0 +1,29 @@
use axum::{
body::Body,
http::{Response},
response::Redirect,
Form,
};
use crate::{formats::populate_graph, types::Node, handlers};
pub async fn nexus(template: &str) -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
let root_node = graph.get_root().unwrap_or_default();
let nodes: Vec<Node> = graph.nodes.into_values().collect();
context.insert("nodes", &nodes);
context.insert("root_node", &root_node);
handlers::template::by_filename(template, &context, 500, None, false)
}
pub async fn search(Form(query): Form<Query>) -> Redirect {
Redirect::permanent(format!("/node/{}", query.node).as_str())
}
#[derive(serde::Deserialize)]
pub struct Query {
node: String,
}

36
src/handlers/raw.rs Normal file
View file

@ -0,0 +1,36 @@
use axum::{
body::Body,
http::{header, HeaderValue, Response, StatusCode},
};
pub 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
}

77
src/handlers/template.rs Normal file
View file

@ -0,0 +1,77 @@
use axum::{
body::Body,
http::{header, Response, StatusCode},
};
use crate::handlers::raw::make_response;
pub(super) fn by_filename(
name: &str,
context: &tera::Context,
error_code: u16,
error_message: Option<String>,
is_error: bool,
) -> Response<Body> {
let (body, render_status) = render(name, context, error_message);
let status_code = if is_error { error_code } else { render_status };
make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")])
}
pub async fn static_template_handler(name: &str) -> Response<Body> {
by_filename(name, &tera::Context::new(), 500, None, false)
}
pub(super) fn render(
name: &str,
// TODO take Option, skip context if None,
// then template_handler can replace static_template_handler
context: &tera::Context,
error_message: Option<String>,
) -> (String, u16) {
// TODO just return an Option<String> here
let tera = match tera::Tera::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/templates/**/*"
)) {
Ok(t) => t,
Err(e) => {
println!("Tera parsing error: {e:#?}");
panic!("{e}")
},
};
match tera.render(name, context) {
Ok(t) => (t, 200),
Err(e) => {
let mut error_context = tera::Context::new();
let out_error_message = match error_message {
Some(s) => &format!(
"Template render failed.\n\
User message: {s},
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
),
None => &format!(
"Template render failed.\n\
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
),
};
error_context.insert("message", out_error_message);
error_context.insert(
"title",
&StatusCode::INTERNAL_SERVER_ERROR.to_string(),
);
(
tera.render("error.html", &error_context)
.unwrap_or(out_error_message.clone()),
500,
)
},
}
}

View file

@ -1,17 +1,17 @@
use axum::{ use axum::{routing::get, Router};
body::Body,
extract::Path,
http::{header, HeaderValue, Response, StatusCode},
response::{Redirect},
routing::get,
Form, Router,
};
use formats::{populate_graph, serialize_graph, Format}; use handlers::{
use types::Node; graph::node,
navigation::{nexus, search},
fixed::{file, serial},
template::static_template_handler,
error::not_found,
};
use formats::Format;
mod formats; mod formats;
mod types; mod types;
mod handlers;
static ONSET: std::sync::LazyLock<std::time::Instant> = static ONSET: std::sync::LazyLock<std::time::Instant> =
std::sync::LazyLock::new(std::time::Instant::now); std::sync::LazyLock::new(std::time::Instant::now);
@ -32,19 +32,19 @@ async fn main() {
})); }));
let app = Router::new() let app = Router::new()
.route("/", get(index).post(query)) .route("/", get(|| nexus("index.html")).post(search))
.route("/graph/toml", get(toml_graph)) .route("/graph/toml", get(|| serial(&Format::Toml)))
.route("/graph/json", get(json_graph)) .route("/graph/json", get(|| serial(&Format::Json)))
.route( .route(
"/static/style.css", "/static/style.css",
get(|| file_handler("./static/style.css", "text/css")), get(|| file("./static/style.css", "text/css")),
) )
.route( .route(
"/static/favicon.svg", "/static/favicon.svg",
get(|| file_handler("./static/favicon.svg", "image/svg+xml")), get(|| file("./static/favicon.svg", "image/svg+xml")),
) )
.route("/node/{node_id}", get(node_view).post(node_view)) .route("/node/{node_id}", get(node).post(node))
.route("/tree", get(tree)) .route("/tree", get(|| nexus("tree.html")))
.route("/about", get(|| static_template_handler("about.html"))) .route("/about", get(|| static_template_handler("about.html")))
.route( .route(
"/acknowledgments", "/acknowledgments",
@ -67,262 +67,3 @@ async fn main() {
} }
} }
} }
fn make_body(
name: &str,
context: &tera::Context,
error_message: Option<String>,
) -> (String, u16) {
let tera = match tera::Tera::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/templates/**/*"
)) {
Ok(t) => t,
Err(e) => {
println!("Tera parsing error: {e:#?}");
panic!("{e}")
},
};
match tera.render(name, context) {
Ok(t) => (t, 200),
Err(e) => {
let mut error_context = tera::Context::new();
let out_error_message = match error_message {
Some(s) => &format!(
"Template render failed.\n\
User message: {s},
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
),
None => &format!(
"Template render failed.\n\
Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>"
),
};
error_context.insert("message", out_error_message);
error_context.insert(
"title",
&StatusCode::INTERNAL_SERVER_ERROR.to_string(),
);
(
tera.render("error.html", &error_context)
.unwrap_or(out_error_message.clone()),
500,
)
},
}
}
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(
name: &str,
context: &tera::Context,
error_code: u16,
error_message: Option<String>,
is_error: bool,
) -> Response<Body> {
let (body, render_status) = make_body(name, context, error_message);
let status_code = if is_error { error_code } else { render_status };
make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")])
}
async fn node_view(Path(id): Path<String>) -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
let nodes = graph.nodes;
let empty_node =
Node::new(Some(format!("Could not find node with ID {id}.")));
let node: &Node = nodes.get(&id).unwrap_or(&empty_node);
context.insert("id", &id);
context.insert("title", &node.title);
context.insert("text", &node.text);
context.insert("connections", &node.connections.clone());
context.insert("incoming", &graph.incoming.get(&id));
let not_found = node.clone() == empty_node;
let template_name = "node.html".to_string();
template_handler(
&template_name,
&context,
if not_found { 404 } else { 500 },
Some(
format!(
"Failed to generate page for node {} (ID {}).\n\
Node struct: <pre>{:#?}</pre>",
node.title, id, node
)
.to_owned(),
),
not_found,
)
}
async fn index() -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
let root_node = graph.get_root().unwrap_or_default();
let nodes: Vec<Node> = graph.nodes.into_values().collect();
context.insert("nodes", &nodes);
context.insert("root_node", &root_node);
template_handler("index.html", &context, 500, None, false)
}
async fn tree() -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
let root_node = graph.get_root().unwrap_or_default();
let nodes: Vec<Node> = graph.nodes.into_values().collect();
context.insert("nodes", &nodes);
context.insert("root_node", &root_node);
template_handler("tree.html", &context, 500, None, false)
}
#[expect(clippy::unused_async)]
async fn static_template_handler(name: &str) -> Response<Body> {
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)]
struct Query {
node: String,
}
async fn query(Form(query): Form<Query>) -> Redirect {
Redirect::permanent(format!("/node/{}", query.node).as_str())
}
async fn json_graph() -> Response<Body> {
let graph = populate_graph();
let body = serialize_graph(&Format::Json, &graph);
make_response(&body, 200, &[(header::CONTENT_TYPE, "application/json")])
}
async fn toml_graph() -> Response<Body> {
let graph = populate_graph();
let body = serialize_graph(&Format::Toml, &graph);
make_response(&body, 200, &[(header::CONTENT_TYPE, "text/plain")])
}
fn make_error_body(code: Option<u16>, message: Option<&str>) -> String {
let mut context = tera::Context::new();
let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error");
context.insert(
"title",
&StatusCode::from_u16(out_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
.to_string(),
);
context.insert("message", out_message);
context.insert("status_code", &out_code.to_string());
make_body(
"error.html",
&context,
Some(&format!(
"Failed to render template for Error {out_code}: {out_message}"
))
.cloned(),
)
.0
}
fn make_error_response(
code: Option<u16>,
message: Option<&str>,
) -> Response<Body> {
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));
make_response(&body, out_code, &[(header::CONTENT_TYPE, "text/html")])
}
async fn not_found() -> Response<Body> {
make_error_response(
Some(404),
Some("The page you tried to access could not be found."),
)
}