1use std::{
2 collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
4};
5
6use serde::Serialize;
7use tera::{Context, Tera};
8use tokio::io::{AsyncWrite, AsyncWriteExt, BufWriter};
9use tracing::{debug, error};
10
11use super::{error::Error, gn::GnTarget, path_mapper::PathMapping};
12
13#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14pub struct MappedNode {
15 pub id: String,
17
18 pub path: PathBuf,
20
21 pub display_name: String,
23}
24
25#[derive(Debug, PartialEq, Serialize)]
30pub struct Group {
31 pub name: String,
33
34 pub zoomed: bool,
36
37 pub color: String,
39
40 pub nodes: HashSet<MappedNode>,
42}
43
44impl Group {
45 pub fn zoomed(&self, id_map: &mut HashMap<String, String>) -> Self {
49 let mut nodes = HashSet::new();
50
51 for n in self.nodes.iter() {
52 let new_id = format!("z{}", n.id);
53 nodes.insert(MappedNode {
54 id: new_id.clone(),
55 path: n.path.clone(),
56 display_name: n.display_name.clone(),
57 });
58 id_map.insert(n.id.clone(), new_id);
59 }
60 Self {
61 name: format!("{} (ZOOM)", self.name),
62 zoomed: true,
63 color: self.color.clone(), nodes,
65 }
66 }
67}
68
69#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)]
70pub struct LinkNode {
71 pub group_id: String,
72 pub node_id: Option<String>,
73}
74
75impl LinkNode {
76 pub fn without_node(&self) -> LinkNode {
77 if self.node_id.is_none() {
78 self.clone()
79 } else {
80 LinkNode {
81 group_id: self.group_id.clone(),
82 node_id: None,
83 }
84 }
85 }
86
87 pub fn try_remap(&self, m: &HashMap<String, String>) -> Option<Self> {
88 let node_id = match self.node_id {
89 Some(ref id) => Some(m.get(id)?.clone()),
90 None => None,
91 };
92
93 Some(Self {
94 group_id: m.get(&self.group_id)?.clone(),
95 node_id,
96 })
97 }
98}
99
100#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)]
101pub struct GraphLink {
102 pub from: LinkNode,
103 pub to: LinkNode,
104 pub color: Option<String>, pub is_bold: bool, }
107
108impl GraphLink {
109 pub fn try_remap(&self, m: &HashMap<String, String>) -> Option<Self> {
110 Some(Self {
111 from: self.from.try_remap(m)?,
112 to: self.to.try_remap(m)?,
113 ..self.clone()
114 })
115 }
116}
117
118#[derive(Debug, Default, Serialize)]
119pub struct Graph {
120 groups: HashMap<String, Group>,
121 links: HashSet<GraphLink>,
122 zoomed: HashSet<String>,
123}
124
125impl Graph {
126 pub async fn write_dot<D>(&self, dest: D) -> Result<(), Error>
127 where
128 D: AsyncWrite + Unpin,
129 {
130 let mut writer = BufWriter::new(dest);
131
132 let mut tera = Tera::default();
133 tera.add_raw_template("dot_template", include_str!("dot.template"))
134 .map_err(Error::RenderError)?;
135
136 writer
137 .write(
138 tera.render(
139 "dot_template",
140 &Context::from_serialize(self).map_err(Error::RenderError)?,
141 )
142 .map_err(Error::RenderError)?
143 .to_string()
144 .as_bytes(),
145 )
146 .await
147 .map_err(|source| Error::AsyncIOError {
148 source,
149 message: "Error writing.",
150 })?;
151 writer.flush().await.map_err(|source| Error::AsyncIOError {
152 source,
153 message: "Error flushing writer.",
154 })
155 }
156}
157
158#[derive(Debug, Default)]
159pub struct GraphBuilder {
160 graph: Graph,
162
163 path_maps: HashMap<PathBuf, PathMapping>,
165
166 group_name_to_id: HashMap<String, String>,
168
169 placement_maps: HashMap<PathBuf, LinkNode>,
171
172 focus_zoomed: HashSet<String>,
174}
175
176impl GraphBuilder {
177 pub fn new(paths: impl Iterator<Item = PathMapping>) -> Self {
178 Self {
179 path_maps: paths.map(|v| (v.from.clone(), v)).collect(),
180 ..Default::default()
181 }
182 }
183
184 pub fn known_path(&self, path: &Path) -> bool {
185 self.path_maps.contains_key(path)
186 }
187
188 pub fn group_extensions(&mut self, extensions: &[&str]) {
189 let groups = self
191 .path_maps
192 .keys()
193 .map(|p| p.with_extension(""))
194 .collect::<HashSet<_>>()
195 .into_iter()
196 .map(|stem| {
197 extensions
198 .iter()
199 .map(|e| stem.with_extension(e))
200 .filter(|p| self.known_path(p))
201 .filter(|p| !self.placement_maps.contains_key(p))
202 .collect::<Vec<_>>()
203 })
204 .filter(|e| e.len() > 1)
205 .collect::<Vec<_>>();
206
207 for group in groups {
208 let mut name = self
209 .path_maps
210 .get(group.first().expect("size at least 2"))
211 .expect("known")
212 .to
213 .clone();
214
215 if let Some(idx) = name.rfind('.') {
216 let (prefix, _) = name.split_at(idx);
217 name = String::from(prefix);
218 }
219 self.define_group(&name, "aqua", group);
220 }
221 }
222
223 pub fn color_from(&mut self, group_name: &str, color: &str, is_bold: bool) {
224 let group_id = match self.group_name_to_id.get(group_name) {
225 Some(id) => id,
226 None => {
227 error!("Group {} does not exist. Cannot color.", group_name);
228 return;
229 }
230 };
231
232 let keys = self
233 .graph
234 .links
235 .iter()
236 .filter(|l| &l.from.group_id == group_id)
237 .filter(|l| l.color.is_none())
238 .cloned()
239 .collect::<Vec<_>>();
240
241 for k in keys {
242 self.graph.links.remove(&k);
243 self.graph.links.insert(GraphLink {
244 color: Some(color.into()),
245 is_bold,
246 ..k
247 });
248 }
249 }
250
251 pub fn color_to(&mut self, group_name: &str, color: &str, is_bold: bool) {
252 let group_id = match self.group_name_to_id.get(group_name) {
253 Some(id) => id,
254 None => {
255 error!("Group {} does not exist. Cannot color.", group_name);
256 return;
257 }
258 };
259
260 let keys = self
261 .graph
262 .links
263 .iter()
264 .filter(|l| &l.to.group_id == group_id)
265 .filter(|l| l.color.is_none())
266 .cloned()
267 .collect::<Vec<_>>();
268
269 for k in keys {
270 self.graph.links.remove(&k);
271 self.graph.links.insert(GraphLink {
272 color: Some(color.into()),
273 is_bold,
274 ..k
275 });
276 }
277 }
278
279 pub fn build(mut self) -> Graph {
281 let known_placement = self.placement_maps.keys().collect::<HashSet<_>>();
283
284 let no_link_nodes = self
286 .path_maps
287 .keys()
288 .filter(|k| !known_placement.contains(*k))
289 .cloned()
290 .collect::<Vec<_>>();
291
292 if !no_link_nodes.is_empty() {
293 self.define_group("NO DEPENDENCIES OR GROUPS", "gray85", no_link_nodes);
294 }
295
296 let mut link_map = HashMap::new();
298
299 let mut new_groups = Vec::new();
300
301 let mut zoom_colors = [
302 "powderblue",
303 "peachpuff",
304 "thistle",
305 "honeydew",
306 "khaki",
307 "lavender",
308 ]
309 .iter()
310 .cycle();
311
312 for (id, group) in self
313 .graph
314 .groups
315 .iter()
316 .filter(|(id, _)| self.graph.zoomed.contains(*id))
317 {
318 let new_id = format!("z{}", id);
319 link_map.insert(id.clone(), new_id.clone());
320 new_groups.push((new_id, {
321 let mut z = group.zoomed(&mut link_map);
322 z.color = zoom_colors.next().expect("infinite").to_string();
323 z
324 }));
325 }
326 self.graph.zoomed = new_groups.iter().map(|(id, _)| id.clone()).collect();
328 self.graph.groups.extend(new_groups);
329
330 let zoom_links = self
331 .graph
332 .links
333 .iter()
334 .filter(|l| {
335 link_map.contains_key(&l.from.group_id) && link_map.contains_key(&l.to.group_id)
336 })
337 .filter_map(|l| {
338 if !(self.focus_zoomed.is_empty()
339 || l.from.group_id == l.to.group_id
340 || self.focus_zoomed.contains(&l.from.group_id)
341 || self.focus_zoomed.contains(&l.to.group_id))
342 {
343 return None;
344 }
345
346 let mut link = match l.try_remap(&link_map) {
347 Some(value) => value,
348 None => {
349 error!("FAILED TO REMAP: {:?}", l);
350 return None;
351 }
352 };
353
354 if l.from.group_id != l.to.group_id {
355 if self.focus_zoomed.contains(&l.to.group_id) {
356 link.color = Some("maroon".into());
357 } else if self.focus_zoomed.contains(&l.from.group_id) {
358 link.color = Some("darkblue".into());
359 }
360 }
361
362 Some(link)
363 })
364 .collect::<HashSet<_>>();
365
366 let links = self
368 .graph
369 .links
370 .iter()
371 .map(|l| GraphLink {
372 from: l.from.without_node(),
373 to: l.to.without_node(),
374 ..l.clone()
375 })
376 .filter(|l| l.from != l.to)
377 .collect::<HashSet<_>>();
378
379 self.graph.links = {
380 let mut v = HashSet::new();
381 v.extend(links);
382 v.extend(zoom_links);
383 v
384 };
385 self.graph
386 }
387
388 fn ensure_link_node(&mut self, path: &Path) -> Option<LinkNode> {
389 let full_location = match self.placement_maps.get(path) {
390 Some(location) => location,
391 None => {
392 let mapped_name = match self.path_maps.get(path) {
393 Some(mapping) => mapping.to.clone(),
394 None => {
395 error!("Unexpected missing mapping for {:?}", path);
396 return None;
397 }
398 };
399
400 self.define_group(&mapped_name, "thistle", [path]);
402 self.placement_maps.get(path).expect("just created a group")
403 }
404 };
405
406 Some(full_location.clone())
407 }
408
409 pub fn add_link(&mut self, from: &Path, to: &Path) {
410 let from = match self.ensure_link_node(from) {
411 Some(p) => p,
412 None => {
413 debug!("NOT MAPPED: {:?}", from);
414 return;
415 }
416 };
417
418 let to = match self.ensure_link_node(to) {
419 Some(p) => p,
420 None => {
421 debug!("NOT MAPPED: {:?}", to);
422 return;
423 }
424 };
425
426 if from == to {
427 return;
428 }
429
430 self.graph.links.insert(GraphLink {
431 from,
432 to,
433 color: None,
434 is_bold: false,
435 });
436 }
437
438 pub fn add_groups_from_gn(
439 &mut self,
440 gn_groups: Vec<GnTarget>,
441 ignore_targets: HashSet<String>,
442 ) {
443 for target in gn_groups
444 .into_iter()
445 .filter(|g| !ignore_targets.contains(&g.name))
446 {
447 let items = target
448 .sources
449 .into_iter()
450 .filter(|p| self.known_path(p))
451 .collect::<Vec<_>>();
452 if !items.is_empty() {
453 self.define_group(&target.name, "lightgreen", items);
454 }
455 }
456 }
457
458 pub fn define_group<T, P>(&mut self, group_name: &str, color: &str, items: T)
459 where
460 T: IntoIterator<Item = P>,
461 P: AsRef<Path>,
462 {
463 if self.group_name_to_id.contains_key(group_name) {
464 error!("Group {:?} already exists", group_name);
465 return;
466 }
467
468 let mut g = Group {
469 zoomed: false,
470 name: group_name.into(),
471 color: color.into(),
472 nodes: HashSet::default(),
473 };
474 let group_id = format!("grp_{}", uuid::Uuid::now_v6(&[1, 0, 0, 0, 0, 0]))
475 .to_string()
476 .replace('-', "_");
477
478 for path in items {
479 let path = path.as_ref();
480 if let Some(placement) = self.placement_maps.get(path) {
481 let duplicate_pos = self
482 .graph
483 .groups
484 .get(&placement.group_id)
485 .map(|g| g.name.clone())
486 .unwrap_or_else(|| format!("ID: {}", placement.group_id));
487 error!(
488 "{:?} in both: {:?} and {:?}",
489 path, group_name, duplicate_pos
490 );
491 continue;
492 }
493
494 let m = match self.path_maps.get(path) {
495 Some(m) => m,
496 None => {
497 error!("{:?} is a source file without a map entry. Cannot add to group (is this a loaded source file?).", path);
501 continue;
502 }
503 };
504
505 let node_id = format!(
506 "node_{}",
507 uuid::Uuid::now_v6(&[0, 0, 0, 0, 0, g.nodes.len() as u8])
508 )
509 .to_string()
510 .replace('-', "_");
511 g.nodes.insert(MappedNode {
512 id: node_id.clone(),
513 path: PathBuf::from(path),
514 display_name: m.to.clone(),
515 });
516
517 self.placement_maps.insert(
518 PathBuf::from(path),
519 LinkNode {
520 group_id: group_id.clone(),
521 node_id: Some(node_id),
522 },
523 );
524 }
525
526 if g.nodes.is_empty() {
527 error!("Group {:?} is empty. Will not create.", group_name);
528 return;
529 }
530
531 self.group_name_to_id
532 .insert(group_name.into(), group_id.clone());
533 self.graph.groups.insert(group_id, g);
534 }
535
536 pub fn zoom_in(&mut self, group: &str, focused: bool) {
537 let id = match self.group_name_to_id.get(group) {
538 Some(id) => id,
539 None => {
540 error!("Group {:?} was NOT found", group);
541 return;
542 }
543 };
544
545 self.graph.zoomed.insert(id.clone());
546 if focused {
547 self.focus_zoomed.insert(id.clone());
548 }
549 }
550}