Skip to main content

wowlab_docgen_cli/infra/
dep_graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use wowlab_common::mermaid;
4use wowlab_fs::path::Path;
5
6use crate::{display_name, infra::cargo_metadata};
7
8#[must_use]
9pub fn build(workspace: &crate::WorkspaceIndex) -> String {
10    workspace.dependency_graph().to_string()
11}
12
13pub fn load(root: &Path) -> String {
14    let Ok(meta) = cargo_metadata::load(root) else {
15        return String::new();
16    };
17
18    let member_ids: BTreeSet<&str> = meta.workspace_members.iter().map(AsRef::as_ref).collect();
19
20    let mut id_to_name: BTreeMap<&str, String> = BTreeMap::new();
21
22    for pkg in &meta.packages {
23        if member_ids.contains(pkg.id.as_str()) {
24            id_to_name.insert(&pkg.id, display_name(&pkg.name));
25        }
26    }
27
28    let mut graph = mermaid::Graph::new(mermaid::Direction::LR).nodes(id_to_name.values().cloned());
29
30    if let Some(resolve) = &meta.resolve {
31        for node in &resolve.nodes {
32            let Some(from_name) = id_to_name.get(node.id.as_ref()) else {
33                continue;
34            };
35
36            for dep in &node.deps {
37                let Some(to_name) = id_to_name.get(dep.pkg.as_ref()) else {
38                    continue;
39                };
40
41                if from_name != to_name {
42                    graph = graph.edge(from_name, to_name);
43                }
44            }
45        }
46    }
47
48    graph.build()
49}
50
51#[cfg(test)]
52mod tests {
53    use googletest::prelude::*;
54
55    use super::*;
56
57    #[gtest]
58    fn parse_cargo_metadata_json() -> Result<()> {
59        let json = r#"{
60            "packages": [
61                {"id": "wowlab-common 0.1.0 (path+file:///ws/crates/common)", "name": "wowlab-common"},
62                {"id": "wowlab-engine 0.1.0 (path+file:///ws/crates/engine)", "name": "wowlab-engine"}
63            ],
64            "workspace_members": [
65                "wowlab-common 0.1.0 (path+file:///ws/crates/common)",
66                "wowlab-engine 0.1.0 (path+file:///ws/crates/engine)"
67            ],
68            "resolve": {
69                "nodes": [
70                    {
71                        "id": "wowlab-common 0.1.0 (path+file:///ws/crates/common)",
72                        "deps": []
73                    },
74                    {
75                        "id": "wowlab-engine 0.1.0 (path+file:///ws/crates/engine)",
76                        "deps": [{"pkg": "wowlab-common 0.1.0 (path+file:///ws/crates/common)", "name": "wowlab_common"}]
77                    }
78                ]
79            }
80        }"#;
81
82        let meta: cargo_metadata::CargoMetadata = serde_json::from_str(json).or_fail()?;
83
84        verify_eq!(meta.packages.len(), 2)?;
85        verify_eq!(meta.workspace_members.len(), 2)?;
86        let nodes = &meta.resolve.or_fail()?.nodes;
87
88        verify_eq!(nodes.len(), 2)?;
89
90        verify_eq!(nodes[1].deps.len(), 1)
91    }
92
93    #[gtest]
94    fn empty_for_no_edges() -> Result<()> {
95        let json = r#"{
96            "packages": [{"id": "a", "name": "wowlab-standalone"}],
97            "workspace_members": ["a"],
98            "resolve": {"nodes": [{"id": "a", "deps": []}]}
99        }"#;
100        let meta: cargo_metadata::CargoMetadata = serde_json::from_str(json).or_fail()?;
101
102        verify_eq!(meta.resolve.or_fail()?.nodes[0].deps.len(), 0)
103    }
104}