Setup and apply formatting

This commit is contained in:
Juno Takano 2025-12-12 01:59:48 -03:00
commit 20bd1db1b7
5 changed files with 188 additions and 139 deletions

25
.rustfmt.toml Normal file
View file

@ -0,0 +1,25 @@
match_block_trailing_comma = true
max_width = 80
reorder_imports = false
reorder_modules = false
use_field_init_shorthand = true
use_try_shorthand = true
# blank_lines_lower_bound = 1
# not stabilized yet
# where_single_line = true
# overflow_delimited_expr = true
# normalize_doc_attributes = true
# normalize_comments = true
# inline_attribute_width = 40
# imports_granularity = "Crate"
# hex_literal_case = "Lower"
# group_imports = "StdExternalCrate"
# format_strings = true
# force_multiline_blocks = true
# error_on_unformatted = true
# error_on_line_overflow = true
# condense_wildcard_suffixes = true
# doc_comment_code_block_width = 70
# format_code_in_doc_comments = true
# wrap_comments = true

View file

@ -9,6 +9,15 @@ repository = "https://codeberg.org/jutty/en"
edition = "2024" edition = "2024"
rust-version= "1.91.1" rust-version= "1.91.1"
[package.metadata.bacon.jobs.fmt-check]
command = [
"cargo", "fmt",
"--check",
"--",
"--color=always",
]
need_stdout = true
[lints.rust] [lints.rust]
# levels: allow, expect, warn, force-warn, deny, forbid # levels: allow, expect, warn, force-warn, deny, forbid
unsafe_code = { level = "forbid", priority = 99 } unsafe_code = { level = "forbid", priority = 99 }

View file

@ -3,7 +3,6 @@ use std::collections::HashMap;
use crate::types::{Graph, Node, Edge}; use crate::types::{Graph, Node, Edge};
pub fn populate_graph() -> Graph { pub fn populate_graph() -> Graph {
let toml_source = match std::fs::read_to_string("./static/graph.toml") { let toml_source = match std::fs::read_to_string("./static/graph.toml") {
Ok(s) => s, Ok(s) => s,
Err(e) => format!("Error: {e}"), Err(e) => format!("Error: {e}"),
@ -20,16 +19,13 @@ pub fn populate_graph() -> Graph {
} }
fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> { fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
let mut nodes: HashMap<String, Node> = HashMap::new(); let mut nodes: HashMap<String, Node> = HashMap::new();
for (key, node) in old_nodes { for (key, node) in old_nodes {
let connections = node.connections.clone().unwrap_or_default(); let connections = node.connections.clone().unwrap_or_default();
let mut new_edges = connections.clone(); let mut new_edges = connections.clone();
for (i, edge) in connections.iter().enumerate() { for (i, edge) in connections.iter().enumerate() {
let mut new_edge = edge.clone(); let mut new_edge = edge.clone();
// Populate empty "from" IDs in edges with node's ID // Populate empty "from" IDs in edges with node's ID
@ -38,11 +34,13 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
} }
// Flag detached edges // Flag detached edges
if ! old_nodes.contains_key(&edge.to) { if !old_nodes.contains_key(&edge.to) {
new_edge.detached = true; new_edge.detached = true;
} }
if let Some(e) = new_edges.get_mut(i) { *e = new_edge; } if let Some(e) = new_edges.get_mut(i) {
*e = new_edge;
}
} }
// Create connections for each link // Create connections for each link
@ -77,13 +75,13 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
// Construct a HashMap with incoming connections (reversed edges) // Construct a HashMap with incoming connections (reversed edges)
fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> { fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
let mut incoming: HashMap<String, Vec<Edge>> = HashMap::new(); let mut incoming: HashMap<String, Vec<Edge>> = HashMap::new();
for node in nodes.clone().into_values() {
for node in nodes.clone().into_values() {
let empty_vec: Vec<Edge> = vec![]; let empty_vec: Vec<Edge> = vec![];
for edge in &node.connections.clone().unwrap_or_default() { for edge in &node.connections.clone().unwrap_or_default() {
let mut edges = incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone(); let mut edges =
incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone();
edges.extend_from_slice(std::slice::from_ref(edge)); edges.extend_from_slice(std::slice::from_ref(edge));
incoming.insert(edge.to.clone(), edges.clone()); incoming.insert(edge.to.clone(), edges.clone());
} }
@ -94,39 +92,31 @@ fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
pub enum Format { pub enum Format {
Toml, Toml,
Json Json,
} }
pub fn serialize_graph(out_format: &Format, graph: &Graph) -> String { pub fn serialize_graph(out_format: &Format, graph: &Graph) -> String {
match *out_format { match *out_format {
Format::Toml => { Format::Toml => match toml::to_string(graph) {
match toml::to_string(graph) { Ok(s) => s,
Ok(s) => s, Err(e) => e.to_string(),
Err(e) => e.to_string(),
}
}, },
Format::Json => { Format::Json => match serde_json::to_string(graph) {
match serde_json::to_string(graph) { Ok(s) => s,
Ok(s) => s, Err(e) => e.to_string(),
Err(e) => e.to_string(),
}
}, },
} }
} }
pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph { pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph {
match *in_format { match *in_format {
Format::Toml => { match toml::from_str(serial) { Format::Toml => match toml::from_str(serial) {
Ok(g) => g, Ok(g) => g,
Err(error) => Graph::new(Some(error.to_string())) Err(error) => Graph::new(Some(error.to_string())),
}}, },
Format::Json => { match serde_json::from_str(serial) { Format::Json => match serde_json::from_str(serial) {
Ok(g) => g, Ok(g) => g,
Err(error) => Graph::new(Some(error.to_string())) Err(error) => Graph::new(Some(error.to_string())),
}} },
} }
} }

View file

@ -1,11 +1,10 @@
use axum::{ use axum::{
body::Body, body::Body,
extract::Path, extract::Path,
http::{ header, HeaderValue, Response, StatusCode }, http::{header, HeaderValue, Response, StatusCode},
response::{ Redirect }, response::{Redirect},
routing::get, routing::get,
Form, Form, Router,
Router,
}; };
use formats::{populate_graph, serialize_graph, Format}; use formats::{populate_graph, serialize_graph, Format};
@ -19,44 +18,50 @@ static ONSET: std::sync::LazyLock<std::time::Instant> =
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
std::panic::set_hook(Box::new(|info| { std::panic::set_hook(Box::new(|info| {
let payload = info
let payload = info.payload_as_str().unwrap_or( .payload_as_str()
"No string payload. Is edition > 2021?"); .unwrap_or("No string payload. Is edition > 2021?");
let location = info.location().map_or_else( let location = info.location().map_or_else(
|| "location unavailable".to_string(), || "location unavailable".to_string(),
|s| format!("{}:{}:{}", s.file(), s.line(), s.column())); |s| format!("{}:{}:{}", s.file(), s.line(), s.column()),
);
eprintln!(" P! [{:?}] {}: {}", ONSET.elapsed(), location, payload); eprintln!(" P! [{:?}] {}: {}", ONSET.elapsed(), location, payload);
})); }));
let app = Router::new() let app = Router::new()
.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( .route(
|| { file_handler("./static/style.css", "text/css") })) "/static/style.css",
.route("/static/favicon.svg", get( get(|| file_handler("./static/style.css", "text/css")),
|| { file_handler("./static/favicon.svg", "image/svg+xml") })) )
.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(|| { .route(
static_template_handler("acknowledgments.html") "/acknowledgments",
})) get(|| static_template_handler("acknowledgments.html")),
.fallback(not_found) )
; .fallback(not_found);
if let Ok(listener) = tokio::net::TcpListener::bind("0.0.0.0:3000").await
.or(Err("Failed to instantiate Tokio listener")) {
if let Ok(listener) = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.or(Err("Failed to instantiate Tokio listener"))
{
match axum::serve(listener, app).await { match axum::serve(listener, app).await {
Ok(()) => (), Ok(()) => (),
Err(e) => { Err(e) => {
eprintln!("Failed to serve application with axum::serve: {e:#?}"); eprintln!(
"Failed to serve application with axum::serve: {e:#?}"
);
std::process::exit(1); std::process::exit(1);
}, },
} }
@ -68,21 +73,20 @@ fn make_body(
context: &tera::Context, context: &tera::Context,
error_message: Option<String>, error_message: Option<String>,
) -> (String, u16) { ) -> (String, u16) {
let tera = match tera::Tera::new(concat!(
let tera = match tera::Tera::new( env!("CARGO_MANIFEST_DIR"),
concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"), "/templates/**/*"
) { )) {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
println!("Tera parsing error: {e:#?}"); println!("Tera parsing error: {e:#?}");
panic!("{e}") panic!("{e}")
} },
}; };
match tera.render(name, context) { match tera.render(name, context) {
Ok(t) => (t, 200), Ok(t) => (t, 200),
Err(e) => { Err(e) => {
let mut error_context = tera::Context::new(); let mut error_context = tera::Context::new();
let out_error_message = match error_message { let out_error_message = match error_message {
@ -92,47 +96,54 @@ fn make_body(
Engine message:\n<pre>{e:#?}</pre>\n\ Engine message:\n<pre>{e:#?}</pre>\n\
Context:\n<pre>{context:#?}</pre>" Context:\n<pre>{context:#?}</pre>"
), ),
None => { None => &format!(
&format!( "Template render failed.\n\
"Template render failed.\n\ Engine message:\n<pre>{e:#?}</pre>\n\
Engine message:\n<pre>{e:#?}</pre>\n\ Context:\n<pre>{context:#?}</pre>"
Context:\n<pre>{context:#?}</pre>" ),
)
}
}; };
error_context.insert("message", out_error_message); error_context.insert("message", out_error_message);
error_context.insert("title", error_context.insert(
&StatusCode::INTERNAL_SERVER_ERROR.to_string()); "title",
&StatusCode::INTERNAL_SERVER_ERROR.to_string(),
);
(tera.render("error.html", &error_context) (
.unwrap_or(out_error_message.clone()), 500) tera.render("error.html", &error_context)
} .unwrap_or(out_error_message.clone()),
500,
)
},
} }
} }
fn make_response( fn make_response(
body: &str, body: &str,
status_code: u16, status_code: u16,
headers: &[(header::HeaderName, &str)] headers: &[(header::HeaderName, &str)],
) -> Response<Body> { ) -> Response<Body> {
let mut response = Response::new(Body::from(body.to_owned())); let mut response = Response::new(Body::from(body.to_owned()));
*response.status_mut() = StatusCode::from_u16(status_code) *response.status_mut() = StatusCode::from_u16(status_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
for header in headers { for header in headers {
if let Ok(wrapped) = HeaderValue::from_str(header.1) { if let Ok(wrapped) = HeaderValue::from_str(header.1) {
if let Some(overwritten) = response.headers_mut().insert( if let Some(overwritten) =
header.0.clone(), response.headers_mut().insert(header.0.clone(), wrapped)
wrapped, {
) { eprintln!("[make_response] Overwrote header {overwritten:?} \ eprintln!(
because another for key {} already existed", header.0); "[make_response] Overwrote header {overwritten:?} \
because another for key {} already existed",
header.0
);
} }
} else { } else {
eprintln!("[make_response] Failed to wrap header value {}", eprintln!(
header.1); "[make_response] Failed to wrap header value {}",
header.1
);
} }
} }
@ -146,25 +157,20 @@ fn template_handler(
error_message: Option<String>, error_message: Option<String>,
is_error: bool, is_error: bool,
) -> Response<Body> { ) -> Response<Body> {
let (body, render_status) = make_body(name, context, error_message);
let (body, render_status) = make_body(
name, context, error_message);
let status_code = if is_error { error_code } else { render_status }; let status_code = if is_error { error_code } else { render_status };
make_response(&body, status_code, make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")])
&[(header::CONTENT_TYPE, "text/html")])
} }
async fn node_view(Path(id): Path<String>) -> Response<Body> { async fn node_view(Path(id): Path<String>) -> Response<Body> {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
let graph = populate_graph(); let graph = populate_graph();
let nodes = graph.nodes; let nodes = graph.nodes;
let empty_node = Node::new( let empty_node =
Some(format!("Could not find node with ID {id}.")), Node::new(Some(format!("Could not find node with ID {id}.")));
);
let node: &Node = nodes.get(&id).unwrap_or(&empty_node); let node: &Node = nodes.get(&id).unwrap_or(&empty_node);
@ -181,17 +187,19 @@ async fn node_view(Path(id): Path<String>) -> Response<Body> {
&template_name, &template_name,
&context, &context,
if not_found { 404 } else { 500 }, if not_found { 404 } else { 500 },
Some(format!( Some(
"Failed to generate page for node {} (ID {}).\n\ format!(
Node struct: <pre>{:#?}</pre>", "Failed to generate page for node {} (ID {}).\n\
node.title, id, node Node struct: <pre>{:#?}</pre>",
).to_owned()), node.title, id, node
)
.to_owned(),
),
not_found, not_found,
) )
} }
async fn index() -> Response<Body> { 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();
let root_node = graph.get_root().unwrap_or_default(); let root_node = graph.get_root().unwrap_or_default();
@ -204,7 +212,6 @@ async fn index() -> Response<Body> {
} }
async fn tree() -> Response<Body> { 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();
let root_node = graph.get_root().unwrap_or_default(); let root_node = graph.get_root().unwrap_or_default();
@ -222,14 +229,12 @@ async fn static_template_handler(name: &str) -> Response<Body> {
} }
#[expect(clippy::unused_async)] #[expect(clippy::unused_async)]
async fn file_handler( async fn file_handler(file_path: &str, content_type: &str) -> Response<Body> {
file_path: &str,
content_type: &str,
) -> Response<Body> {
let content = match std::fs::read(file_path) { let content = match std::fs::read(file_path) {
Ok(s) => s, Ok(s) => s,
Err(e) => panic!("[static_file_handler] Failed to read file contents: {e}"), Err(e) => {
panic!("[static_file_handler] Failed to read file contents: {e}")
},
}; };
let mut response = Response::new(Body::from(content)); let mut response = Response::new(Body::from(content));
@ -238,17 +243,25 @@ async fn file_handler(
if let Ok(header_value) = HeaderValue::from_str(content_type) { if let Ok(header_value) = HeaderValue::from_str(content_type) {
if let Some(h) = response.headers_mut().insert(header, header_value) { if let Some(h) = response.headers_mut().insert(header, header_value) {
eprintln!("[static_file_handler] Overwrote existing header {h:?} \ eprintln!(
because a header for the same key existed"); "[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 \ } else {
header value from {content_type}"); } eprintln!(
"[static_file_handler] Failed to create content type \
header value from {content_type}"
);
}
response response
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct Query { node: String } struct Query {
node: String,
}
async fn query(Form(query): Form<Query>) -> Redirect { async fn query(Form(query): Form<Query>) -> Redirect {
Redirect::permanent(format!("/node/{}", query.node).as_str()) Redirect::permanent(format!("/node/{}", query.node).as_str())
@ -268,41 +281,43 @@ async fn toml_graph() -> Response<Body> {
make_response(&body, 200, &[(header::CONTENT_TYPE, "text/plain")]) make_response(&body, 200, &[(header::CONTENT_TYPE, "text/plain")])
} }
fn make_error_body( fn make_error_body(code: Option<u16>, message: Option<&str>) -> String {
code: Option<u16>,
message: Option<&str>,
) -> String {
let mut context = tera::Context::new(); let mut context = tera::Context::new();
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");
context.insert("title", &StatusCode::from_u16(out_code) context.insert(
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR).to_string()); "title",
&StatusCode::from_u16(out_code)
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
.to_string(),
);
context.insert("message", out_message); context.insert("message", out_message);
context.insert("status_code", &out_code.to_string()); context.insert("status_code", &out_code.to_string());
make_body("error.html", &context, Some(&format!( make_body(
"Failed to render template for Error {out_code}: {out_message}" "error.html",
)).cloned()).0 &context,
Some(&format!(
"Failed to render template for Error {out_code}: {out_message}"
))
.cloned(),
)
.0
} }
fn make_error_response( fn make_error_response(
code: Option<u16>, code: Option<u16>,
message: Option<&str>, message: Option<&str>,
) -> Response<Body> { ) -> 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( make_response(&body, out_code, &[(header::CONTENT_TYPE, "text/html")])
&body,
out_code,
&[(header::CONTENT_TYPE, "text/html")],
)
} }
async fn not_found() -> Response<Body> { async fn not_found() -> Response<Body> {

View file

@ -6,16 +6,21 @@ use serde::{Serialize, Deserialize};
pub struct Graph { pub struct Graph {
pub nodes: HashMap<String, Node>, pub nodes: HashMap<String, Node>,
pub root_node: String, pub root_node: String,
#[serde(default)] pub messages: Vec<String>, #[serde(default)]
#[serde(skip)] pub incoming: HashMap<String, Vec<Edge>>, pub messages: Vec<String>,
#[serde(skip)]
pub incoming: HashMap<String, Vec<Edge>>,
} }
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Node { pub struct Node {
pub text: String, pub text: String,
#[serde(default)] pub title: String, #[serde(default)]
#[serde(default)] pub links: Vec<String>, pub title: String,
#[serde(default)] pub id: String, #[serde(default)]
pub links: Vec<String>,
#[serde(default)]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub connections: Option<Vec<Edge>>, pub connections: Option<Vec<Edge>>,
@ -24,9 +29,12 @@ pub struct Node {
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)] #[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Edge { pub struct Edge {
pub to: String, pub to: String,
#[serde(default)] pub anchor: String, #[serde(default)]
#[serde(default)] pub from: String, pub anchor: String,
#[serde(default)] pub detached: bool, #[serde(default)]
pub from: String,
#[serde(default)]
pub detached: bool,
} }
impl Graph { impl Graph {
@ -35,8 +43,10 @@ impl Graph {
nodes: HashMap::new(), nodes: HashMap::new(),
root_node: "VoidNode".to_string(), root_node: "VoidNode".to_string(),
incoming: HashMap::new(), incoming: HashMap::new(),
messages: vec![message messages: vec![
.unwrap_or("This graph is empty or in error".to_string())], message
.unwrap_or("This graph is empty or in error".to_string()),
],
} }
} }
@ -52,7 +62,7 @@ impl Node {
title: "Pure Void".to_string(), title: "Pure Void".to_string(),
text: match message { text: match message {
Some(s) => s, Some(s) => s,
None => "Node is empty, missing or wasn't found.".to_string() None => "Node is empty, missing or wasn't found.".to_string(),
}, },
connections: None, connections: None,
links: vec![], links: vec![],