#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct DepGraphExportConfig {
pub graph_name: String,
pub directed: bool,
pub pretty: bool,
}
#[derive(Debug, Clone)]
pub struct DepNode {
pub id: String,
pub label: String,
pub node_type: String,
}
#[derive(Debug, Clone)]
pub struct DepEdge {
pub from: String,
pub to: String,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct DepGraphExport {
pub nodes: Vec<DepNode>,
pub edges: Vec<DepEdge>,
pub total_bytes: usize,
}
pub fn default_dep_graph_export_config() -> DepGraphExportConfig {
DepGraphExportConfig {
graph_name: "asset_deps".to_string(),
directed: true,
pretty: true,
}
}
pub fn new_dep_graph_export() -> DepGraphExport {
DepGraphExport {
nodes: Vec::new(),
edges: Vec::new(),
total_bytes: 0,
}
}
pub fn dep_graph_add_node(graph: &mut DepGraphExport, node: DepNode) {
graph.nodes.push(node);
}
pub fn dep_graph_add_edge(graph: &mut DepGraphExport, edge: DepEdge) {
graph.edges.push(edge);
}
pub fn dep_graph_to_dot(graph: &mut DepGraphExport, cfg: &DepGraphExportConfig) -> String {
let kw = if cfg.directed { "digraph" } else { "graph" };
let arrow = if cfg.directed { "->" } else { "--" };
let mut out = format!("{} {} {{\n", kw, cfg.graph_name);
for n in &graph.nodes {
out.push_str(&format!(
" \"{}\" [label=\"{}\" type=\"{}\"];\n",
n.id, n.label, n.node_type
));
}
for e in &graph.edges {
if e.label.is_empty() {
out.push_str(&format!(" \"{}\" {} \"{}\";\n", e.from, arrow, e.to));
} else {
out.push_str(&format!(
" \"{}\" {} \"{}\" [label=\"{}\"];\n",
e.from, arrow, e.to, e.label
));
}
}
out.push('}');
graph.total_bytes = out.len();
out
}
pub fn dep_graph_to_json(graph: &mut DepGraphExport, cfg: &DepGraphExportConfig) -> String {
let indent = if cfg.pretty { " " } else { "" };
let nl = if cfg.pretty { "\n" } else { "" };
let mut out = format!("{{{nl}");
out.push_str(&format!("{indent}\"nodes\":[{nl}"));
for (i, n) in graph.nodes.iter().enumerate() {
let comma = if i + 1 < graph.nodes.len() { "," } else { "" };
out.push_str(&format!(
"{indent}{indent}{{\"id\":\"{}\",\"label\":\"{}\",\"type\":\"{}\"}}{}{nl}",
n.id, n.label, n.node_type, comma
));
}
out.push_str(&format!("{indent}],{nl}"));
out.push_str(&format!("{indent}\"edges\":[{nl}"));
for (i, e) in graph.edges.iter().enumerate() {
let comma = if i + 1 < graph.edges.len() { "," } else { "" };
out.push_str(&format!(
"{indent}{indent}{{\"from\":\"{}\",\"to\":\"{}\",\"label\":\"{}\"}}{}{nl}",
e.from, e.to, e.label, comma
));
}
out.push_str(&format!("{indent}]{nl}}}"));
graph.total_bytes = out.len();
out
}
pub fn dep_graph_node_count(graph: &DepGraphExport) -> usize {
graph.nodes.len()
}
pub fn dep_graph_edge_count(graph: &DepGraphExport) -> usize {
graph.edges.len()
}
pub fn dep_graph_write_to_file(
graph: &mut DepGraphExport,
cfg: &DepGraphExportConfig,
_path: &str,
) -> usize {
let dot = dep_graph_to_dot(graph, cfg);
graph.total_bytes = dot.len();
graph.total_bytes
}
pub fn dep_graph_clear(graph: &mut DepGraphExport) {
graph.nodes.clear();
graph.edges.clear();
graph.total_bytes = 0;
}
fn make_node(id: &str, label: &str, node_type: &str) -> DepNode {
DepNode {
id: id.to_string(),
label: label.to_string(),
node_type: node_type.to_string(),
}
}
fn make_edge(from: &str, to: &str, label: &str) -> DepEdge {
DepEdge {
from: from.to_string(),
to: to.to_string(),
label: label.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_dep_graph_export_config();
assert!(cfg.directed);
assert!(cfg.pretty);
assert_eq!(cfg.graph_name, "asset_deps");
}
#[test]
fn new_graph_is_empty() {
let g = new_dep_graph_export();
assert_eq!(dep_graph_node_count(&g), 0);
assert_eq!(dep_graph_edge_count(&g), 0);
}
#[test]
fn add_node_increments_count() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Body Mesh", "mesh"));
assert_eq!(dep_graph_node_count(&g), 1);
}
#[test]
fn add_edge_increments_count() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
dep_graph_add_node(&mut g, make_node("n2", "Texture", "texture"));
dep_graph_add_edge(&mut g, make_edge("n1", "n2", "uses"));
assert_eq!(dep_graph_edge_count(&g), 1);
}
#[test]
fn dot_output_contains_digraph() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
let cfg = default_dep_graph_export_config();
let dot = dep_graph_to_dot(&mut g, &cfg);
assert!(dot.contains("digraph"));
assert!(dot.contains("n1"));
}
#[test]
fn dot_undirected_uses_graph_keyword() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
let cfg = DepGraphExportConfig {
graph_name: "deps".to_string(),
directed: false,
pretty: true,
};
let dot = dep_graph_to_dot(&mut g, &cfg);
assert!(dot.contains("graph deps"));
assert!(!dot.contains("digraph"));
}
#[test]
fn json_output_contains_nodes_and_edges() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
dep_graph_add_edge(&mut g, make_edge("n1", "n2", ""));
let cfg = default_dep_graph_export_config();
let json = dep_graph_to_json(&mut g, &cfg);
assert!(json.contains("\"nodes\""));
assert!(json.contains("\"edges\""));
}
#[test]
fn write_to_file_sets_total_bytes() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
let cfg = default_dep_graph_export_config();
let n = dep_graph_write_to_file(&mut g, &cfg, "/tmp/deps.dot");
assert!(n > 0);
assert_eq!(g.total_bytes, n);
}
#[test]
fn clear_resets_state() {
let mut g = new_dep_graph_export();
dep_graph_add_node(&mut g, make_node("n1", "Mesh", "mesh"));
dep_graph_add_edge(&mut g, make_edge("n1", "n2", ""));
let cfg = default_dep_graph_export_config();
dep_graph_write_to_file(&mut g, &cfg, "/tmp/deps.dot");
dep_graph_clear(&mut g);
assert_eq!(dep_graph_node_count(&g), 0);
assert_eq!(dep_graph_edge_count(&g), 0);
assert_eq!(g.total_bytes, 0);
}
}