include_graph/dependencies/
graph.rs

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    // unique id
16    pub id: String,
17
18    // actual file this references
19    pub path: PathBuf,
20
21    // mapped name for display
22    pub display_name: String,
23}
24
25/// A group of related items.
26///
27/// MAY also be a singular item inside, however a graph is generally
28/// a group of named items
29#[derive(Debug, PartialEq, Serialize)]
30pub struct Group {
31    /// nice display name
32    pub name: String,
33
34    /// If this groop is zoomed in
35    pub zoomed: bool,
36
37    // some color name to use
38    pub color: String,
39
40    /// what are the nodes
41    pub nodes: HashSet<MappedNode>,
42}
43
44impl Group {
45    /// re-creates a new version of the group with all unique IDs changed new
46    ///
47    /// returns a brand new unique id for the group as well as a remade version
48    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(), // TODO: nicer colors?
64            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>, // specific color for a link
105    pub is_bold: bool,         // should the link color be bold?
106}
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    /// Actual graph being built
161    graph: Graph,
162
163    /// known path maps, keyed for fast access to the mapped name
164    path_maps: HashMap<PathBuf, PathMapping>,
165
166    /// map a group name
167    group_name_to_id: HashMap<String, String>,
168
169    /// where nodes are placed
170    placement_maps: HashMap<PathBuf, LinkNode>,
171
172    /// What graphs are focused zoomed. Remove links that span non-focused
173    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        // Get every single possible grouping
190        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    // final consumption of self to build the graph
280    pub fn build(mut self) -> Graph {
281        // Group all items without links
282        let known_placement = self.placement_maps.keys().collect::<HashSet<_>>();
283
284        // create a single group of all node-ids that have no links ... to see stand-alone items
285        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        // figure out zoomed items;
297        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        // zoom changed now
327        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        // Create group links only here
367        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                // have to create a stand-alone group
401                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                    // Generally this means someone created a `manual group` however source file was not
498                    // loaded, for example loading sources from compile_database but not all files are compiled
499                    // by this build run
500                    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}