Here goes something
This commit is contained in:
commit
a7d944bbd4
16 changed files with 2700 additions and 0 deletions
97
src/formats.rs
Normal file
97
src/formats.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::*;
|
||||
|
||||
pub enum Format {
|
||||
Toml,
|
||||
Json
|
||||
}
|
||||
|
||||
pub fn serialize_graph(out_format: Format, graph: &Graph) -> String {
|
||||
|
||||
match out_format {
|
||||
Format::Toml => {
|
||||
match toml::to_string(graph) {
|
||||
Ok(s) => s,
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
},
|
||||
Format::Json => {
|
||||
match serde_json::to_string(graph) {
|
||||
Ok(s) => s,
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize_graph(in_format: Format, serial: &String) -> Graph {
|
||||
|
||||
match in_format {
|
||||
Format::Toml => { match toml::from_str(&serial) {
|
||||
Ok(g) => g,
|
||||
Err(error) => Graph::new(Some(error.to_string()))
|
||||
}},
|
||||
Format::Json => { match serde_json::from_str(&serial) {
|
||||
Ok(g) => g,
|
||||
Err(error) => Graph::new(Some(error.to_string()))
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn populate_graph() -> Graph {
|
||||
let toml_source = match std::fs::read_to_string("./static/graph.toml") {
|
||||
Ok(s) => s,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
let graph = deserialize_graph(Format::Toml, &toml_source);
|
||||
let mut new_nodes: HashMap<String, Node> = HashMap::new();
|
||||
let mut incoming: HashMap<String, Vec<Edge>> = HashMap::new();
|
||||
|
||||
// If an edge has no "from" ID, default to its node's ID
|
||||
for (key, node) in graph.nodes.iter() {
|
||||
|
||||
let mut new_node = node.clone();
|
||||
let connections = node.connections.clone().unwrap_or_default();
|
||||
|
||||
for (i, edge) in connections.iter().enumerate() {
|
||||
if edge.from == "" {
|
||||
let new_edge = Edge {
|
||||
from: key.to_string(),
|
||||
..edge.clone()
|
||||
};
|
||||
let mut vec = connections.clone();
|
||||
vec[i] = new_edge;
|
||||
new_node = Node {
|
||||
connections: Some(vec),
|
||||
..node.clone()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
new_nodes.insert(key.to_string(), new_node.clone());
|
||||
}
|
||||
|
||||
// Construct a HashMap with incoming connections (reversed edges)
|
||||
for node in new_nodes.clone().into_values() {
|
||||
|
||||
let empty_vec: Vec<Edge> = vec![];
|
||||
for edge in node.connections.clone().unwrap_or_default().iter() {
|
||||
|
||||
let vec = incoming.get(&edge.to.clone()).unwrap_or(&empty_vec);
|
||||
if vec.contains(edge) {
|
||||
vec.clone().extend_from_slice(&[edge.clone()]);
|
||||
incoming.insert(edge.to.clone(), vec.clone());
|
||||
} else {
|
||||
incoming.insert(edge.to.clone(), vec![edge.clone()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Graph {
|
||||
nodes: new_nodes,
|
||||
incoming: incoming,
|
||||
..graph
|
||||
}
|
||||
|
||||
}
|
||||
203
src/main.rs
Normal file
203
src/main.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
use axum::{
|
||||
extract::Path,
|
||||
http::{header, StatusCode},
|
||||
response::{ Html, IntoResponse, Redirect },
|
||||
routing::get,
|
||||
Form,
|
||||
Router,
|
||||
};
|
||||
|
||||
mod types;
|
||||
mod formats;
|
||||
|
||||
use formats::*;
|
||||
use types::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(index).post(query))
|
||||
.route("/graph/toml", get(toml_graph))
|
||||
.route("/graph/json", get(json_graph))
|
||||
.route("/static/style.css", get(stylesheet))
|
||||
.route("/node/{node_id}", get(node_view).post(node_view))
|
||||
.route("/tree", get(tree))
|
||||
.fallback(not_found)
|
||||
;
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
fn make_body(
|
||||
name: &str,
|
||||
context: tera::Context,
|
||||
error_code: u16,
|
||||
error_message: &str,
|
||||
) -> String {
|
||||
|
||||
let tera = tera::Tera::new(
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"),
|
||||
).unwrap_or(tera::Tera::default());
|
||||
|
||||
let mut error_context = tera::Context::new();
|
||||
let error = StatusCode::from_u16(error_code)
|
||||
.unwrap_or(StatusCode::NOT_IMPLEMENTED);
|
||||
error_context.insert("title", &error.to_string());
|
||||
error_context.insert(
|
||||
"message",
|
||||
&format!("Error while filling template {}: {}", name, error_message),
|
||||
);
|
||||
|
||||
tera.render(name, &context)
|
||||
.unwrap_or(tera.render("error.html", &error_context)
|
||||
.unwrap_or(error_message.to_string()))
|
||||
}
|
||||
|
||||
|
||||
fn template_handler(
|
||||
name: &str,
|
||||
context: tera::Context,
|
||||
error_code: u16,
|
||||
error_message: &str,
|
||||
) -> Html<String> {
|
||||
let body = make_body(name, context, error_code, error_message);
|
||||
Html(body)
|
||||
}
|
||||
|
||||
async fn node_view(Path(id): Path<String>) -> impl IntoResponse {
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
|
||||
let graph = populate_graph();
|
||||
let nodes = graph.nodes;
|
||||
let empty_node = Node::new(
|
||||
Some(format!("Could not find node with ID {}.", id)),
|
||||
);
|
||||
|
||||
let node: &Node = nodes.get(&id).unwrap_or(&empty_node);
|
||||
|
||||
context.insert("id", &node.id);
|
||||
context.insert("title", &node.title);
|
||||
context.insert("body", &node.body);
|
||||
context.insert("connections", &node.connections.clone());
|
||||
context.insert("incoming", &graph.incoming.get(&node.id));
|
||||
|
||||
template_handler(
|
||||
"node.html",
|
||||
context,
|
||||
500,
|
||||
&format!(
|
||||
r#"Failed to generate page for node {} (ID {}) with {} outgoing,
|
||||
{} incoming connections and body "{}""#,
|
||||
node.title,
|
||||
node.id,
|
||||
node.connections.iter().len(),
|
||||
graph.incoming.get(&node.id).iter().len(),
|
||||
node.body,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
async fn index() -> Html<String> {
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
let graph = populate_graph();
|
||||
let root_node = graph.get_root();
|
||||
let nodes: Vec<Node> = graph.nodes.into_values().collect();
|
||||
|
||||
context.insert("nodes", &nodes);
|
||||
context.insert("root_node", &root_node);
|
||||
|
||||
template_handler("index.html", context, 500, "Failed to render template.")
|
||||
}
|
||||
|
||||
async fn tree() -> Html<String> {
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
let graph = populate_graph();
|
||||
let root_node = graph.get_root();
|
||||
let nodes: Vec<Node> = graph.nodes.into_values().collect();
|
||||
|
||||
context.insert("nodes", &nodes);
|
||||
context.insert("root_node", &root_node);
|
||||
|
||||
template_handler("tree.html", context, 500, "Failed to render template")
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Query { node: String }
|
||||
|
||||
async fn query(Form(query): Form<Query>) -> Redirect {
|
||||
Redirect::permanent(format!("/node/{}", query.node).as_str())
|
||||
}
|
||||
|
||||
async fn json_graph() -> impl IntoResponse {
|
||||
let graph = populate_graph();
|
||||
let body = serialize_graph(Format::Json, &graph);
|
||||
|
||||
([(header::CONTENT_TYPE, "application/json")], body)
|
||||
}
|
||||
|
||||
async fn toml_graph() -> impl IntoResponse {
|
||||
let graph = populate_graph();
|
||||
let body = serialize_graph(Format::Toml, &graph);
|
||||
|
||||
([(header::CONTENT_TYPE, "text/plain")], body)
|
||||
}
|
||||
|
||||
async fn stylesheet() -> impl IntoResponse {
|
||||
let body = match std::fs::read_to_string("./static/style.css") {
|
||||
Ok(s) => s,
|
||||
Err(e) => format!("Error: {e}"),
|
||||
};
|
||||
|
||||
([(header::CONTENT_TYPE, "text/css")], body)
|
||||
}
|
||||
|
||||
fn make_error_body(
|
||||
code: Option<u16>,
|
||||
message: Option<&str>,
|
||||
) -> String {
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
|
||||
let code = code.unwrap_or(501);
|
||||
let message = &message.unwrap_or("Unknown error");
|
||||
|
||||
context.insert("title", &StatusCode::from_u16(code)
|
||||
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR).to_string());
|
||||
context.insert("message", message);
|
||||
context.insert("status_code", &code.to_string());
|
||||
|
||||
make_body("error.html", context, 500, &format!(
|
||||
"Failed to render template for Error {}: {}",
|
||||
code,
|
||||
message,
|
||||
))
|
||||
}
|
||||
|
||||
fn make_error_response(
|
||||
code: Option<u16>,
|
||||
message: Option<&str>,
|
||||
) -> impl IntoResponse {
|
||||
|
||||
let code = code.unwrap_or(501);
|
||||
let message = &message.unwrap_or("Unknown error");
|
||||
|
||||
let body = make_error_body(Some(code), Some(message));
|
||||
|
||||
(
|
||||
StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
[(header::CONTENT_TYPE, "text/html")],
|
||||
body.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn not_found() -> impl IntoResponse {
|
||||
make_error_response(
|
||||
Some(404),
|
||||
Some("The page you tried to access could not be found."),
|
||||
)
|
||||
}
|
||||
62
src/types.rs
Normal file
62
src/types.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Graph {
|
||||
pub messages: Vec<String>,
|
||||
pub root_node: String,
|
||||
pub nodes: HashMap<String, Node>,
|
||||
#[serde(skip)]
|
||||
pub incoming: HashMap<String, Vec<Edge>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Default, PartialEq, Deserialize)]
|
||||
pub struct Edge {
|
||||
#[serde(default)]
|
||||
pub anchor: String,
|
||||
#[serde(default)]
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct Node {
|
||||
pub title: String,
|
||||
pub id: String,
|
||||
pub body: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub connections: Option<Vec<Edge>>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
pub fn new(message: Option<String>) -> Graph {
|
||||
Self {
|
||||
nodes: HashMap::new(),
|
||||
incoming: HashMap::new(),
|
||||
root_node: "".to_string(),
|
||||
messages: vec![message
|
||||
.unwrap_or("This graph is empty or in error".to_string())],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> Option<Node> {
|
||||
match self.nodes.get(&self.root_node) {
|
||||
Some(n) => Some(n.clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(message: Option<String>) -> Node {
|
||||
Self {
|
||||
title: "Empty Node".to_string(),
|
||||
id: "EmptyNode".to_string(),
|
||||
body: match message {
|
||||
Some(s) => s,
|
||||
None => "Node is empty, missing or wasn't found.".to_string()
|
||||
},
|
||||
connections: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue