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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
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<Graph, String> {
|
||||
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<Graph, String> {
|
||||
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<Graph, SerialError> {
|
||||
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::<Graph>(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::<Graph>(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<String, SerialError> {
|
||||
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<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)]
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ pub struct Meta {
|
|||
pub version: Option<Version>,
|
||||
#[serde(default)]
|
||||
pub messages: Vec<String>,
|
||||
#[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<Version> {
|
||||
Self::from(env!("CARGO_PKG_VERSION"))
|
||||
Version::from(env!("CARGO_PKG_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 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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u16>,
|
||||
message: Option<&str>,
|
||||
graph: &Graph,
|
||||
) -> Response<Body> {
|
||||
let out_code = code.unwrap_or(500);
|
||||
let out_message = &message.unwrap_or("Unknown error");
|
||||
|
||||
let body = make_body(Some(out_code), Some(out_message));
|
||||
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<u16>, message: Option<&str>) -> String {
|
||||
fn make_body(
|
||||
code: Option<u16>,
|
||||
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<u16>, 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<u16>, message: Option<&str>) -> String {
|
|||
.0
|
||||
}
|
||||
|
||||
pub async fn not_found() -> Response<Body> {
|
||||
pub async fn not_found(State(state): State<GlobalState>) -> Response<Body> {
|
||||
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<Body> {
|
|||
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::<String>()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Body> {
|
|||
response
|
||||
}
|
||||
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn serial(format: &Format) -> Response<Body> {
|
||||
let graph = Graph::load();
|
||||
let body = Graph::to_serial(&graph, format);
|
||||
pub async fn serial(
|
||||
Path(format): Path<String>,
|
||||
State(state): State<GlobalState>,
|
||||
) -> Response<Body> {
|
||||
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<Body> {
|
||||
handlers::error::by_code(
|
||||
Some(code),
|
||||
Some(
|
||||
format!(
|
||||
"<p>{message}</p>\n\
|
||||
<p>Check the <a href=/data>data</a> \n\
|
||||
page for the available formats.</p>"
|
||||
)
|
||||
.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 {
|
||||
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]
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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<String>) -> Response<Body> {
|
||||
pub async fn node(
|
||||
Path(id): Path<String>,
|
||||
State(state): State<GlobalState>,
|
||||
) -> Response<Body> {
|
||||
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<String>) -> Response<Body> {
|
|||
}
|
||||
|
||||
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: <pre>{:#?}</pre>",
|
||||
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<Body> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Body> {
|
||||
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<GlobalState>) -> Response<Body> {
|
||||
handlers::template::with_graph("index", state).await
|
||||
}
|
||||
|
||||
pub async fn tree() -> Response<Body> {
|
||||
let instant = now();
|
||||
let mut context = tera::Context::default();
|
||||
let mut graph = Graph::load();
|
||||
pub async fn about(State(state): State<GlobalState>) -> Response<Body> {
|
||||
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<GlobalState>) -> Response<Body> {
|
||||
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::<Vec<Node>>(),
|
||||
);
|
||||
} else {
|
||||
context.insert(
|
||||
"nodes",
|
||||
&graph.nodes.values().cloned().collect::<Vec<Node>>(),
|
||||
);
|
||||
}
|
||||
|
||||
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 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<Query>) -> 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<Body> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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,
|
||||
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<pre>{e:#?}</pre>\n\
|
||||
Context:\n<pre>{context:#?}</pre>"
|
||||
Engine message:\n<pre>{e:#?}</pre>"
|
||||
),
|
||||
None => &format!(
|
||||
None => format!(
|
||||
"Template render failed.\n\
|
||||
Engine message:\n<pre>{e:#?}</pre>\n\
|
||||
Context:\n<pre>{context:#?}</pre>"
|
||||
Engine message:\n<pre>{e:#?}</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(
|
||||
"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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Token> = 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,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub mod delimiter {
|
|||
}
|
||||
|
||||
impl Default for Delimiters {
|
||||
fn default() -> Self {
|
||||
fn default() -> Delimiters {
|
||||
Delimiters {
|
||||
atomic: vec!['`', '|', '\\'],
|
||||
double: vec!['_', '~'],
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@
|
|||
{% if graph.meta.config.tree %}
|
||||
<li><a href="/tree">Tree</a></li>
|
||||
{% endif %}
|
||||
{% if graph.meta.config.raw %}
|
||||
<li><a href="/data">Data</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if graph.meta.config.node_selector or graph.meta.config.navbar_search %}
|
||||
<div class="nav-inputs">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% set config = graph.meta.config %}
|
||||
|
||||
{% block title %}Data{% endblock title %}
|
||||
|
||||
|
|
@ -31,15 +30,15 @@
|
|||
</table>
|
||||
</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>
|
||||
<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>
|
||||
{% if config.raw_toml %}
|
||||
{% if graph.meta.config.raw_toml %}
|
||||
<li><a href="/graph/toml">TOML</a></li>
|
||||
{% endif %}
|
||||
{% if config.raw_json %}
|
||||
{% if graph.meta.config.raw_json %}
|
||||
<li><a href="/graph/json">JSON</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
<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 %}
|
||||
<p>Check the
|
||||
{% if graph.meta.config.raw_toml %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue