wowlab_docgen_cli/infra/
dep_graph.rs1use 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}