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"
rust-version= "1.91.1"
[package.metadata.bacon.jobs.fmt-check]
command = [
"cargo", "fmt",
"--check",
"--",
"--color=always",
]
need_stdout = true
[lints.rust]
# levels: allow, expect, warn, force-warn, deny, forbid
unsafe_code = { level = "forbid", priority = 99 }

View file

@ -3,7 +3,6 @@ use std::collections::HashMap;
use crate::types::{Graph, Node, Edge};
pub fn populate_graph() -> Graph {
let toml_source = match std::fs::read_to_string("./static/graph.toml") {
Ok(s) => s,
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> {
let mut nodes: HashMap<String, Node> = HashMap::new();
for (key, node) in old_nodes {
let connections = node.connections.clone().unwrap_or_default();
let mut new_edges = connections.clone();
for (i, edge) in connections.iter().enumerate() {
let mut new_edge = edge.clone();
// 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
if ! old_nodes.contains_key(&edge.to) {
if !old_nodes.contains_key(&edge.to) {
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
@ -77,13 +75,13 @@ fn modulate_nodes(old_nodes: &HashMap<String, Node>) -> HashMap<String, Node> {
// Construct a HashMap with incoming connections (reversed edges)
fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
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![];
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));
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 {
Toml,
Json
Json,
}
pub fn serialize_graph(out_format: &Format, graph: &Graph) -> String {
match *out_format {
Format::Toml => {
match toml::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
}
Format::Toml => match toml::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
},
Format::Json => {
match serde_json::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
}
Format::Json => match serde_json::to_string(graph) {
Ok(s) => s,
Err(e) => e.to_string(),
},
}
}
pub fn deserialize_graph(in_format: &Format, serial: &str) -> Graph {
match *in_format {
Format::Toml => { match toml::from_str(serial) {
Format::Toml => match toml::from_str(serial) {
Ok(g) => g,
Err(error) => Graph::new(Some(error.to_string()))
}},
Format::Json => { match serde_json::from_str(serial) {
Err(error) => Graph::new(Some(error.to_string())),
},
Format::Json => match serde_json::from_str(serial) {
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::{
body::Body,
extract::Path,
http::{ header, HeaderValue, Response, StatusCode },
response::{ Redirect },
http::{header, HeaderValue, Response, StatusCode},
response::{Redirect},
routing::get,
Form,
Router,
Form, Router,
};
use formats::{populate_graph, serialize_graph, Format};
@ -19,44 +18,50 @@ static ONSET: std::sync::LazyLock<std::time::Instant> =
#[tokio::main]
async fn main() {
std::panic::set_hook(Box::new(|info| {
let payload = info.payload_as_str().unwrap_or(
"No string payload. Is edition > 2021?");
let payload = info
.payload_as_str()
.unwrap_or("No string payload. Is edition > 2021?");
let location = info.location().map_or_else(
|| "location unavailable".to_string(),
|s| format!("{}:{}:{}", s.file(), s.line(), s.column()));
|| "location unavailable".to_string(),
|s| format!("{}:{}:{}", s.file(), s.line(), s.column()),
);
eprintln!(" P! [{:?}] {}: {}", ONSET.elapsed(), location, payload);
}));
let app = Router::new()
.route("/", get(index).post(query))
.route("/graph/toml", get(toml_graph))
.route("/graph/json", get(json_graph))
.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(
"/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")
}))
.fallback(not_found)
;
if let Ok(listener) = tokio::net::TcpListener::bind("0.0.0.0:3000").await
.or(Err("Failed to instantiate Tokio listener")) {
.route(
"/acknowledgments",
get(|| static_template_handler("acknowledgments.html")),
)
.fallback(not_found);
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 {
Ok(()) => (),
Err(e) => {
eprintln!("Failed to serve application with axum::serve: {e:#?}");
eprintln!(
"Failed to serve application with axum::serve: {e:#?}"
);
std::process::exit(1);
},
}
@ -68,21 +73,20 @@ fn make_body(
context: &tera::Context,
error_message: Option<String>,
) -> (String, u16) {
let tera = match tera::Tera::new(
concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"),
) {
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 {
@ -92,47 +96,54 @@ fn make_body(
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>"
)
}
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());
error_context.insert(
"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(
body: &str,
status_code: u16,
headers: &[(header::HeaderName, &str)]
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);
.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);
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);
eprintln!(
"[make_response] Failed to wrap header value {}",
header.1
);
}
}
@ -146,25 +157,20 @@ fn template_handler(
error_message: Option<String>,
is_error: bool,
) -> 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 };
make_response(&body, status_code,
&[(header::CONTENT_TYPE, "text/html")])
make_response(&body, status_code, &[(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 graph = populate_graph();
let nodes = graph.nodes;
let empty_node = Node::new(
Some(format!("Could not find node with ID {id}.")),
);
let empty_node =
Node::new(Some(format!("Could not find node with ID {id}.")));
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,
&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()),
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();
@ -204,7 +212,6 @@ async fn index() -> Response<Body> {
}
async fn tree() -> Response<Body> {
let mut context = tera::Context::new();
let graph = populate_graph();
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)]
async fn file_handler(
file_path: &str,
content_type: &str,
) -> Response<Body> {
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}"),
Err(e) => {
panic!("[static_file_handler] Failed to read file contents: {e}")
},
};
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 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");
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}"); }
} else {
eprintln!(
"[static_file_handler] Failed to create content type \
header value from {content_type}"
);
}
response
}
#[derive(serde::Deserialize)]
struct Query { node: String }
struct Query {
node: String,
}
async fn query(Form(query): Form<Query>) -> Redirect {
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")])
}
fn make_error_body(
code: Option<u16>,
message: Option<&str>,
) -> String {
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(
"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
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")],
)
make_response(&body, out_code, &[(header::CONTENT_TYPE, "text/html")])
}
async fn not_found() -> Response<Body> {

View file

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