Further centralize state, return result from serial methods
This commit is contained in:
parent
93c62229ad
commit
c23d35217d
15 changed files with 471 additions and 244 deletions
225
src/graph.rs
225
src/graph.rs
|
|
@ -39,100 +39,149 @@ pub struct Stats {
|
||||||
pub detached: HashMap<String, u32>,
|
pub detached: HashMap<String, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
|
||||||
pub struct QueryResult {
|
|
||||||
pub node: Option<Node>,
|
|
||||||
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 {
|
impl Graph {
|
||||||
pub fn error(message: Option<&str>) -> Graph {
|
pub fn with_message(message: &str) -> Graph {
|
||||||
let graph = Graph::default();
|
let graph = Graph::default();
|
||||||
|
let mut messages = graph.meta.messages;
|
||||||
|
messages.push(message.to_string());
|
||||||
Graph {
|
Graph {
|
||||||
meta: Meta {
|
meta: Meta {
|
||||||
messages: message.map_or(vec![], |m| vec![m.to_string()]),
|
messages,
|
||||||
..graph.meta
|
..graph.meta
|
||||||
},
|
},
|
||||||
..graph
|
..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
|
/// 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 {
|
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
|
/// 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
|
/// 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() {
|
/// # Errors
|
||||||
Self::read_file(None)
|
/// Propagates errors from `Graph::read_file`.
|
||||||
} else {
|
pub fn load_file(path: Option<&str>) -> Result<Graph, String> {
|
||||||
Self::read_file(Some(path))
|
let mut graph = Graph::read_file(path)?;
|
||||||
};
|
|
||||||
graph.modulate();
|
graph.modulate();
|
||||||
graph
|
Ok(graph)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a TOML fie into a Graph without modulating it
|
/// Reads a TOML file into a Graph without modulating it.
|
||||||
pub fn read_file(in_path: Option<&str>) -> Graph {
|
///
|
||||||
|
/// # 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<Graph, String> {
|
||||||
let cli_path = Arguments::default().parse().graph_path;
|
let cli_path = Arguments::default().parse().graph_path;
|
||||||
let path = in_path.map_or(cli_path, PathBuf::from);
|
let path = in_path.map_or(cli_path, PathBuf::from);
|
||||||
|
|
||||||
let toml_source = match std::fs::read_to_string(path) {
|
let toml_source = match std::fs::read_to_string(path) {
|
||||||
Ok(s) => s,
|
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<Graph, SerialError> {
|
||||||
match *format {
|
match *format {
|
||||||
Format::TOML => match toml::from_str(serial) {
|
Format::TOML => match toml::from_str::<Graph>(serial) {
|
||||||
Ok(g) => g,
|
Ok(graph) => Ok(graph),
|
||||||
Err(error) => Graph::error(Some(&error.to_string())),
|
Err(error) => Err(SerialError {
|
||||||
|
cause: SerialErrorCause::MalformedInput,
|
||||||
|
message: error.to_string(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
Format::JSON => match serde_json::from_str(serial) {
|
Format::JSON => match serde_json::from_str::<Graph>(serial) {
|
||||||
Ok(g) => g,
|
Ok(graph) => Ok(graph),
|
||||||
Err(error) => Graph::error(Some(&error.to_string())),
|
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<String, SerialError> {
|
||||||
match *format {
|
match *format {
|
||||||
Format::TOML => match toml::to_string(graph) {
|
Format::TOML => match toml::to_string(graph) {
|
||||||
Ok(s) => s,
|
Ok(s) => Ok(s),
|
||||||
Err(e) => e.to_string(),
|
Err(e) => Err(SerialError {
|
||||||
|
cause: SerialErrorCause::MalformedInput,
|
||||||
|
message: e.to_string(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
Format::JSON => match serde_json::to_string(graph) {
|
Format::JSON => match serde_json::to_string(graph) {
|
||||||
Ok(s) => s,
|
Ok(s) => Ok(s),
|
||||||
Err(e) => e.to_string(),
|
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) {
|
pub fn modulate(&mut self) {
|
||||||
|
let mut instant = now();
|
||||||
|
instant = tlog!(&instant, "Started node modulation");
|
||||||
self.map_lowercase_keys();
|
self.map_lowercase_keys();
|
||||||
|
instant = tlog!(&instant, "Mapped lowercase keys");
|
||||||
self.modulate_nodes();
|
self.modulate_nodes();
|
||||||
|
instant = tlog!(&instant, "Modulated nodes");
|
||||||
self.modulate_edges();
|
self.modulate_edges();
|
||||||
|
instant = tlog!(&instant, "Modulated edges");
|
||||||
self.map_incoming();
|
self.map_incoming();
|
||||||
|
instant = tlog!(&instant, "Mapped incoming edges");
|
||||||
self.parse_config();
|
self.parse_config();
|
||||||
|
tlog!(&instant, "Parsed configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a HashMap with incoming connections (reversed edges)
|
// Construct a HashMap with incoming connections (reversed edges)
|
||||||
|
|
@ -392,6 +441,76 @@ impl Graph {
|
||||||
pub enum Format {
|
pub enum Format {
|
||||||
TOML,
|
TOML,
|
||||||
JSON,
|
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<SerialError> 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<Node>,
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
|
|
@ -400,7 +519,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_graph() {
|
fn empty_graph() {
|
||||||
let graph = Graph::error(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"));
|
let graph = Graph::with_message("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj");
|
||||||
assert!(graph.nodes.is_empty());
|
assert!(graph.nodes.is_empty());
|
||||||
assert!(graph.incoming.is_empty());
|
assert!(graph.incoming.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -427,15 +546,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let graph = Graph::from_serial(json, &Format::JSON);
|
let deserialize_result = Graph::from_serial(json, &Format::JSON);
|
||||||
assert!(graph.meta.messages.is_empty());
|
println!("{deserialize_result:?}");
|
||||||
|
assert!(deserialize_result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bad_json() {
|
fn bad_json() {
|
||||||
let graph = Graph::from_serial(":::", &Format::JSON);
|
assert!(Graph::from_serial(":::", &Format::JSON).is_err_and(|e| {
|
||||||
let message = graph.meta.messages.first().unwrap();
|
e.message.contains("expected value at line 1 column 1")
|
||||||
assert!(message.contains("expected value at line 1 column 1"));
|
},));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,8 +576,7 @@ mod serial_tests {
|
||||||
|
|
||||||
let graph = Graph::load();
|
let graph = Graph::load();
|
||||||
let message = graph.meta.messages.first().unwrap();
|
let message = graph.meta.messages.first().unwrap();
|
||||||
assert!(message.contains("TOML parse error"));
|
assert!(message.contains("Failed reading file at"));
|
||||||
assert!(message.contains("No such file or directory"));
|
|
||||||
|
|
||||||
assert!(std::env::set_current_dir(original_working_directory).is_ok());
|
assert!(std::env::set_current_dir(original_working_directory).is_ok());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ pub struct Meta {
|
||||||
pub version: Option<Version>,
|
pub version: Option<Version>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub messages: Vec<String>,
|
pub messages: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub malformed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Meta {
|
impl Default for Meta {
|
||||||
|
|
@ -15,6 +17,7 @@ impl Default for Meta {
|
||||||
config: Config::default(),
|
config: Config::default(),
|
||||||
version: Version::from_env(),
|
version: Version::from_env(),
|
||||||
messages: vec![],
|
messages: vec![],
|
||||||
|
malformed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +112,7 @@ fn mkfalse() -> bool {
|
||||||
fn mk8() -> u16 {
|
fn mk8() -> u16 {
|
||||||
8
|
8
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::graph::Graph;
|
use crate::graph::Graph;
|
||||||
|
|
@ -186,7 +190,7 @@ pub struct Version {
|
||||||
|
|
||||||
impl Version {
|
impl Version {
|
||||||
pub fn from_env() -> Option<Version> {
|
pub fn from_env() -> Option<Version> {
|
||||||
Self::from(env!("CARGO_PKG_VERSION"))
|
Version::from(env!("CARGO_PKG_VERSION"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from(version: &str) -> Option<Version> {
|
pub fn from(version: &str) -> Option<Version> {
|
||||||
|
|
|
||||||
25
src/log.rs
25
src/log.rs
|
|
@ -27,7 +27,7 @@ impl Data {
|
||||||
let trace_string = format!("{trace:?}");
|
let trace_string = format!("{trace:?}");
|
||||||
let filter = env::var("DEBUG_FILTER").unwrap_or_default();
|
let filter = env::var("DEBUG_FILTER").unwrap_or_default();
|
||||||
let exclude = env::var("DEBUG_EXCLUDE").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 message_level = message_level_opt.unwrap_or(MESSAGE_DEFAULT);
|
||||||
let path = make_display_path(captured_path, &env_level);
|
let path = make_display_path(captured_path, &env_level);
|
||||||
|
|
||||||
|
|
@ -69,19 +69,19 @@ impl Data {
|
||||||
trace,
|
trace,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn env_level() -> Level {
|
pub fn env_level() -> Level {
|
||||||
if let Ok(level) = env::var("DEBUG") {
|
if let Ok(level) = env::var("DEBUG") {
|
||||||
Level::from(level.as_str())
|
Level::from(level.as_str())
|
||||||
} else {
|
} else {
|
||||||
ENV_DEFAULT
|
ENV_DEFAULT
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::print_stderr)]
|
#[allow(clippy::print_stderr)]
|
||||||
pub fn print_state() {
|
pub fn print_state() {
|
||||||
let env_level = Data::env_level();
|
let env_level = env_level();
|
||||||
let version = env!("CARGO_PKG_VERSION");
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
if env_level == ENV_DEFAULT {
|
if env_level == ENV_DEFAULT {
|
||||||
eprintln!("en {version}");
|
eprintln!("en {version}");
|
||||||
|
|
@ -93,16 +93,19 @@ pub fn print_state() {
|
||||||
#[allow(clippy::print_stderr)]
|
#[allow(clippy::print_stderr)]
|
||||||
pub fn timed(past: &Instant, message: &str) -> Instant {
|
pub fn timed(past: &Instant, message: &str) -> Instant {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let level = Data::env_level();
|
let env_level = env_level();
|
||||||
let duration = now.duration_since(*past);
|
let duration = now.duration_since(*past);
|
||||||
let display_duration = if duration.as_millis() > 1000 {
|
let display_duration = if duration.as_millis() > 1000 {
|
||||||
format!("{}s {}ms", duration.as_secs(), duration.subsec_millis())
|
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())
|
format!("{}ns", duration.as_nanos())
|
||||||
} else {
|
} else {
|
||||||
format!("{}ms", duration.as_millis())
|
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}");
|
eprintln!("[tlog] +{display_duration} {message}");
|
||||||
}
|
}
|
||||||
now
|
now
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{backtrace, io, panic};
|
use std::{backtrace, io, panic};
|
||||||
|
|
||||||
use en::{prelude::*, log, ONSET, graph::Graph, syntax};
|
use en::{ONSET, graph::Graph, log, prelude::*, syntax};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
#[allow(clippy::print_stderr, clippy::print_stdout)]
|
#[allow(clippy::print_stderr, clippy::print_stdout)]
|
||||||
|
|
@ -40,7 +40,7 @@ async fn main() -> io::Result<()> {
|
||||||
let graph = Graph::load();
|
let graph = Graph::load();
|
||||||
instant = tlog!(&instant, "Loaded graph");
|
instant = tlog!(&instant, "Loaded graph");
|
||||||
|
|
||||||
let router = en::router::new(&graph);
|
let router = en::router::new(graph);
|
||||||
tlog!(&instant, "Initialized router");
|
tlog!(&instant, "Initialized router");
|
||||||
|
|
||||||
let listener =
|
let listener =
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
mod handlers {
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|
@ -11,57 +11,68 @@ mod handlers {
|
||||||
pub mod error;
|
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()
|
let mut router = Router::default()
|
||||||
.route(
|
.route(
|
||||||
"/",
|
"/",
|
||||||
get(|| handlers::navigation::page("index.html"))
|
get(handlers::navigation::index).post(handlers::navigation::search),
|
||||||
.post(handlers::navigation::search),
|
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/node/{node_id}",
|
"/node/{node_id}",
|
||||||
get(handlers::graph::node).post(handlers::graph::node),
|
get(handlers::graph::node).post(handlers::graph::node),
|
||||||
)
|
)
|
||||||
.route("/data", get(handlers::navigation::data))
|
.route("/data", get(handlers::navigation::data))
|
||||||
|
.route("/graph/{format}", get(handlers::fixed::serial))
|
||||||
.route("/search", get(handlers::navigation::search))
|
.route("/search", get(handlers::navigation::search))
|
||||||
.route("/redirect", get(handlers::navigation::redirect))
|
.route("/redirect", get(handlers::navigation::redirect))
|
||||||
.route(
|
.route(
|
||||||
"/static/style.css",
|
"/static/style.css",
|
||||||
get(|| handlers::fixed::file("./static/style.css", "text/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(
|
.route(
|
||||||
"/static/favicon.svg",
|
"/static/favicon.svg",
|
||||||
get(|| {
|
get(|| {
|
||||||
handlers::fixed::file("./static/favicon.svg", "image/svg+xml")
|
handlers::fixed::file("./static/favicon.svg", "image/svg+xml")
|
||||||
}),
|
}),
|
||||||
)
|
);
|
||||||
.fallback(handlers::error::not_found);
|
|
||||||
|
|
||||||
if graph.meta.config.about {
|
if state.graph.meta.config.tree {
|
||||||
router = router
|
|
||||||
.route("/about", get(|| handlers::navigation::page("about.html")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if graph.meta.config.tree {
|
|
||||||
router = router.route("/tree", get(handlers::navigation::tree));
|
router = router.route("/tree", get(handlers::navigation::tree));
|
||||||
}
|
}
|
||||||
|
if state.graph.meta.config.about {
|
||||||
if graph.meta.config.raw {
|
router = router.route("/about", get(handlers::navigation::about));
|
||||||
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)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
|
.fallback(handlers::error::not_found)
|
||||||
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -89,7 +100,7 @@ mod tests {
|
||||||
},
|
},
|
||||||
..default_graph
|
..default_graph
|
||||||
};
|
};
|
||||||
let router = new(&graph);
|
let router = new(graph);
|
||||||
|
|
||||||
router
|
router
|
||||||
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
.oneshot(Request::builder().uri(uri).body(Body::empty()).unwrap())
|
||||||
|
|
@ -152,7 +163,7 @@ mod tests {
|
||||||
config.raw_toml = false;
|
config.raw_toml = false;
|
||||||
|
|
||||||
let response = request("/graph/toml", Some(&config)).await;
|
let response = request("/graph/toml", Some(&config)).await;
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -161,7 +172,7 @@ mod tests {
|
||||||
config.raw_json = false;
|
config.raw_json = false;
|
||||||
|
|
||||||
let response = request("/graph/json", Some(&config)).await;
|
let response = request("/graph/json", Some(&config)).await;
|
||||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -170,8 +181,8 @@ mod tests {
|
||||||
config.raw = false;
|
config.raw = false;
|
||||||
|
|
||||||
let toml_response = request("/graph/toml", Some(&config)).await;
|
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;
|
let json_response = request("/graph/json", Some(&config)).await;
|
||||||
assert_eq!(json_response.status(), StatusCode::NOT_FOUND);
|
assert_eq!(json_response.status(), StatusCode::FORBIDDEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
|
extract::State,
|
||||||
http::{Response, StatusCode, header},
|
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(
|
pub(in crate::router::handlers) fn by_code(
|
||||||
code: Option<u16>,
|
code: Option<u16>,
|
||||||
message: Option<&str>,
|
message: Option<&str>,
|
||||||
|
graph: &Graph,
|
||||||
) -> 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_body(Some(out_code), Some(out_message));
|
let body = make_body(Some(out_code), Some(out_message), graph);
|
||||||
|
|
||||||
handlers::raw::make_response(
|
handlers::raw::make_response(
|
||||||
&body,
|
&body,
|
||||||
|
|
@ -21,10 +26,13 @@ pub(in crate::router::handlers) fn by_code(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_body(code: Option<u16>, message: Option<&str>) -> String {
|
fn make_body(
|
||||||
|
code: Option<u16>,
|
||||||
|
message: Option<&str>,
|
||||||
|
graph: &Graph,
|
||||||
|
) -> String {
|
||||||
let mut context = tera::Context::default();
|
let mut context = tera::Context::default();
|
||||||
|
|
||||||
let graph = Graph::load();
|
|
||||||
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");
|
||||||
|
|
||||||
|
|
@ -35,12 +43,12 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
context.insert("graph", &graph);
|
context.insert("graph", graph);
|
||||||
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());
|
||||||
|
|
||||||
handlers::template::render(
|
handlers::template::render(
|
||||||
"error.html",
|
"error",
|
||||||
&context,
|
&context,
|
||||||
Some(&format!(
|
Some(&format!(
|
||||||
"Failed to render template for Error {out_code}: {out_message}"
|
"Failed to render template for Error {out_code}: {out_message}"
|
||||||
|
|
@ -50,10 +58,11 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
|
||||||
.0
|
.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn not_found() -> Response<Body> {
|
pub async fn not_found(State(state): State<GlobalState>) -> Response<Body> {
|
||||||
by_code(
|
by_code(
|
||||||
Some(404),
|
Some(404),
|
||||||
Some("The page you tried to access could not be found."),
|
Some("The page you tried to access could not be found."),
|
||||||
|
&state.graph,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,27 +70,32 @@ pub async fn not_found() -> Response<Body> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{
|
use axum::{
|
||||||
http::{StatusCode},
|
http::{StatusCode},
|
||||||
|
extract::State,
|
||||||
};
|
};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn not_found() {
|
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);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn internal_error() {
|
async fn internal_error() {
|
||||||
assert!(by_code(Some(201), None).status() == 201);
|
let graph = Graph::load();
|
||||||
assert!(by_code(Some(304), None).status() == 304);
|
assert!(by_code(Some(201), None, &graph).status() == 201);
|
||||||
assert!(by_code(Some(418), None).status() == 418);
|
assert!(by_code(Some(304), None, &graph).status() == 304);
|
||||||
assert!(by_code(Some(505), None).status() == 505);
|
assert!(by_code(Some(418), None, &graph).status() == 418);
|
||||||
|
assert!(by_code(Some(505), None, &graph).status() == 505);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_message() {
|
fn custom_message() {
|
||||||
let pattern = "sibPtt0mvHPWS9HQ0YBQfGu8cUs954LZ";
|
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));
|
||||||
assert!(!body.contains(&pattern.chars().rev().collect::<String>()));
|
assert!(!body.contains(&pattern.chars().rev().collect::<String>()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
http::{Response, StatusCode, header, HeaderValue},
|
extract::{Path, State},
|
||||||
|
http::{HeaderValue, Response, StatusCode, header},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
router::handlers,
|
graph::{Format, Graph, SerialErrorCause},
|
||||||
graph::{Graph, Format},
|
router::{GlobalState, handlers},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
|
@ -41,22 +42,68 @@ pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::unused_async)]
|
pub async fn serial(
|
||||||
pub async fn serial(format: &Format) -> Response<Body> {
|
Path(format): Path<String>,
|
||||||
let graph = Graph::load();
|
State(state): State<GlobalState>,
|
||||||
let body = Graph::to_serial(&graph, format);
|
) -> Response<Body> {
|
||||||
|
let config = &state.graph.meta.config;
|
||||||
|
|
||||||
match *format {
|
let make_error = |code: u16, message: &str| -> Response<Body> {
|
||||||
Format::TOML => handlers::raw::make_response(
|
handlers::error::by_code(
|
||||||
&body,
|
Some(code),
|
||||||
200,
|
Some(
|
||||||
&[(header::CONTENT_TYPE, "text/plain")],
|
format!(
|
||||||
),
|
"<p>{message}</p>\n\
|
||||||
Format::JSON => handlers::raw::make_response(
|
<p>Check the <a href=/data>data</a> \n\
|
||||||
&body,
|
page for the available formats.</p>"
|
||||||
200,
|
)
|
||||||
&[(header::CONTENT_TYPE, "application/json")],
|
.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<Body> {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
async fn wrap_serial(format: &str) -> Response<Body> {
|
||||||
|
let state = GlobalState {
|
||||||
|
graph: Graph::load(),
|
||||||
|
};
|
||||||
|
serial(Path(format.to_string()), State(state)).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serial_toml() {
|
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);
|
assert!(response.status() == 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serial_toml_content_type() {
|
async fn serial_toml_content_type() {
|
||||||
let response = serial(&Format::TOML).await;
|
let response = wrap_serial("TOML").await;
|
||||||
assert!(
|
assert!(
|
||||||
response.headers().get(header::CONTENT_TYPE).unwrap()
|
response.headers().get(header::CONTENT_TYPE).unwrap()
|
||||||
== "text/plain"
|
== "text/plain"
|
||||||
|
|
@ -81,7 +141,7 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serial_json_content_type() {
|
async fn serial_json_content_type() {
|
||||||
let response = serial(&Format::JSON).await;
|
let response = wrap_serial("json").await;
|
||||||
assert!(
|
assert!(
|
||||||
response.headers().get(header::CONTENT_TYPE).unwrap()
|
response.headers().get(header::CONTENT_TYPE).unwrap()
|
||||||
== "application/json"
|
== "application/json"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
use axum::response::IntoResponse as _;
|
use axum::{
|
||||||
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
|
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<String>) -> Response<Body> {
|
pub async fn node(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(state): State<GlobalState>,
|
||||||
|
) -> Response<Body> {
|
||||||
let instant = now();
|
let instant = now();
|
||||||
let graph = Graph::load();
|
let result = state.graph.find_node(&id);
|
||||||
let result = graph.find_node(&id);
|
|
||||||
let found = result.node.is_some();
|
let found = result.node.is_some();
|
||||||
let node = result
|
let node = result
|
||||||
.node
|
.node
|
||||||
|
|
@ -25,20 +34,19 @@ pub async fn node(Path(id): Path<String>) -> Response<Body> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut context = tera::Context::default();
|
let mut context = tera::Context::default();
|
||||||
context.insert("graph", &graph);
|
context.insert("graph", &state.graph);
|
||||||
context.insert("node", &node);
|
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);
|
tlog!(&instant, "Assembled response for node {}", node.id);
|
||||||
handlers::template::by_filename(
|
handlers::template::with_context(
|
||||||
"node.html",
|
"node",
|
||||||
&context,
|
&context,
|
||||||
if found { 500 } else { 404 },
|
if found { 500 } else { 404 },
|
||||||
Some(
|
Some(
|
||||||
format!(
|
format!(
|
||||||
"Failed to generate page for node {} (ID {}).\n\
|
"Failed to generate page for node {} (ID {}).",
|
||||||
Node struct: <pre>{:#?}</pre>",
|
node.title, id
|
||||||
node.title, id, node
|
|
||||||
)
|
)
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
),
|
),
|
||||||
|
|
@ -52,17 +60,26 @@ mod tests {
|
||||||
http::{HeaderName, StatusCode},
|
http::{HeaderName, StatusCode},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::graph::Graph;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
async fn wrap_node(query: &str) -> Response<Body> {
|
||||||
|
let state = GlobalState {
|
||||||
|
graph: Graph::load(),
|
||||||
|
};
|
||||||
|
node(Path(query.to_string()), axum::extract::State(state)).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn syntax() {
|
async fn syntax() {
|
||||||
let response = node(Path("Syntax".to_string())).await;
|
let response = wrap_node("Syntax").await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn syntax_content_type() {
|
async fn syntax_content_type() {
|
||||||
let response = node(Path("Syntax".to_string())).await;
|
let response = wrap_node("Syntax").await;
|
||||||
assert!(
|
assert!(
|
||||||
response
|
response
|
||||||
.headers()
|
.headers()
|
||||||
|
|
@ -76,19 +93,19 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn not_found() {
|
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);
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn redirect() {
|
async fn redirect() {
|
||||||
let response = node(Path("syntax".to_string())).await;
|
let response = wrap_node("syntax").await;
|
||||||
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
|
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn docs_redirect() {
|
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);
|
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,45 @@
|
||||||
use axum::{
|
use axum::{Form, body::Body, extract::State, http::Response, response::Redirect};
|
||||||
body::Body,
|
|
||||||
http::{Response},
|
|
||||||
response::Redirect,
|
|
||||||
Form,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
graph::{Graph, Node},
|
router::{GlobalState, handlers},
|
||||||
router::handlers,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[expect(clippy::unused_async)]
|
pub async fn index(State(state): State<GlobalState>) -> Response<Body> {
|
||||||
pub async fn page(template: &str) -> Response<Body> {
|
handlers::template::with_graph("index", state).await
|
||||||
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 tree() -> Response<Body> {
|
pub async fn about(State(state): State<GlobalState>) -> Response<Body> {
|
||||||
let instant = now();
|
handlers::template::with_graph("about", state).await
|
||||||
let mut context = tera::Context::default();
|
}
|
||||||
let mut graph = Graph::load();
|
|
||||||
|
|
||||||
context.insert("graph", &graph);
|
pub async fn tree(State(state): State<GlobalState>) -> Response<Body> {
|
||||||
if let Some(root_node) = graph.get_root() {
|
let instant = now();
|
||||||
graph.nodes.remove(&root_node.id);
|
|
||||||
|
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("root_node", &root_node);
|
||||||
context.insert(
|
|
||||||
"nodes",
|
|
||||||
&graph.nodes.values().cloned().collect::<Vec<Node>>(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
context.insert(
|
|
||||||
"nodes",
|
|
||||||
&graph.nodes.values().cloned().collect::<Vec<Node>>(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog!(&instant, "Assembled response for tree endpoint");
|
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<Body> {
|
pub async fn data(State(state): State<GlobalState>) -> Response<Body> {
|
||||||
let instant = now();
|
let instant = now();
|
||||||
let mut context = tera::Context::default();
|
|
||||||
let graph = Graph::load();
|
|
||||||
|
|
||||||
let mut detached_pairs: Vec<(String, u32)> =
|
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));
|
detached_pairs.sort_by(|a, b| b.1.cmp(&a.1));
|
||||||
|
|
||||||
context.insert("graph", &graph);
|
let mut context = tera::Context::default();
|
||||||
context.insert("detached_count", &graph.stats.detached.len());
|
context.insert("graph", &state.graph);
|
||||||
|
context.insert("detached_count", &state.graph.stats.detached.len());
|
||||||
context.insert("detached_pairs", &detached_pairs);
|
context.insert("detached_pairs", &detached_pairs);
|
||||||
|
|
||||||
tlog!(&instant, "Assembled response for data endpoint");
|
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<Query>) -> Redirect {
|
pub async fn search(Form(query): Form<Query>) -> Redirect {
|
||||||
|
|
@ -79,11 +57,18 @@ pub struct Query {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{
|
use axum::http::StatusCode;
|
||||||
http::{StatusCode},
|
use crate::graph::Graph;
|
||||||
};
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
async fn wrap_page(path: &str) -> Response<Body> {
|
||||||
|
let state = GlobalState {
|
||||||
|
graph: Graph::load(),
|
||||||
|
};
|
||||||
|
handlers::template::with_graph(path, state).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn search_redirect() {
|
async fn search_redirect() {
|
||||||
let query = Form(Query {
|
let query = Form(Query {
|
||||||
|
|
@ -95,19 +80,19 @@ mod tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn about_page_ok() {
|
async fn about_page_ok() {
|
||||||
let response = page("about.html").await;
|
let response = wrap_page("about").await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn tree_page_ok() {
|
async fn tree_page_ok() {
|
||||||
let response = page("tree.html").await;
|
let response = wrap_page("tree").await;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn inexistent_page_error() {
|
async fn inexistent_page_error() {
|
||||||
let response = page("HBvcwqT8wLk6hxk1GdvNcEzJ6IiZ2Fod").await;
|
let response = wrap_page("HBvcwqT8wLk6hxk1GdvNcEzJ6IiZ2Fod").await;
|
||||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,28 @@ use axum::{
|
||||||
http::{header, Response, StatusCode},
|
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<Body> {
|
||||||
|
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,
|
name: &str,
|
||||||
context: &tera::Context,
|
context: &tera::Context,
|
||||||
error_code: u16,
|
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")])
|
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(
|
pub(in crate::router::handlers) fn render(
|
||||||
name: &str,
|
template: &str,
|
||||||
// TODO take Option, skip context if None,
|
// TODO take Option, skip context if None,
|
||||||
// then template_handler can replace static_template_handler
|
// then template_handler can replace static_template_handler
|
||||||
context: &tera::Context,
|
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) => {
|
Ok(t) => {
|
||||||
tlog!(&instant, "Rendered template {name}");
|
tlog!(&instant, "Rendered template {template}");
|
||||||
(t, 200)
|
(t, 200)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let mut error_context = tera::Context::default();
|
let mut error_context = tera::Context::default();
|
||||||
|
|
||||||
let out_error_message = match error_message {
|
let mut out_error_message = match error_message {
|
||||||
Some(s) => &format!(
|
Some(s) => format!(
|
||||||
"Template render failed.\n\
|
"Template render failed.\n\
|
||||||
User message: {s},
|
User message: {s},
|
||||||
Engine message:\n<pre>{e:#?}</pre>\n\
|
Engine message:\n<pre>{e:#?}</pre>"
|
||||||
Context:\n<pre>{context:#?}</pre>"
|
|
||||||
),
|
),
|
||||||
None => &format!(
|
None => format!(
|
||||||
"Template render failed.\n\
|
"Template render failed.\n\
|
||||||
Engine message:\n<pre>{e:#?}</pre>\n\
|
Engine message:\n<pre>{e:#?}</pre>"
|
||||||
Context:\n<pre>{context:#?}</pre>"
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
error_context.insert("message", out_error_message);
|
if log::env_level() >= VERBOSE {
|
||||||
|
out_error_message = format!(
|
||||||
|
"{out_error_message}\n\
|
||||||
|
Context:\n<pre>{context:#?}</pre>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log!(ERROR, "{out_error_message}");
|
||||||
|
error_context.insert("message", &out_error_message);
|
||||||
error_context.insert(
|
error_context.insert(
|
||||||
"title",
|
"title",
|
||||||
&StatusCode::INTERNAL_SERVER_ERROR.to_string(),
|
&StatusCode::INTERNAL_SERVER_ERROR.to_string(),
|
||||||
|
|
@ -113,31 +141,21 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn by_filename_forced_error() {
|
fn by_filename_forced_error() {
|
||||||
let response = by_filename(
|
let response =
|
||||||
"index.html",
|
with_context("index", &tera::Context::default(), 418, None, true);
|
||||||
&tera::Context::default(),
|
|
||||||
418,
|
|
||||||
None,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
assert_eq!(response.status(), 418);
|
assert_eq!(response.status(), 418);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn by_filename_index() {
|
fn by_filename_index() {
|
||||||
let response = by_filename(
|
let response =
|
||||||
"index.html",
|
with_context("index", &tera::Context::default(), 418, None, false);
|
||||||
&tera::Context::default(),
|
|
||||||
418,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
assert_eq!(response.status(), 200);
|
assert_eq!(response.status(), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn by_filename_file_not_found() {
|
fn by_filename_file_not_found() {
|
||||||
let response = by_filename(
|
let response = with_context(
|
||||||
"bwbl3BnWsluIgbO2NV9t3vtihwcjuF6t",
|
"bwbl3BnWsluIgbO2NV9t3vtihwcjuF6t",
|
||||||
&tera::Context::default(),
|
&tera::Context::default(),
|
||||||
418,
|
418,
|
||||||
|
|
@ -150,7 +168,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn by_filename_empty() {
|
fn by_filename_empty() {
|
||||||
let response =
|
let response =
|
||||||
by_filename("", &tera::Context::default(), 418, None, false);
|
with_context("", &tera::Context::default(), 418, None, false);
|
||||||
assert_eq!(response.status(), 500);
|
assert_eq!(response.status(), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,7 +181,7 @@ mod tests {
|
||||||
context.insert("node", &node);
|
context.insert("node", &node);
|
||||||
context.insert("graph", &graph);
|
context.insert("graph", &graph);
|
||||||
context.insert("incoming", &graph.incoming.get(&node.id));
|
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_eq!(status, 200);
|
||||||
assert!(body.matches(payload).count() == 1);
|
assert!(body.matches(payload).count() == 1);
|
||||||
}
|
}
|
||||||
|
|
@ -203,8 +221,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_bad_context() {
|
fn render_bad_context() {
|
||||||
let (body, status) =
|
let (body, status) = render("node", &tera::Context::default(), None);
|
||||||
render("node.html", &tera::Context::default(), None);
|
|
||||||
assert!(body.matches("Template render failed.").count() > 0);
|
assert!(body.matches("Template render failed.").count() > 0);
|
||||||
assert_eq!(status, 500);
|
assert_eq!(status, 500);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,11 @@ const LEXMAP: LexMap = &[
|
||||||
];
|
];
|
||||||
|
|
||||||
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput {
|
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput {
|
||||||
let mut instant = now();
|
|
||||||
let mut tokens: Vec<Token> = Vec::default();
|
let mut tokens: Vec<Token> = Vec::default();
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
|
|
||||||
let segments = segment::segment(text);
|
let segments = segment::segment(text);
|
||||||
let segments_count = segments.len();
|
|
||||||
instant = tlog!(&instant, "Segmented {segments_count} segments");
|
|
||||||
let lexemes = Lexeme::collect(&segments);
|
let lexemes = Lexeme::collect(&segments);
|
||||||
instant = tlog!(&instant, "{segments_count} segments: Collected lexemes");
|
|
||||||
|
|
||||||
log!(VERBOSE, "Segments: {segments:?}");
|
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);
|
context::close(&state, &mut tokens);
|
||||||
tlog!(&instant, "{segments_count} segments: Closed");
|
|
||||||
|
|
||||||
TokenOutput {
|
TokenOutput {
|
||||||
tokens,
|
tokens,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ pub mod delimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Delimiters {
|
impl Default for Delimiters {
|
||||||
fn default() -> Self {
|
fn default() -> Delimiters {
|
||||||
Delimiters {
|
Delimiters {
|
||||||
atomic: vec!['`', '|', '\\'],
|
atomic: vec!['`', '|', '\\'],
|
||||||
double: vec!['_', '~'],
|
double: vec!['_', '~'],
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@
|
||||||
{% if graph.meta.config.tree %}
|
{% if graph.meta.config.tree %}
|
||||||
<li><a href="/tree">Tree</a></li>
|
<li><a href="/tree">Tree</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if graph.meta.config.raw %}
|
|
||||||
<li><a href="/data">Data</a></li>
|
<li><a href="/data">Data</a></li>
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
{% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}
|
{% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}
|
||||||
<div class="nav-inputs">
|
<div class="nav-inputs">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set config = graph.meta.config %}
|
|
||||||
|
|
||||||
{% block title %}Data{% endblock title %}
|
{% block title %}Data{% endblock title %}
|
||||||
|
|
||||||
|
|
@ -31,15 +30,15 @@
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
{% if config.raw_toml or config.raw_json %}
|
{% if graph.meta.config.raw and (graph.meta.config.raw_toml or graph.meta.config.raw_json) %}
|
||||||
<h2>Raw formats</h2>
|
<h2>Raw formats</h2>
|
||||||
<p>The raw data used to render this graph is available in the following formats:</p>
|
<p>Structured data representing this graph is available in the following formats:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% if config.raw_toml %}
|
{% if graph.meta.config.raw_toml %}
|
||||||
<li><a href="/graph/toml">TOML</a></li>
|
<li><a href="/graph/toml">TOML</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if config.raw_json %}
|
{% if graph.meta.config.raw_json %}
|
||||||
<li><a href="/graph/json">JSON</a></li>
|
<li><a href="/graph/json">JSON</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
<p>There are no nodes. The graph is either empty or failed to parse.</p>
|
<p>There are no nodes. The graph is either empty or failed to parse.</p>
|
||||||
|
{% if graph.meta.messages %}
|
||||||
|
<p>Error messages:</p>
|
||||||
|
<pre>
|
||||||
|
{{ graph.meta.messages }}
|
||||||
|
</pre>
|
||||||
|
{% endif %}
|
||||||
{% if graph.meta.config.raw %}
|
{% if graph.meta.config.raw %}
|
||||||
<p>Check the
|
<p>Check the
|
||||||
{% if graph.meta.config.raw_toml %}
|
{% if graph.meta.config.raw_toml %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue