1use std::{
4 collections::{hash_map::DefaultHasher, HashMap},
5 env,
6 fs::File,
7 hash::{Hash, Hasher},
8 io::{self, Write},
9 path::Path,
10 sync::{LazyLock, Mutex},
11};
12
13use crate::{model::PlainModel, trim};
14mod render;
15pub use render::{Render, RenderError};
16
17#[derive(Debug)]
18pub struct ColorMap {
19 lookup: HashMap<usize, usize>,
20 colors: Vec<usize>,
21}
22impl Default for ColorMap {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl ColorMap {
29 pub fn new() -> Self {
30 Self {
31 lookup: HashMap::new(),
32 colors: (1usize..=8).collect(),
33 }
34 }
35 pub fn get(&mut self, rate: usize) -> usize {
36 *self.lookup.entry(rate).or_insert_with(|| {
37 let color = self.colors[0];
38 self.colors.rotate_left(1);
39 color
40 })
41 }
42}
43pub static COLORMAP: LazyLock<Mutex<ColorMap>> = LazyLock::new(|| Mutex::new(ColorMap::new()));
44
45#[derive(Debug, thiserror::Error)]
46pub enum GraphError {
47 #[error("failed to write Graphviz file")]
48 ToDot(#[from] io::Error),
49}
50
51#[derive(Debug, Default, Clone)]
52enum GraphTheme {
53 #[default]
54 Screen,
55 Paper,
56}
57impl GraphTheme {
58 pub fn new() -> Self {
59 match env::var("FLOWCHART_THEME") {
60 Ok(var) => match var.to_lowercase().as_str() {
61 "screen" => Self::Screen,
62 "paper" => Self::Paper,
63 _ => Self::default(),
64 },
65 Err(_) => Self::default(),
66 }
67 }
68 pub fn into_string(self, actors: String, inputs: String, outputs: String) -> String {
69 match self {
70 Self::Screen => format!(
71 r#"
72 digraph G {{
73 overlap = false;
74 splines = true;
75 bgcolor = gray24;
76 {{node [shape=box, width=0.75, margin="0.025", style="rounded,filled", fillcolor=lightgray]; {};}}
77 node [shape=point, fillcolor=gray24, color=lightgray];
78
79 /* Outputs */
80 {{
81 edge [arrowhead=none,colorscheme=dark28,fontsize=9, fontcolor=lightgray,fontname="times:italic"];
82 {}
83 }}
84 /* Inputs */
85 {{
86 edge [arrowhead=vee, colorscheme=dark28]
87 {}
88 }}
89 }}
90 "#,
91 actors, outputs, inputs,
92 ),
93 Self::Paper => format!(
94 r#"
95 digraph G {{
96 overlap = scale;
97 splines = true;
98 {{node [shape=box, width=0.75, margin="0.025", style="rounded,filled"]; {};}}
99 node [shape=point, fillcolor=gray24, color=lightgray];
100
101 /* Outputs */
102 {{
103 edge [arrowhead=none,colorscheme=dark28,fontsize=9,fontname="times:italic"];
104 {}
105 }}
106 /* Inputs */
107 {{
108 edge [arrowhead=vee, colorscheme=dark28]
109 {}
110 }}
111 }}
112 "#,
113 actors, outputs, inputs,
114 ),
115 }
116 }
117}
118
119#[derive(Debug, Hash, Default, Clone)]
135pub struct Graph {
136 pub(crate) name: String,
137 actors: PlainModel,
138 to_dot: bool,
139}
140impl Graph {
141 pub fn new(name: String, actors: impl Into<PlainModel>) -> Self {
142 let mut hasher = DefaultHasher::new();
143 let mut actors: PlainModel = actors.into();
144 actors.iter_mut().for_each(|actor| {
145 actor.client = trim(&actor.client);
146 actor.hash(&mut hasher);
147 actor.hash = hasher.finish();
148 });
149 Self {
150 name,
151 actors,
152 to_dot: env::var("TO_DOT").is_ok(),
153 }
154 }
155 pub fn to_string(&self) -> String {
157 let color_map = &*COLORMAP;
158 let inputs: Vec<_> = self
159 .actors
160 .iter()
161 .filter_map(|actor| {
162 actor.inputs.as_ref().map(|inputs| {
163 inputs
164 .iter()
165 .map(|input| {
166 let color = color_map.lock().unwrap().get(input.rate());
167 input.as_formatted_input(actor.hash, color)
168 })
169 .collect::<Vec<String>>()
170 })
171 })
172 .flatten()
173 .collect();
174 let outputs: Vec<_> = self
175 .actors
176 .iter()
177 .filter_map(|actor| {
178 actor.outputs.as_ref().map(|outputs| {
179 outputs
180 .iter()
181 .map(|output| {
182 let color = color_map.lock().unwrap().get(output.rate());
183 output.as_formatted_output(actor.hash, color)
184 })
185 .collect::<Vec<String>>()
186 })
187 })
188 .flatten()
189 .collect();
190 GraphTheme::new().into_string(
191 self.actors
192 .iter()
193 .map(|actor| {
194 if let Some(image) = actor.image.as_ref() {
195 format!(
196 r#"{} [label="{}", labelloc=t, image="{}"]"#,
197 actor.hash, actor.client, image
198 )
199 } else {
200 format!(r#"{} [label="{}"]"#, actor.hash, actor.client)
201 }
202 })
203 .collect::<Vec<String>>()
204 .join("; "),
205 outputs.join("\n"),
206 inputs.join("\n"),
207 )
208 }
209 pub fn to_dot(&self) -> std::result::Result<&Self, GraphError> {
211 if self.to_dot {
212 let data_repo = env::var("DATA_REPO").unwrap_or(".".into());
213 let path = Path::new(&data_repo).join(format!("{}.dot", self.name));
214 let mut file = File::create(&path)?;
215 write!(&mut file, "{}", self.to_string())?;
216 for actor in &self.actors {
217 if let Some(graph) = actor.graph.as_ref() {
218 graph.to_dot()?;
219 }
220 }
221 }
222 Ok(self)
223 }
224 pub fn walk(&self) -> Render {
225 let mut render = Render::from(self);
226 for actor in &self.actors {
227 if let Some(graph) = actor.graph.as_ref() {
228 render
229 .child
230 .get_or_insert(Vec::new())
231 .push(Box::new(graph.walk()));
232 }
233 }
234 log::debug!("{:}", render);
235 render
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::trim;
242
243 #[test]
244 fn parse_client_name() {
245 let a = trim("print");
246 dbg!(&a);
247 let a = trim("a::b::print");
248 dbg!(a);
249 let a = trim("a::b::print<w::W,q::s::C>");
250 dbg!(a);
251 }
253}