Merge serial module into the graph module

This commit is contained in:
Juno Takano 2026-01-13 12:11:51 -03:00
commit 697dcc720d
17 changed files with 421 additions and 332 deletions

View file

@ -1,8 +1,14 @@
use std::collections::HashMap;
use std::{collections::HashMap, path::PathBuf};
use serde::{Serialize, Deserialize};
use crate::syntax::content;
use crate::syntax::{
command::Arguments,
content::{
self,
parser::{flatten, Token, token::Anchor},
},
};
use crate::prelude::*;
pub use {
node::Node,
@ -46,17 +52,225 @@ impl std::fmt::Display for QueryResult {
}
impl Graph {
pub fn new(message: Option<&str>) -> Graph {
pub fn error(message: Option<&str>) -> Graph {
let graph = Graph::default();
Graph {
nodes: HashMap::default(),
root_node: "VoidNode".to_string(),
incoming: HashMap::default(),
lowercase_keymap: HashMap::default(),
meta: Meta {
config: Config::default(),
version: (0, 1, 0),
messages: message.map_or(vec![], |m| vec![m.to_string()]),
..graph.meta
},
..graph
}
}
/// Loads a TOML file from the default location and returns a modulated Graph
pub fn load() -> Graph {
Self::load_file("")
}
/// 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))
};
graph.modulate();
graph
}
/// Reads a TOML fie into a Graph without modulating it
pub fn read_file(in_path: Option<&str>) -> Graph {
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}"),
};
Self::from_serial(&toml_source, &Format::TOML)
}
pub fn from_serial(serial: &str, format: &Format) -> Graph {
match *format {
Format::TOML => match toml::from_str(serial) {
Ok(g) => g,
Err(error) => Graph::error(Some(&error.to_string())),
},
Format::JSON => match serde_json::from_str(serial) {
Ok(g) => g,
Err(error) => Graph::error(Some(&error.to_string())),
},
}
}
pub fn to_serial(graph: &Graph, format: &Format) -> String {
match *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 modulate(&mut self) {
self.map_lowercase_keys();
self.modulate_nodes();
self.modulate_edges();
self.map_incoming();
self.parse_config();
}
// Construct a HashMap with incoming connections (reversed edges)
fn map_incoming(&mut self) {
for node in self.nodes.clone().into_values() {
for edge in node.connections.clone().unwrap_or_default().values() {
let mut edges = self
.incoming
.get(&edge.to.clone())
.unwrap_or(&vec![])
.clone();
edges.extend_from_slice(std::slice::from_ref(edge));
self.incoming.insert(edge.to.clone(), edges.clone());
}
}
}
fn modulate_nodes(&mut self) {
let in_nodes = self.nodes.clone();
for (key, node) in in_nodes.clone() {
let connections = node.connections.clone().unwrap_or_default();
let mut new_edges = connections.clone();
// Modulate connections
for (connection_key, edge) in connections {
let mut new_edge = edge.clone();
// Populate empty "from" IDs in edges with node's ID
if edge.from.is_empty() {
new_edge.from.clone_from(&connection_key);
}
// Flag detached edges
if !in_nodes.contains_key(&edge.to) {
new_edge.detached = false;
}
if let Some(e) = new_edges.get_mut(&connection_key) {
*e = new_edge;
}
}
// Create connections for each link
for link in &node.links {
new_edges.insert(
link.clone(),
Edge {
from: key.clone(),
to: link.clone(),
detached: !in_nodes.clone().contains_key(link),
},
);
}
// Populate empty titles with IDs
let new_title = if node.title.is_empty() {
key.clone()
} else {
node.title.clone()
};
// Populate empty summaries with the leading part of the node text
let summary = if node.summary.is_empty() {
let first_line = if let Some(first) =
node.text.lines().find(|s| !s.is_empty())
{
String::from(first)
} else {
node.text.clone()
};
let mut candidate =
if let Some(dot_split) = first_line.split_once('.') {
format!("{}.", dot_split.0)
} else {
first_line
};
if candidate.len() > 300 {
candidate.truncate(300);
candidate.push('…');
}
candidate
} else {
node.summary.clone()
};
// Assemble new node
let new_node = Node {
id: key.clone(),
title: new_title,
summary: flatten(&summary, self),
connections: Some(new_edges),
..node.clone()
};
self.nodes.insert(key.clone(), new_node);
}
}
fn modulate_edges(&mut self) {
let graph = self.clone();
let iterator = self.nodes.iter_mut();
for (key, node) in iterator {
// Parse node text
let parse_output = content::rich_parse(&node.text, &graph);
node.text = parse_output.text.unwrap_or_default();
// Create connections for each anchor
let mut parsed_anchor_tokens = parse_output
.tokens
.iter()
.filter(|t| matches!(t, Token::Anchor(_)))
.cloned()
.collect::<Vec<Token>>();
parsed_anchor_tokens.extend_from_slice(
&parse_output
.format_tokens
.iter()
.filter(|t| matches!(t, Token::Anchor(_)))
.cloned()
.collect::<Vec<Token>>(),
);
let mut anchors: Vec<Anchor> = vec![];
for token in parsed_anchor_tokens {
if let Token::Anchor(token_data) = token {
anchors.push(*token_data.clone());
}
}
for anchor in anchors {
if let Some(anchor_node) = anchor.node() {
if let Some(ref mut connections) = node.connections {
connections.insert(
anchor_node.id.clone(),
Edge {
from: key.clone(),
to: anchor_node.id,
detached: false,
},
);
}
}
}
}
}
@ -124,7 +338,7 @@ impl Graph {
self.nodes.get(&self.root_node).cloned()
}
pub fn parse(&mut self) {
pub fn parse_config(&mut self) {
self.meta.config.footer_text =
content::parse(&self.meta.config.footer_text, self);
self.meta.config.about_text =
@ -132,13 +346,18 @@ impl Graph {
}
}
pub enum Format {
TOML,
JSON,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph() {
let graph = Graph::new(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"));
let graph = Graph::error(Some("ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"));
assert!(graph.nodes.is_empty());
assert!(graph.incoming.is_empty());
assert_eq!(
@ -146,4 +365,57 @@ mod tests {
"ISryQFd9peG6eYz9CFRQFWeD1GnPo0oj"
);
}
#[test]
fn good_json() {
let json = r#"
{
"nodes": {
"JSON": {
"text": "",
"title": "JSON",
"links": [],
"id": "JSON",
"hidden": false,
"connections": {}
}
},
"root_node": "JSON"
}
"#;
let graph = Graph::from_serial(json, &Format::JSON);
assert!(graph.meta.messages.is_empty());
}
#[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"));
}
}
#[cfg(test)]
mod serial_tests {
use super::*;
#[test]
fn bad_graph_path() {
let original_working_directory = std::env::current_dir().unwrap();
assert!(
std::env::set_current_dir(std::path::Path::new(
"tests/mocks/no_graph"
))
.is_ok()
);
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!(std::env::set_current_dir(original_working_directory).is_ok());
}
}

View file

@ -1,14 +1,24 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct Meta {
pub config: Config,
#[serde(default = "mkversion")]
pub version: (u8, u8, u8),
#[serde(default)]
pub version: Option<Version>,
#[serde(default)]
pub messages: Vec<String>,
}
impl Default for Meta {
fn default() -> Meta {
Meta {
config: Config::default(),
version: Version::from_env(),
messages: vec![],
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct Config {
#[serde(default)]
@ -99,25 +109,22 @@ fn mkfalse() -> bool {
fn mk8() -> u16 {
8
}
fn mkversion() -> (u8, u8, u8) {
(0, 0, 0)
}
#[cfg(test)]
mod tests {
use crate::graph::Graph;
use super::*;
use crate::syntax::serial::populate_graph;
#[test]
fn empty_footer_text() {
let mut graph = populate_graph();
let mut graph = Graph::load();
graph.meta.config = Config {
footer_text: String::default(),
..graph.meta.config
};
graph.parse();
graph.parse_config();
println!("{:?}", graph.meta.config.footer_text);
assert!(graph.meta.config.footer_text.is_empty());
@ -126,14 +133,14 @@ mod tests {
#[test]
fn config_footer_text() {
let payload = "0kqBrdS8NPrU4xVxh2xW0hUzAw926JCQ";
let mut graph = populate_graph();
let mut graph = Graph::load();
graph.meta.config = Config {
footer_text: format!("`{payload}`"),
..graph.meta.config
};
graph.parse();
graph.parse_config();
assert!(
graph
@ -149,14 +156,14 @@ mod tests {
#[test]
fn config_about_text() {
let payload = "ZqPFl84JlzSS0QUo61RwTUPONIE78Lmw";
let mut graph = populate_graph();
let mut graph = Graph::load();
graph.meta.config = Config {
about_text: format!("`{payload}`"),
..graph.meta.config
};
graph.parse();
graph.parse_config();
assert!(
graph
@ -169,3 +176,59 @@ mod tests {
);
}
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, Debug)]
pub struct Version {
major: u8,
minor: u8,
patch: u8,
}
impl Version {
pub fn from_env() -> Option<Version> {
Self::from(env!("CARGO_PKG_VERSION"))
}
pub fn from(version: &str) -> Option<Version> {
let triple: Vec<String> =
version.split('.').map(str::to_string).collect();
let has_two_dots = version.matches('.').count() == 2;
let has_three_elements = triple.len() == 3;
let has_whitespace = version.contains(' ') || version.contains('\n');
let has_contiguous_dots = version.contains("..");
let ends_with_dot = version.ends_with('.');
let starts_with_dot = version.starts_with('.');
let major: u8 = if let Some(s) = triple.first() {
s.trim_start_matches('v').trim().parse().ok()?
} else {
return None;
};
let minor: u8 = if let Some(s) = triple.get(1) {
s.trim().parse().ok()?
} else {
return None;
};
let patch: u8 = if let Some(s) = triple.get(2) {
s.trim().parse().ok()?
} else {
return None;
};
let conditions = has_two_dots
&& has_three_elements
&& !has_whitespace
&& !has_contiguous_dots
&& !ends_with_dot
&& !starts_with_dot;
conditions.then_some(Version {
major,
minor,
patch,
})
}
}

View file

@ -1,6 +1,6 @@
use std::{backtrace, io, panic};
use en::{prelude::*, ONSET, syntax::serial::populate_graph, syntax};
use en::{prelude::*, ONSET, graph::Graph, syntax};
#[tokio::main]
async fn main() -> io::Result<()> {
@ -34,7 +34,7 @@ async fn main() -> io::Result<()> {
}
}));
let graph = populate_graph();
let graph = Graph::load();
let router = en::router::new(&graph);
let listener =

View file

@ -1,6 +1,6 @@
use axum::{routing::get, Router};
use crate::{syntax::serial::Format, graph::Graph};
use crate::{graph::Format, graph::Graph};
mod handlers {
pub mod graph;
@ -69,8 +69,7 @@ pub fn new(graph: &Graph) -> Router {
#[cfg(test)]
mod tests {
use crate::{
syntax::serial::populate_graph,
graph::{Config, Meta},
graph::{Graph, Config, Meta},
};
use super::*;
@ -82,7 +81,7 @@ mod tests {
use tower::ServiceExt as _;
async fn request(uri: &str, config: Option<&Config>) -> Response<Body> {
let default_graph = populate_graph();
let default_graph = Graph::load();
let graph = Graph {
meta: Meta {
config: config

View file

@ -3,7 +3,7 @@ use axum::{
http::{Response, StatusCode, header},
};
use crate::{syntax::serial::populate_graph, router::handlers};
use crate::{graph::Graph, router::handlers};
pub(in crate::router::handlers) fn by_code(
code: Option<u16>,
@ -26,7 +26,7 @@ fn make_body(code: Option<u16>, message: Option<&str>) -> String {
let out_code = code.unwrap_or(500);
let out_message = &message.unwrap_or("Unknown error");
let config = populate_graph().meta.config;
let config = Graph::load().meta.config;
context.insert(
"title",

View file

@ -6,7 +6,7 @@ use axum::{
use crate::prelude::*;
use crate::{
router::handlers,
syntax::serial::{Format, populate_graph, serialize_graph},
graph::{Graph, Format},
};
/// # Panics
@ -35,8 +35,8 @@ pub async fn file(file_path: &str, content_type: &str) -> Response<Body> {
#[expect(clippy::unused_async)]
pub async fn serial(format: &Format) -> Response<Body> {
let graph = populate_graph();
let body = serialize_graph(format, &graph);
let graph = Graph::load();
let body = Graph::to_serial(&graph, format);
match *format {
Format::TOML => handlers::raw::make_response(

View file

@ -2,10 +2,10 @@ use axum::response::IntoResponse as _;
use axum::{body::Body, extract::Path, http::Response, response::Redirect};
use crate::graph::Edge;
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
use crate::{graph::Graph, router::handlers, graph::Node};
pub async fn node(Path(id): Path<String>) -> Response<Body> {
let graph = populate_graph();
let graph = Graph::load();
let result = graph.find_node(&id);
let found = result.node.is_some();
let nodes: Vec<Node> = graph.nodes.clone().into_values().collect();

View file

@ -5,12 +5,15 @@ use axum::{
Form,
};
use crate::{syntax::serial::populate_graph, router::handlers, graph::Node};
use crate::{
graph::{Graph, Node},
router::handlers,
};
#[expect(clippy::unused_async)]
pub async fn page(template: &str) -> Response<Body> {
let mut context = tera::Context::default();
let graph = populate_graph();
let graph = Graph::load();
let root_node = graph.get_root().unwrap_or_default();
let nodes: Vec<Node> = graph.nodes.into_values().collect();

View file

@ -103,6 +103,7 @@ fn emergency_wrap(error: &tera::Error) -> String {
#[cfg(test)]
mod tests {
use crate::graph::Graph;
use super::*;
@ -154,7 +155,7 @@ mod tests {
let payload = "dBgIw8DnNHxJojiXzu445qUC4UpxwZCy";
let mut context = tera::Context::default();
let node = crate::graph::Node::new(Some(payload.to_string()));
let graph = crate::syntax::serial::populate_graph();
let graph = Graph::load();
context.insert("node", &node);
context
.insert("text", &crate::syntax::content::parse(&node.text, &graph));

View file

@ -1,3 +1,2 @@
pub mod command;
pub mod content;
pub mod serial;

View file

@ -15,10 +15,16 @@ type Probe = fn(&Lexeme) -> bool;
type Lexer = fn(&Lexeme) -> Token;
type LexMap<'lm> = &'lm [(Probe, Lexer)];
pub struct TokenOutput {
pub text: Option<String>,
pub tokens: Vec<Token>,
pub format_tokens: Vec<Token>,
}
pub fn parse(text: &str, graph: &Graph) -> String {
parser::read(text, graph)
}
pub fn rich_parse(text: &str, graph: &Graph) -> (String, Vec<Token>) {
pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput {
parser::rich_read(text, graph)
}

View file

@ -1,5 +1,5 @@
use crate::{prelude::*, graph::Graph};
use super::{Parseable as _, LexMap};
use super::{TokenOutput, Parseable as _, LexMap};
use token::{LineBreak, Literal};
use context::{Block, Inline};
pub use {lexeme::Lexeme, token::Token, state::State};
@ -20,7 +20,7 @@ const LEXMAP: LexMap = &[
}),
];
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> Vec<Token> {
fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> TokenOutput {
let mut tokens: Vec<Token> = Vec::default();
let mut state = State::default();
@ -75,27 +75,37 @@ fn lex(text: &str, map: LexMap, graph: &Graph, blocking: bool) -> Vec<Token> {
}
context::close(&state, &mut tokens);
tokens
TokenOutput {
tokens,
format_tokens: state.format_tokens,
text: None,
}
}
pub(super) fn read(input: &str, graph: &Graph) -> String {
parse(&lex(input, LEXMAP, graph, true))
parse(&lex(input, LEXMAP, graph, true).tokens)
}
pub(super) fn rich_read(input: &str, graph: &Graph) -> (String, Vec<Token>) {
let tokens = lex(input, LEXMAP, graph, true);
let text = parse(&tokens);
(text, tokens)
pub(super) fn rich_read(input: &str, graph: &Graph) -> TokenOutput {
let lex_output = lex(input, LEXMAP, graph, true);
let text = parse(&lex_output.tokens);
TokenOutput {
text: Some(text),
tokens: lex_output.tokens,
format_tokens: lex_output.format_tokens,
}
}
/// Apply end-to-end point and inline parsing for nested formatting, such as
/// inside the display text of anchors and list items
pub fn format(input: &str, graph: &Graph) -> String {
parse(&lex(input, LEXMAP, graph, false))
pub fn format(input: &str, graph: &Graph) -> (String, Vec<Token>) {
let tokens = lex(input, LEXMAP, graph, false).tokens;
(parse(&tokens), tokens)
}
// Strip special syntax for display in noninteractive or plain-text display
pub fn flatten(input: &str, graph: &Graph) -> String {
let tokens = lex(input, LEXMAP, graph, true);
let tokens = lex(input, LEXMAP, graph, true).tokens;
let flat = tokens.iter().map(Token::flatten).collect::<String>();
log!("Flattened {tokens:?} to {flat}");
flat

View file

@ -67,6 +67,9 @@ pub fn parse(
} else if lexeme.match_char('|') && lexeme.is_next_delimiter() {
log!("End: Pipe followed by delimiter");
if buffer.destination.is_empty() {
if candidate.text().contains(':') {
candidate.set_external(true);
}
push(Some(&candidate.text().clone()), tokens, state, graph);
} else {
push(Some(&buffer.destination.clone()), tokens, state, graph);

View file

@ -48,7 +48,10 @@ pub fn parse(
}
if item_candidate.depth.is_some() {
// if the current item candidate has a known depth, push it
item_candidate.text = format(&item_candidate.text, graph);
let (text, format_tokens) =
format(&item_candidate.text, graph);
item_candidate.text = text;
state.format_tokens.extend_from_slice(&format_tokens);
candidate.items.push(item_candidate.clone());
}
// push list candidate, reset state and exit context
@ -60,7 +63,9 @@ pub fn parse(
} else if lexeme.match_char('\n') {
// found end of item, push it and reset state
log!("Accepting item candidate {item_candidate}");
item_candidate.text = format(&item_candidate.text, graph);
let (text, format_tokens) = format(&item_candidate.text, graph);
item_candidate.text = text;
state.format_tokens.extend_from_slice(&format_tokens);
candidate.items.push(item_candidate.clone());
*item_candidate = Item::default();
buffer.depth = 0;

View file

@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::syntax::content::parser::{
Token,
context::{Block, Context, Inline},
token::{Anchor, Item, List},
};
@ -11,6 +12,7 @@ pub struct State {
pub dom_ids: HashMap<String, Vec<String>>,
pub switches: Switches,
pub buffers: Buffers,
pub format_tokens: Vec<Token>,
}
#[derive(Clone, Debug)]
@ -91,6 +93,7 @@ impl Default for State {
depth: 0,
},
},
format_tokens: vec![],
}
}
}

View file

@ -1,275 +0,0 @@
use std::collections::HashMap;
use crate::{
syntax::{
command::Arguments,
content::{
self,
parser::{flatten, Token, token::Anchor},
},
},
graph::{Edge, Graph, Node},
};
pub fn populate_graph() -> Graph {
let args = Arguments::default().parse();
let toml_source = match std::fs::read_to_string(args.graph_path) {
Ok(s) => s,
Err(e) => format!("Error: {e}"),
};
let mut graph = deserialize_graph(&Format::TOML, &toml_source);
modulate_graph(&mut graph)
}
fn modulate_graph(in_graph: &mut Graph) -> Graph {
in_graph.map_lowercase_keys();
let nodes = modulate_nodes(in_graph);
let mut graph = Graph {
incoming: make_incoming(&nodes),
nodes,
..in_graph.clone()
};
graph.parse();
graph
}
fn modulate_nodes(graph: &Graph) -> HashMap<String, Node> {
let in_nodes = graph.nodes.clone();
let mut first_pass_nodes: HashMap<String, Node> = HashMap::default();
for (key, node) in in_nodes.clone() {
let connections = node.connections.clone().unwrap_or_default();
let mut new_edges = connections.clone();
// Modulate connections
for (connection_key, edge) in connections {
let mut new_edge = edge.clone();
// Populate empty "from" IDs in edges with node's ID
if edge.from.is_empty() {
new_edge.from.clone_from(&connection_key);
}
// Flag detached edges
if !in_nodes.contains_key(&edge.to) {
new_edge.detached = true;
}
if let Some(e) = new_edges.get_mut(&connection_key) {
*e = new_edge;
}
}
// Create connections for each link
for link in &node.links {
new_edges.insert(
link.clone(),
Edge {
from: key.clone(),
to: link.clone(),
detached: !in_nodes.clone().contains_key(link),
},
);
}
// Populate empty titles with IDs
let new_title = if node.title.is_empty() {
key.clone()
} else {
node.title.clone()
};
// Populate empty summaries with the leading part of the node text
let summary = if node.summary.is_empty() {
let first_line = if let Some(first) =
node.text.lines().find(|s| !s.is_empty())
{
String::from(first)
} else {
node.text.clone()
};
let mut candidate =
if let Some(dot_split) = first_line.split_once('.') {
format!("{}.", dot_split.0)
} else {
first_line
};
if candidate.len() > 300 {
candidate.truncate(300);
candidate.push('…');
}
candidate
} else {
node.summary.clone()
};
// Assemble new node
let new_node = Node {
id: key.clone(),
title: new_title,
summary: flatten(&summary, graph),
connections: Some(new_edges),
..node.clone()
};
first_pass_nodes.insert(key.clone(), new_node);
}
let mut second_pass_nodes: HashMap<String, Node> = HashMap::default();
for (key, node) in first_pass_nodes.clone() {
let first_pass_graph = Graph {
nodes: first_pass_nodes.clone(),
..graph.clone()
};
// Parse node text
let (text, tokens) = content::rich_parse(&node.text, &first_pass_graph);
// Create connections for each anchor
let parsed_anchors =
tokens.iter().filter(|t| matches!(t, Token::Anchor(_)));
let mut anchors: Vec<Anchor> = vec![];
for anchor in parsed_anchors {
if let Token::Anchor(a) = anchor {
anchors.push(*a.clone());
}
}
let mut new_edges = node.connections.clone().unwrap_or_default();
for anchor in anchors {
if let Some(anchor_node) = anchor.node() {
new_edges.insert(
anchor_node.id.clone(),
Edge {
from: key.clone(),
to: anchor_node.id,
detached: false,
},
);
}
}
// Assemble new node
let new_node = Node {
connections: Some(new_edges),
text,
..node.clone()
};
second_pass_nodes.insert(key.clone(), new_node);
}
second_pass_nodes
}
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: &str) -> 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())),
},
}
}
// Construct a HashMap with incoming connections (reversed edges)
fn make_incoming(nodes: &HashMap<String, Node>) -> HashMap<String, Vec<Edge>> {
let mut incoming: HashMap<String, Vec<Edge>> = HashMap::default();
for node in nodes.clone().into_values() {
let empty_vec: Vec<Edge> = vec![];
for edge in node.connections.clone().unwrap_or_default().values() {
let mut edges =
incoming.get(&edge.to.clone()).unwrap_or(&empty_vec).clone();
edges.extend_from_slice(std::slice::from_ref(edge));
incoming.insert(edge.to.clone(), edges.clone());
}
}
incoming
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn good_json() {
let json = r#"
{
"nodes": {
"JSON": {
"text": "",
"title": "JSON",
"links": [],
"id": "JSON",
"hidden": false,
"connections": {}
}
},
"root_node": "JSON"
}
"#;
let graph = deserialize_graph(&Format::JSON, json);
assert!(graph.meta.messages.is_empty());
}
#[test]
fn bad_json() {
let graph = deserialize_graph(&Format::JSON, ":::");
let message = graph.meta.messages.first().unwrap();
assert!(message.contains("expected value at line 1 column 1"));
}
}
#[cfg(test)]
mod serial_tests {
use super::*;
#[test]
fn bad_graph_path() {
let original_working_directory = std::env::current_dir().unwrap();
assert!(
std::env::set_current_dir(std::path::Path::new(
"tests/mocks/no_graph"
))
.is_ok()
);
let graph = populate_graph();
let message = graph.meta.messages.first().unwrap();
assert!(message.contains("TOML parse error"));
assert!(message.contains("No such file or directory"));
assert!(std::env::set_current_dir(original_working_directory).is_ok());
}
}

View file

@ -517,7 +517,7 @@ en is only possible thanks to a number of projects and people:
text = """
- [ ] Performance
- [ ] Caching
- [ ] Move more logic from Serial to Graph submodules
- [x] Move more logic from Serial to Graph submodules
- [ ] Further centralize state
- [ ] Reduce O(n) calls in the formats module
- [ ] Input syntax