diff --git a/src/graph.rs b/src/graph.rs index 7c539a4..8a3e01c 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -39,100 +39,149 @@ pub struct Stats { pub detached: HashMap, } -#[derive(Clone, Default, Debug)] -pub struct QueryResult { - pub node: Option, - pub redirect: bool, - pub exact: bool, -} - -impl std::fmt::Display for QueryResult { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let meta = if self.redirect { "[redirect] " } else { "" }; - let node = if let Some(n) = &self.node { - n.id.clone() - } else { - String::from("No Match") - }; - write!(f, "QueryResult: {meta}{node}") - } -} - impl Graph { - pub fn error(message: Option<&str>) -> Graph { + pub fn with_message(message: &str) -> Graph { let graph = Graph::default(); + let mut messages = graph.meta.messages; + messages.push(message.to_string()); Graph { meta: Meta { - messages: message.map_or(vec![], |m| vec![m.to_string()]), + messages, ..graph.meta }, ..graph } } + pub fn malformed(message: Option<&str>) -> Graph { + let mut graph = if let Some(m) = message { + Graph::with_message(m) + } else { + Graph::default() + }; + graph.meta.malformed = true; + graph + } + /// Loads a TOML file from the default location and returns a modulated Graph + /// + /// Returns a graph with an error message if any errors are propagated to it. pub fn load() -> Graph { - Self::load_file("") + let result = Graph::load_file(None); + match result { + Ok(graph) => graph, + Err(error) => Graph::malformed(Some(&error)), + } } /// Takes a file path to a TOML file and returns a modulated Graph /// /// If `path` is an empty string, it will fallback to CLI arguments - pub fn load_file(path: &str) -> Graph { - let mut graph = if path.is_empty() { - Self::read_file(None) - } else { - Self::read_file(Some(path)) - }; + /// + /// # Errors + /// Propagates errors from `Graph::read_file`. + pub fn load_file(path: Option<&str>) -> Result { + let mut graph = Graph::read_file(path)?; graph.modulate(); - graph + Ok(graph) } - /// Reads a TOML fie into a Graph without modulating it - pub fn read_file(in_path: Option<&str>) -> Graph { + /// Reads a TOML file into a Graph without modulating it. + /// + /// # Errors + /// Returns Err if it can't read the contents of `in_path`. + /// Propagates errors from `Graph::from_serial`. + pub fn read_file(in_path: Option<&str>) -> Result { let cli_path = Arguments::default().parse().graph_path; let path = in_path.map_or(cli_path, PathBuf::from); let toml_source = match std::fs::read_to_string(path) { Ok(s) => s, - Err(e) => format!("Error: {e}"), + Err(e) => { + log!(ERROR, "Failed reading {e}"); + return Err("Failed reading file at {path}".to_string()); + }, }; - Self::from_serial(&toml_source, &Format::TOML) + let result = Graph::from_serial(&toml_source, &Format::TOML)?; + Ok(result) } - pub fn from_serial(serial: &str, format: &Format) -> Graph { + /// Deserializes the given format into a graph. + /// + /// # Errors + /// Errors on unsupported formats. + /// Propagates serialization errors. + pub fn from_serial( + serial: &str, + format: &Format, + ) -> Result { match *format { - Format::TOML => match toml::from_str(serial) { - Ok(g) => g, - Err(error) => Graph::error(Some(&error.to_string())), + Format::TOML => match toml::from_str::(serial) { + Ok(graph) => Ok(graph), + Err(error) => Err(SerialError { + cause: SerialErrorCause::MalformedInput, + message: error.to_string(), + }), }, - Format::JSON => match serde_json::from_str(serial) { - Ok(g) => g, - Err(error) => Graph::error(Some(&error.to_string())), + Format::JSON => match serde_json::from_str::(serial) { + Ok(graph) => Ok(graph), + Err(error) => Err(SerialError { + cause: SerialErrorCause::MalformedInput, + message: error.to_string(), + }), }, + Format::Unsupported => Err(SerialError { + cause: SerialErrorCause::UnsupportedFormat, + message: "Unsupported format".to_string(), + }), } } - pub fn to_serial(graph: &Graph, format: &Format) -> String { + /// Serializes a graph to the given format. + /// + /// # Errors + /// Errors on unsupported formats. + /// Propagates serialization errors. + pub fn to_serial( + graph: &Graph, + format: &Format, + ) -> Result { match *format { Format::TOML => match toml::to_string(graph) { - Ok(s) => s, - Err(e) => e.to_string(), + Ok(s) => Ok(s), + Err(e) => Err(SerialError { + cause: SerialErrorCause::MalformedInput, + message: e.to_string(), + }), }, Format::JSON => match serde_json::to_string(graph) { - Ok(s) => s, - Err(e) => e.to_string(), + Ok(s) => Ok(s), + Err(e) => Err(SerialError { + cause: SerialErrorCause::MalformedInput, + message: e.to_string(), + }), }, + Format::Unsupported => Err(SerialError { + cause: SerialErrorCause::UnsupportedFormat, + message: "Unsupported format".to_string(), + }), } } pub fn modulate(&mut self) { + let mut instant = now(); + instant = tlog!(&instant, "Started node modulation"); self.map_lowercase_keys(); + instant = tlog!(&instant, "Mapped lowercase keys"); self.modulate_nodes(); + instant = tlog!(&instant, "Modulated nodes"); self.modulate_edges(); + instant = tlog!(&instant, "Modulated edges"); self.map_incoming(); + instant = tlog!(&instant, "Mapped incoming edges"); self.parse_config(); + tlog!(&instant, "Parsed configuration"); } // Construct a HashMap with incoming connections (reversed edges) @@ -392,6 +441,76 @@ impl Graph { pub enum Format { TOML, JSON, + Unsupported, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum SerialErrorCause { + UnsupportedFormat, + MalformedInput, +} + +impl std::fmt::Display for SerialErrorCause { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let text = match self { + SerialErrorCause::MalformedInput => "Malformed Input", + SerialErrorCause::UnsupportedFormat => "Unsupported Format", + }; + write!(f, "{text}") + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SerialError { + pub cause: SerialErrorCause, + pub message: String, +} + +impl From for String { + fn from(error: SerialError) -> String { + format!("{}: {}", error.cause, error.message) + } +} + +impl From<&str> for Format { + fn from(s: &str) -> Format { + if s.to_lowercase() == "toml" { + Format::TOML + } else if s.to_lowercase() == "json" { + Format::JSON + } else { + Format::Unsupported + } + } +} + +impl std::fmt::Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Format::TOML => write!(f, "TOML"), + Format::JSON => write!(f, "JSON"), + Format::Unsupported => write!(f, "Unsupported"), + } + } +} + +#[derive(Clone, Default, Debug)] +pub struct QueryResult { + pub node: Option, + pub redirect: bool, + pub exact: bool, +} + +impl std::fmt::Display for QueryResult { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let meta = if self.redirect { "[redirect] " } else { "" }; + let node = if let Some(n) = &self.node { + n.id.clone() + } else { + String::from("No Match") + }; + write!(f, "QueryResult: {meta}{node}") + } } #[cfg(test)] @@ -400,7 +519,7 @@ mod tests { #[test] fn empty_graph() { - let graph = Graph::error(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj")); + let graph = Graph::with_message("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"); assert!(graph.nodes.is_empty()); assert!(graph.incoming.is_empty()); assert_eq!( @@ -427,15 +546,16 @@ mod tests { } "#; - let graph = Graph::from_serial(json, &Format::JSON); - assert!(graph.meta.messages.is_empty()); + let deserialize_result = Graph::from_serial(json, &Format::JSON); + println!("{deserialize_result:?}"); + assert!(deserialize_result.is_ok()); } #[test] fn bad_json() { - let graph = Graph::from_serial(":::", &Format::JSON); - let message = graph.meta.messages.first().unwrap(); - assert!(message.contains("expected value at line 1 column 1")); + assert!(Graph::from_serial(":::", &Format::JSON).is_err_and(|e| { + e.message.contains("expected value at line 1 column 1") + },)); } } @@ -456,8 +576,7 @@ mod serial_tests { let graph = Graph::load(); let message = graph.meta.messages.first().unwrap(); - assert!(message.contains("TOML parse error")); - assert!(message.contains("No such file or directory")); + assert!(message.contains("Failed reading file at")); assert!(std::env::set_current_dir(original_working_directory).is_ok()); } diff --git a/src/graph/meta.rs b/src/graph/meta.rs index 3108b94..bea1b2b 100644 --- a/src/graph/meta.rs +++ b/src/graph/meta.rs @@ -7,6 +7,8 @@ pub struct Meta { pub version: Option, #[serde(default)] pub messages: Vec, + #[serde(default)] + pub malformed: bool, } impl Default for Meta { @@ -15,6 +17,7 @@ impl Default for Meta { config: Config::default(), version: Version::from_env(), messages: vec![], + malformed: false, } } } @@ -109,6 +112,7 @@ fn mkfalse() -> bool { fn mk8() -> u16 { 8 } + #[cfg(test)] mod tests { use crate::graph::Graph; @@ -186,7 +190,7 @@ pub struct Version { impl Version { pub fn from_env() -> Option { - Self::from(env!("CARGO_PKG_VERSION")) + Version::from(env!("CARGO_PKG_VERSION")) } pub fn from(version: &str) -> Option { diff --git a/src/log.rs b/src/log.rs index 7bd8ff3..4f1c85e 100644 --- a/src/log.rs +++ b/src/log.rs @@ -27,7 +27,7 @@ impl Data { let trace_string = format!("{trace:?}"); let filter = env::var("DEBUG_FILTER").unwrap_or_default(); let exclude = env::var("DEBUG_EXCLUDE").unwrap_or_default(); - let env_level = Data::env_level(); + let env_level = env_level(); let message_level = message_level_opt.unwrap_or(MESSAGE_DEFAULT); let path = make_display_path(captured_path, &env_level); @@ -69,19 +69,19 @@ impl Data { trace, } } +} - pub fn env_level() -> Level { - if let Ok(level) = env::var("DEBUG") { - Level::from(level.as_str()) - } else { - ENV_DEFAULT - } +pub fn env_level() -> Level { + if let Ok(level) = env::var("DEBUG") { + Level::from(level.as_str()) + } else { + ENV_DEFAULT } } #[allow(clippy::print_stderr)] pub fn print_state() { - let env_level = Data::env_level(); + let env_level = env_level(); let version = env!("CARGO_PKG_VERSION"); if env_level == ENV_DEFAULT { eprintln!("en {version}"); @@ -93,16 +93,19 @@ pub fn print_state() { #[allow(clippy::print_stderr)] pub fn timed(past: &Instant, message: &str) -> Instant { let now = Instant::now(); - let level = Data::env_level(); + let env_level = env_level(); let duration = now.duration_since(*past); let display_duration = if duration.as_millis() > 1000 { format!("{}s {}ms", duration.as_secs(), duration.subsec_millis()) - } else if duration.as_millis() == 0 { + } else if duration.as_millis() <= 1 { + if env_level < Level::VERBOSE { + return now; + } format!("{}ns", duration.as_nanos()) } else { format!("{}ms", duration.as_millis()) }; - if !message.is_empty() && Level::DEBUG <= level { + if !message.is_empty() && Level::DEBUG <= env_level { eprintln!("[tlog] +{display_duration} {message}"); } now diff --git a/src/main.rs b/src/main.rs index 5b037b3..b23dc2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::{backtrace, io, panic}; -use en::{prelude::*, log, ONSET, graph::Graph, syntax}; +use en::{ONSET, graph::Graph, log, prelude::*, syntax}; #[tokio::main] #[allow(clippy::print_stderr, clippy::print_stdout)] @@ -40,7 +40,7 @@ async fn main() -> io::Result<()> { let graph = Graph::load(); instant = tlog!(&instant, "Loaded graph"); - let router = en::router::new(&graph); + let router = en::router::new(graph); tlog!(&instant, "Initialized router"); let listener = diff --git a/src/router.rs b/src/router.rs index 82f51c3..46be06a 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,6 +1,6 @@ -use axum::{routing::get, Router}; +use axum::{Router, routing::get}; -use crate::{graph::Format, graph::Graph}; +use crate::graph::Graph; mod handlers { pub mod graph; @@ -11,57 +11,68 @@ mod handlers { pub mod error; } -pub fn new(graph: &Graph) -> Router { +#[derive(Clone)] +pub struct GlobalState { + pub graph: Graph, +} + +pub fn new(graph: Graph) -> Router { + let state = GlobalState { graph }; + let mut router = Router::default() .route( "/", - get(|| handlers::navigation::page("index.html")) - .post(handlers::navigation::search), + get(handlers::navigation::index).post(handlers::navigation::search), ) .route( "/node/{node_id}", get(handlers::graph::node).post(handlers::graph::node), ) .route("/data", get(handlers::navigation::data)) + .route("/graph/{format}", get(handlers::fixed::serial)) .route("/search", get(handlers::navigation::search)) .route("/redirect", get(handlers::navigation::redirect)) .route( "/static/style.css", get(|| handlers::fixed::file("./static/style.css", "text/css")), ) + .route( + "/static/fonts/sans", + get(|| handlers::fixed::file("./static/fonts/sans", "")), + ) + .route( + "/static/fonts/didone", + get(|| handlers::fixed::file("./static/fonts/didone", "")), + ) + .route( + "/static/fonts/mono", + get(|| handlers::fixed::file("./static/fonts/mono", "")), + ) + .route( + "/static/fonts/title", + get(|| handlers::fixed::file("./static/fonts/title", "")), + ) + .route( + "/static/fonts/prose", + get(|| handlers::fixed::file("./static/fonts/prose", "")), + ) .route( "/static/favicon.svg", get(|| { handlers::fixed::file("./static/favicon.svg", "image/svg+xml") }), - ) - .fallback(handlers::error::not_found); + ); - if graph.meta.config.about { - router = router - .route("/about", get(|| handlers::navigation::page("about.html"))); - } - - if graph.meta.config.tree { + if state.graph.meta.config.tree { router = router.route("/tree", get(handlers::navigation::tree)); } - - if graph.meta.config.raw { - if graph.meta.config.raw_json { - router = router.route( - "/graph/json", - get(|| handlers::fixed::serial(&Format::JSON)), - ); - } - if graph.meta.config.raw_toml { - router = router.route( - "/graph/toml", - get(|| handlers::fixed::serial(&Format::TOML)), - ); - } + if state.graph.meta.config.about { + router = router.route("/about", get(handlers::navigation::about)); } router + .fallback(handlers::error::not_found) + .with_state(state) } #[cfg(test)] @@ -89,7 +100,7 @@ mod tests { }, ..default_graph }; - let router = new(&graph); + let router = new(graph); router .oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap()) @@ -152,7 +163,7 @@ mod tests { config.raw_toml = false; let response = request("/graph/toml", Some(&config)).await; - assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[tokio::test] @@ -161,7 +172,7 @@ mod tests { config.raw_json = false; let response = request("/graph/json", Some(&config)).await; - assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(response.status(), StatusCode::FORBIDDEN); } #[tokio::test] @@ -170,8 +181,8 @@ mod tests { config.raw = false; let toml_response = request("/graph/toml", Some(&config)).await; - assert_eq!(toml_response.status(), StatusCode::NOT_FOUND); + assert_eq!(toml_response.status(), StatusCode::FORBIDDEN); let json_response = request("/graph/json", Some(&config)).await; - assert_eq!(json_response.status(), StatusCode::NOT_FOUND); + assert_eq!(json_response.status(), StatusCode::FORBIDDEN); } } diff --git a/src/router/handlers/error.rs b/src/router/handlers/error.rs index c4d8e84..5367f8b 100644 --- a/src/router/handlers/error.rs +++ b/src/router/handlers/error.rs @@ -1,18 +1,23 @@ use axum::{ body::Body, + extract::State, http::{Response, StatusCode, header}, }; -use crate::{graph::Graph, router::handlers}; +use crate::{ + graph::Graph, + router::{GlobalState, handlers}, +}; pub(in crate::router::handlers) fn by_code( code: Option, message: Option<&str>, + graph: &Graph, ) -> Response { 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)); + let body = make_body(Some(out_code), Some(out_message), graph); handlers::raw::make_response( &body, @@ -21,10 +26,13 @@ pub(in crate::router::handlers) fn by_code( ) } -fn make_body(code: Option, message: Option<&str>) -> String { +fn make_body( + code: Option, + message: Option<&str>, + graph: &Graph, +) -> String { let mut context = tera::Context::default(); - let graph = Graph::load(); let out_code = code.unwrap_or(500); let out_message = &message.unwrap_or("Unknown error"); @@ -35,12 +43,12 @@ fn make_body(code: Option, message: Option<&str>) -> String { .to_string(), ); - context.insert("graph", &graph); + context.insert("graph", graph); context.insert("message", out_message); context.insert("status_code", &out_code.to_string()); handlers::template::render( - "error.html", + "error", &context, Some(&format!( "Failed to render template for Error {out_code}: {out_message}" @@ -50,10 +58,11 @@ fn make_body(code: Option, message: Option<&str>) -> String { .0 } -pub async fn not_found() -> Response { +pub async fn not_found(State(state): State) -> Response { by_code( Some(404), Some("The page you tried to access could not be found."), + &state.graph, ) } @@ -61,27 +70,32 @@ pub async fn not_found() -> Response { mod tests { use axum::{ http::{StatusCode}, + extract::State, }; use super::*; #[tokio::test] async fn not_found() { - let response = super::not_found().await; + let state = State(GlobalState { + graph: Graph::load(), + }); + let response = super::not_found(state).await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn internal_error() { - assert!(by_code(Some(201), None).status() == 201); - assert!(by_code(Some(304), None).status() == 304); - assert!(by_code(Some(418), None).status() == 418); - assert!(by_code(Some(505), None).status() == 505); + let graph = Graph::load(); + assert!(by_code(Some(201), None, &graph).status() == 201); + assert!(by_code(Some(304), None, &graph).status() == 304); + assert!(by_code(Some(418), None, &graph).status() == 418); + assert!(by_code(Some(505), None, &graph).status() == 505); } #[test] fn custom_message() { let pattern = "sibPtt0mvHPWS9HQ0YBQfGu8cUs954LZ"; - let body = make_body(Some(501), Some(pattern)); + let body = make_body(Some(501), Some(pattern), &Graph::load()); assert!(body.contains(pattern)); assert!(!body.contains(&pattern.chars().rev().collect::())); } diff --git a/src/router/handlers/fixed.rs b/src/router/handlers/fixed.rs index 6fcc911..3bcc8a2 100644 --- a/src/router/handlers/fixed.rs +++ b/src/router/handlers/fixed.rs @@ -1,12 +1,13 @@ use axum::{ body::Body, - http::{Response, StatusCode, header, HeaderValue}, + extract::{Path, State}, + http::{HeaderValue, Response, StatusCode, header}, }; use crate::prelude::*; use crate::{ - router::handlers, - graph::{Graph, Format}, + graph::{Format, Graph, SerialErrorCause}, + router::{GlobalState, handlers}, }; /// # Panics @@ -41,22 +42,68 @@ pub async fn file(file_path: &str, content_type: &str) -> Response { response } -#[expect(clippy::unused_async)] -pub async fn serial(format: &Format) -> Response { - let graph = Graph::load(); - let body = Graph::to_serial(&graph, format); +pub async fn serial( + Path(format): Path, + State(state): State, +) -> Response { + let config = &state.graph.meta.config; - 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")], - ), + let make_error = |code: u16, message: &str| -> Response { + handlers::error::by_code( + Some(code), + Some( + format!( + "

{message}

\n\ +

Check the data \n\ + page for the available formats.

" + ) + .as_str(), + ), + &state.graph, + ) + }; + + let forbidden_response = + make_error(403, "This graph format is not available."); + let unsupported_response = + make_error(400, "This graph format is not supported."); + let parse_failure = make_error(505, "The graph has failed to parse."); + + let body = + match Graph::to_serial(&state.graph, &Format::from(format.as_str())) { + Ok(serial) => serial, + Err(error) => match error.cause { + SerialErrorCause::MalformedInput => return parse_failure, + SerialErrorCause::UnsupportedFormat => { + return unsupported_response; + }, + }, + }; + + match Format::from(format.as_str()) { + Format::TOML => { + if config.raw && config.raw_toml { + handlers::raw::make_response( + &body, + 200, + &[(header::CONTENT_TYPE, "text/plain")], + ) + } else { + forbidden_response + } + }, + Format::JSON => { + if config.raw && config.raw_json { + handlers::raw::make_response( + &body, + 200, + &[(header::CONTENT_TYPE, "application/json")], + ) + } else { + forbidden_response + } + }, + Format::Unsupported => unsupported_response, } } @@ -64,15 +111,28 @@ pub async fn serial(format: &Format) -> Response { mod tests { use super::*; + async fn wrap_serial(format: &str) -> Response { + let state = GlobalState { + graph: Graph::load(), + }; + serial(Path(format.to_string()), State(state)).await + } + #[tokio::test] async fn serial_toml() { - let response = serial(&Format::TOML).await; + let response = wrap_serial("toml").await; + assert!(response.status() == 200); + } + + #[tokio::test] + async fn serial_json() { + let response = wrap_serial("json").await; assert!(response.status() == 200); } #[tokio::test] async fn serial_toml_content_type() { - let response = serial(&Format::TOML).await; + let response = wrap_serial("TOML").await; assert!( response.headers().get(header::CONTENT_TYPE).unwrap() == "text/plain" @@ -81,7 +141,7 @@ mod tests { #[tokio::test] async fn serial_json_content_type() { - let response = serial(&Format::JSON).await; + let response = wrap_serial("json").await; assert!( response.headers().get(header::CONTENT_TYPE).unwrap() == "application/json" diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index 581b002..bdf3235 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -1,12 +1,21 @@ -use axum::response::IntoResponse as _; -use axum::{body::Body, extract::Path, http::Response, response::Redirect}; +use axum::{ + extract::State, + response::IntoResponse as _, + {body::Body, extract::Path, http::Response, response::Redirect}, +}; -use crate::{prelude::*, graph::Graph, router::handlers, graph::Node}; +use crate::{ + graph::Node, + prelude::*, + router::{GlobalState, handlers}, +}; -pub async fn node(Path(id): Path) -> Response { +pub async fn node( + Path(id): Path, + State(state): State, +) -> Response { let instant = now(); - let graph = Graph::load(); - let result = graph.find_node(&id); + let result = state.graph.find_node(&id); let found = result.node.is_some(); let node = result .node @@ -25,20 +34,19 @@ pub async fn node(Path(id): Path) -> Response { } let mut context = tera::Context::default(); - context.insert("graph", &graph); + context.insert("graph", &state.graph); context.insert("node", &node); - context.insert("incoming", &graph.incoming.get(&id)); + context.insert("incoming", &state.graph.incoming.get(&id)); tlog!(&instant, "Assembled response for node {}", node.id); - handlers::template::by_filename( - "node.html", + handlers::template::with_context( + "node", &context, if found { 500 } else { 404 }, Some( format!( - "Failed to generate page for node {} (ID {}).\n\ - Node struct:
{:#?}
", - node.title, id, node + "Failed to generate page for node {} (ID {}).", + node.title, id ) .to_owned(), ), @@ -52,17 +60,26 @@ mod tests { http::{HeaderName, StatusCode}, }; + use crate::graph::Graph; + use super::*; + async fn wrap_node(query: &str) -> Response { + let state = GlobalState { + graph: Graph::load(), + }; + node(Path(query.to_string()), axum::extract::State(state)).await + } + #[tokio::test] async fn syntax() { - let response = node(Path("Syntax".to_string())).await; + let response = wrap_node("Syntax").await; assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn syntax_content_type() { - let response = node(Path("Syntax".to_string())).await; + let response = wrap_node("Syntax").await; assert!( response .headers() @@ -76,19 +93,19 @@ mod tests { #[tokio::test] async fn not_found() { - let response = node(Path("InexistentNode".to_string())).await; + let response = wrap_node("InexistentNode").await; assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn redirect() { - let response = node(Path("syntax".to_string())).await; + let response = wrap_node("syntax").await; assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); } #[tokio::test] async fn docs_redirect() { - let response = node(Path("docs".to_string())).await; + let response = wrap_node("docs").await; assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); } } diff --git a/src/router/handlers/navigation.rs b/src/router/handlers/navigation.rs index fa9503a..12bd617 100644 --- a/src/router/handlers/navigation.rs +++ b/src/router/handlers/navigation.rs @@ -1,67 +1,45 @@ -use axum::{ - body::Body, - http::{Response}, - response::Redirect, - Form, -}; +use axum::{Form, body::Body, extract::State, http::Response, response::Redirect}; use crate::{ prelude::*, - graph::{Graph, Node}, - router::handlers, + router::{GlobalState, handlers}, }; -#[expect(clippy::unused_async)] -pub async fn page(template: &str) -> Response { - let instant = now(); - let mut context = tera::Context::default(); - let graph = Graph::load(); - - context.insert("graph", &graph); - - tlog!(&instant, "Assembled response for template {template}"); - handlers::template::by_filename(template, &context, 500, None, false) +pub async fn index(State(state): State) -> Response { + handlers::template::with_graph("index", state).await } -pub async fn tree() -> Response { - let instant = now(); - let mut context = tera::Context::default(); - let mut graph = Graph::load(); +pub async fn about(State(state): State) -> Response { + handlers::template::with_graph("about", state).await +} - context.insert("graph", &graph); - if let Some(root_node) = graph.get_root() { - graph.nodes.remove(&root_node.id); +pub async fn tree(State(state): State) -> Response { + let instant = now(); + + let mut context = tera::Context::default(); + context.insert("graph", &state.graph); + if let Some(root_node) = state.graph.get_root() { context.insert("root_node", &root_node); - context.insert( - "nodes", - &graph.nodes.values().cloned().collect::>(), - ); - } else { - context.insert( - "nodes", - &graph.nodes.values().cloned().collect::>(), - ); } tlog!(&instant, "Assembled response for tree endpoint"); - handlers::template::by_filename("tree.html", &context, 500, None, false) + handlers::template::with_context("tree", &context, 500, None, false) } -pub async fn data() -> Response { +pub async fn data(State(state): State) -> Response { let instant = now(); - let mut context = tera::Context::default(); - let graph = Graph::load(); let mut detached_pairs: Vec<(String, u32)> = - graph.stats.detached.clone().into_iter().collect(); + state.graph.stats.detached.clone().into_iter().collect(); detached_pairs.sort_by(|a, b| b.1.cmp(&a.1)); - context.insert("graph", &graph); - context.insert("detached_count", &graph.stats.detached.len()); + let mut context = tera::Context::default(); + context.insert("graph", &state.graph); + context.insert("detached_count", &state.graph.stats.detached.len()); context.insert("detached_pairs", &detached_pairs); tlog!(&instant, "Assembled response for data endpoint"); - handlers::template::by_filename("data.html", &context, 500, None, false) + handlers::template::with_context("data", &context, 500, None, false) } pub async fn search(Form(query): Form) -> Redirect { @@ -79,11 +57,18 @@ pub struct Query { #[cfg(test)] mod tests { - use axum::{ - http::{StatusCode}, - }; + use axum::http::StatusCode; + use crate::graph::Graph; + use super::*; + async fn wrap_page(path: &str) -> Response { + let state = GlobalState { + graph: Graph::load(), + }; + handlers::template::with_graph(path, state).await + } + #[tokio::test] async fn search_redirect() { let query = Form(Query { @@ -95,19 +80,19 @@ mod tests { #[tokio::test] async fn about_page_ok() { - let response = page("about.html").await; + let response = wrap_page("about").await; assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn tree_page_ok() { - let response = page("tree.html").await; + let response = wrap_page("tree").await; assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn inexistent_page_error() { - let response = page("HBvcwqT8wLk6hxk1GdvNcEzJ6IiZ2Fod").await; + let response = wrap_page("HBvcwqT8wLk6hxk1GdvNcEzJ6IiZ2Fod").await; assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } diff --git a/src/router/handlers/template.rs b/src/router/handlers/template.rs index 199fbca..5ccecc9 100644 --- a/src/router/handlers/template.rs +++ b/src/router/handlers/template.rs @@ -3,9 +3,28 @@ use axum::{ http::{header, Response, StatusCode}, }; -use crate::{prelude::*, router::handlers::raw::make_response}; +use crate::{ + prelude::*, + router::{GlobalState, handlers::raw::make_response}, +}; -pub(in crate::router::handlers) fn by_filename( +/// Assembles a response containing the graph as its only context +/// +/// The template name **must not** contain the extension. +#[expect(clippy::unused_async)] +pub async fn with_graph(template: &str, state: GlobalState) -> Response { + let instant = now(); + let mut context = tera::Context::default(); + context.insert("graph", &state.graph); + + tlog!(&instant, "Assembled response for template {template}"); + with_context(template, &context, 500, None, false) +} + +/// Assembles a response with a custom context. +/// +/// The template name **must not** contain the extension. +pub(in crate::router::handlers) fn with_context( name: &str, context: &tera::Context, error_code: u16, @@ -19,8 +38,11 @@ pub(in crate::router::handlers) fn by_filename( make_response(&body, status_code, &[(header::CONTENT_TYPE, "text/html")]) } +/// Renderes a template into a String and error code +/// +/// The template name **must not** contain the extension (e.g. `.html`). pub(in crate::router::handlers) fn render( - name: &str, + template: &str, // TODO take Option, skip context if None, // then template_handler can replace static_template_handler context: &tera::Context, @@ -35,29 +57,35 @@ pub(in crate::router::handlers) fn render( }, }; - match tera.render(name, context) { + match tera.render(format!("{template}.html").as_str(), context) { Ok(t) => { - tlog!(&instant, "Rendered template {name}"); + tlog!(&instant, "Rendered template {template}"); (t, 200) }, Err(e) => { let mut error_context = tera::Context::default(); - let out_error_message = match error_message { - Some(s) => &format!( + let mut out_error_message = match error_message { + Some(s) => format!( "Template render failed.\n\ User message: {s}, - Engine message:\n
{e:#?}
\n\ - Context:\n
{context:#?}
" + Engine message:\n
{e:#?}
" ), - None => &format!( + None => format!( "Template render failed.\n\ - Engine message:\n
{e:#?}
\n\ - Context:\n
{context:#?}
" + Engine message:\n
{e:#?}
" ), }; - error_context.insert("message", out_error_message); + if log::env_level() >= VERBOSE { + out_error_message = format!( + "{out_error_message}\n\ + Context:\n
{context:#?}
" + ); + } + + log!(ERROR, "{out_error_message}"); + error_context.insert("message", &out_error_message); error_context.insert( "title", &StatusCode::INTERNAL_SERVER_ERROR.to_string(), @@ -113,31 +141,21 @@ mod tests { #[test] fn by_filename_forced_error() { - let response = by_filename( - "index.html", - &tera::Context::default(), - 418, - None, - true, - ); + let response = + with_context("index", &tera::Context::default(), 418, None, true); assert_eq!(response.status(), 418); } #[test] fn by_filename_index() { - let response = by_filename( - "index.html", - &tera::Context::default(), - 418, - None, - false, - ); + let response = + with_context("index", &tera::Context::default(), 418, None, false); assert_eq!(response.status(), 200); } #[test] fn by_filename_file_not_found() { - let response = by_filename( + let response = with_context( "bwbl3BnWsluIgbO2NV9t3vtihwcjuF6t", &tera::Context::default(), 418, @@ -150,7 +168,7 @@ mod tests { #[test] fn by_filename_empty() { let response = - by_filename("", &tera::Context::default(), 418, None, false); + with_context("", &tera::Context::default(), 418, None, false); assert_eq!(response.status(), 500); } @@ -163,7 +181,7 @@ mod tests { context.insert("node", &node); context.insert("graph", &graph); context.insert("incoming", &graph.incoming.get(&node.id)); - let (body, status) = render("node.html", &context, None); + let (body, status) = render("node", &context, None); assert_eq!(status, 200); assert!(body.matches(payload).count() == 1); } @@ -203,8 +221,7 @@ mod tests { #[test] fn render_bad_context() { - let (body, status) = - render("node.html", &tera::Context::default(), None); + let (body, status) = render("node", &tera::Context::default(), None); assert!(body.matches("Template render failed.").count() > 0); assert_eq!(status, 500); } diff --git a/src/syntax/content/parser.rs b/src/syntax/content/parser.rs index fa2318f..0498582 100644 --- a/src/syntax/content/parser.rs +++ b/src/syntax/content/parser.rs @@ -21,15 +21,11 @@ const LEXMAP: LexMap = &[ ]; fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput { - let mut instant = now(); let mut tokens: Vec = Vec::default(); let mut state = State::default(); let segments = segment::segment(text); - let segments_count = segments.len(); - instant = tlog!(&instant, "Segmented {segments_count} segments"); let lexemes = Lexeme::collect(&segments); - instant = tlog!(&instant, "{segments_count} segments: Collected lexemes"); log!(VERBOSE, "Segments: {segments:?}"); @@ -77,10 +73,8 @@ fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput { } } } - instant = tlog!(&instant, "{segments_count} segments: Parsed"); context::close(&state, &mut tokens); - tlog!(&instant, "{segments_count} segments: Closed"); TokenOutput { tokens, diff --git a/src/syntax/content/parser/segment.rs b/src/syntax/content/parser/segment.rs index f8ee4b9..8a8ea76 100644 --- a/src/syntax/content/parser/segment.rs +++ b/src/syntax/content/parser/segment.rs @@ -13,7 +13,7 @@ pub mod delimiter { } impl Default for Delimiters { - fn default() -> Self { + fn default() -> Delimiters { Delimiters { atomic: vec!['`', '|', '\\'], double: vec!['_', '~'], diff --git a/templates/base.html b/templates/base.html index e04d34f..190a0bd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -28,9 +28,7 @@ {% if graph.meta.config.tree %}
  • Tree
  • {% endif %} - {% if graph.meta.config.raw %}
  • Data
  • - {% endif %} {% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}