Reorganize module structure

This commit is contained in:
Juno Takano 2025-12-24 12:45:14 -03:00
commit 14dc84cc43
14 changed files with 19 additions and 23 deletions

6
src/router/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;

View file

@ -0,0 +1,58 @@
use axum::{
body::Body,
http::{Response, StatusCode, header},
};
use crate::{syntax::serial::populate_graph, router::handlers};
pub(in crate::router::handlers) 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");
let config = populate_graph().meta.config;
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());
context.insert("config", &config);
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."),
)
}

View file

@ -0,0 +1,57 @@
use axum::{
body::Body,
http::{Response, StatusCode, header, HeaderValue},
};
use crate::prelude::*;
use crate::{
router::handlers,
syntax::serial::{Format, populate_graph, serialize_graph},
};
/// # Panics
/// Will panic if file read fails.
#[expect(clippy::unused_async)]
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!("Failed to read {file_path} 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) {
log!(
"Overwrote existing header {h:?} because a header for the same key existed"
);
}
} else {
log!("Failed to create content type header value from {content_type}");
}
response
}
#[expect(clippy::unused_async)]
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")],
),
}
}

View file

@ -0,0 +1,42 @@
use axum::response::IntoResponse as _;
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
use crate::syntax::content;
use crate::{syntax::serial::populate_graph, router::handlers, types::Node};
pub async fn node(Path(id): Path<String>) -> Response<Body> {
let graph = populate_graph();
let empty_node = Node::new(Some(format!("Could not find node ID {id}.")));
let node = graph.find_node(&id).unwrap_or(empty_node.clone());
if !graph.nodes.contains_key(&id)
&& graph.lowercase_keymap.contains_key(&id)
{
return Redirect::permanent(format!("/node/{}", node.id).as_str())
.into_response();
}
let mut context = tera::Context::new();
context.insert("node", &node);
context.insert("text", &content::parse(&node.text));
context.insert("incoming", &graph.incoming.get(&id));
context.insert("config", &graph.meta.config.parse_text());
let not_found = node == empty_node;
handlers::template::by_filename(
"node.html",
&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,31 @@
use axum::{
body::Body,
http::{Response},
response::Redirect,
Form,
};
use crate::{syntax::serial::populate_graph, router::handlers, types::Node};
#[expect(clippy::unused_async)]
pub async fn page(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);
context.insert("config", &graph.meta.config.parse_text());
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,
}

View file

@ -0,0 +1,35 @@
use axum::{
body::Body,
http::{header, HeaderValue, Response, StatusCode},
};
use crate::prelude::*;
pub(in crate::router::handlers) 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)
{
log!(
"Overwrote header {overwritten:?} \
because another for key {} already existed",
header.0
);
}
} else {
log!("Failed to wrap header value {}", header.1);
}
}
response
}

View file

@ -0,0 +1,106 @@
use axum::{
body::Body,
http::{header, Response, StatusCode},
};
use crate::{prelude::*, router::handlers::raw::make_response};
pub(in crate::router::handlers) 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(in crate::router::handlers) 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) => {
let early_error_message = format!("{e:#?}");
log!("{}", early_error_message);
return (emergency_wrap(&e), 500);
},
};
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 emergency_wrap(message: &tera::Error) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Pre-Templating Error</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@media (prefers-color-scheme: dark) {{
* {{ background-color: #222222;
color: #f1e9e5; }} }}
* {{ line-height: 1.6em; }}
pre {{ overflow: auto; }}
</style>
</head>
<body>
<h2><strong>Early Pre-Templating Error</strong></h2>
<p>This normally indicates a malformed template.</p>
<pre>
{message}
</pre>
<p>
If you haven't modified templates, plese consider
<a href="https://codeberg.org/jutty/en/issues">reporting it</a>.
</p>
</body>
</html>
"#
)
}