1#![deny(missing_docs)]
2#![deny(unsafe_code)]
3#![deny(unstable_features)]
4
5use std::{
17 collections::HashSet,
18 hash::{DefaultHasher, Hash, Hasher},
19 string::ToString,
20};
21
22pub trait TreeVizNode
25where
26 Self: ToString,
27{
28 fn children(&self) -> Vec<Self>
30 where
31 Self: Sized;
32}
33
34pub fn draw_nodes<T: TreeVizNode + Hash>(graph_name: &str, node: T) -> String {
39 let graph_name: String = graph_name
40 .to_owned()
41 .chars()
42 .filter(|c| *c != ' ' && c.is_ascii())
43 .collect();
44 let mut out = format!("digraph {} {{", graph_name);
45 out = format!("{}\n{}", out, draw_node(None, node, &mut HashSet::new()));
46 out = out
47 .lines()
48 .map(|ln| ln.trim())
49 .filter(|ln| *ln != ";")
50 .collect();
51 out.push('}');
52 out
53}
54
55fn draw_node<T: TreeVizNode + Hash>(
56 parent_hash: Option<u64>,
57 node: T,
58 hashes: &mut HashSet<u64>,
59) -> String {
60 let mut hasher = DefaultHasher::new();
61 node.hash(&mut hasher);
62 let mut hash = hasher.finish();
63 while hashes.contains(&hash) {
64 hash += 1;
65 }
66 hashes.insert(hash);
67 let mut out = format!("{} [label=\"{}\"];\n", hash, sanitize(node.to_string()));
68 if let Some(parent) = parent_hash {
69 out = format!("{}{} -> {};\n", out, parent, hash);
70 }
71 for child in node.children() {
72 out = format!("{}{};\n", out, draw_node(Some(hash), child, hashes));
73 }
74 out
75}
76
77fn sanitize(s: String) -> String {
78 s.replace(|c: char| !c.is_ascii(), "")
79 .replace('"', "\\\"")
80 .chars()
81 .filter(|c| c.is_ascii())
82 .collect()
83}
84
85#[cfg(test)]
86mod tests;
87
88#[cfg(feature = "async")]
89use futures::future::join_all;
90#[cfg(feature = "async")]
91use std::sync::{Arc, Mutex};
92
93#[cfg(feature = "async")]
98pub async fn draw_nodes_async<T: TreeVizNode + Hash>(graph_name: &str, node: T) -> String {
99 let graph_name: String = graph_name
100 .to_owned()
101 .chars()
102 .filter(|c| *c != ' ' && c.is_ascii())
103 .collect();
104 let mut out = format!("digraph {} {{", graph_name);
105 out = format!(
106 "{}\n{}",
107 out,
108 draw_node_async(None, node, Arc::new(Mutex::new(HashSet::new()))).await
109 );
110 out = out
111 .lines()
112 .map(|ln| ln.trim())
113 .filter(|ln| *ln != ";")
114 .collect();
115 out.push('}');
116 out
117}
118
119#[cfg(feature = "async")]
120async fn draw_node_async<T: TreeVizNode + Hash>(
121 parent_hash: Option<u64>,
122 node: T,
123 hashes: Arc<Mutex<HashSet<u64>>>,
124) -> String {
125 let mut hasher = DefaultHasher::new();
126 node.hash(&mut hasher);
127 let mut hash = hasher.finish();
128 {
129 let mut _hashes = hashes.clone();
130 let mut hashesa = _hashes.lock().unwrap();
131 while hashesa.contains(&hash) {
132 hash += 1;
133 }
134 hashesa.insert(hash);
135 }
136 let mut out = format!("{} [label=\"{}\"];\n", hash, sanitize(node.to_string()));
137 if let Some(parent) = parent_hash {
138 out = format!("{}{} -> {};\n", out, parent, hash);
139 }
140 let promises = node
141 .children()
142 .into_iter()
143 .map(|child| draw_node_async(Some(hash), child, hashes.clone()));
144 out = format!(
145 "{}{}",
146 out,
147 join_all(promises)
148 .await
149 .into_iter()
150 .map(|s| format!("{};\n", s))
151 .collect::<String>()
152 );
153 out
154}