Skip to main content

wowlab_docgen_cli/context/
used_by.rs

1use serde::Serialize;
2use wowlab_fs::path::{Path, PathBuf};
3
4use crate::{
5    HasName, RenderCtx, display_name, infra::metadata, sort_by_name, workspace::normalize,
6};
7
8/// A crate that depends on the current package.
9#[derive(Debug, Serialize)]
10// #t(rust_similar_structs) reverse-dependency rows own relative paths while dependency rows own manifest paths
11pub(super) struct UsedByEntry {
12    pub display: String,
13    pub path: String,
14    pub description: String,
15}
16
17impl HasName for UsedByEntry {
18    fn name(&self) -> &str {
19        &self.display
20    }
21}
22
23/// Find workspace crates that depend on the current package via path dependencies.
24pub(super) fn gather(ctx: &RenderCtx<'_>) -> Vec<UsedByEntry> {
25    let crates_dir = ctx.root.join("crates");
26
27    let current_dir = normalize(ctx.dir);
28
29    let mut entries: Vec<UsedByEntry> = ctx
30        .workspace
31        .child_dirs(&crates_dir)
32        .filter_map(|crate_dir| {
33            let crate_metadata = metadata::load(&ctx.workspace, crate_dir);
34
35            let depends_on_us = crate_metadata
36                .workspace_deps
37                .values()
38                .any(|dependency| normalize(&crate_dir.join(&dependency.path)) == current_dir);
39
40            if !depends_on_us {
41                return None;
42            }
43
44            let name = crate_dir.file_name()?.to_string_lossy().to_string();
45
46            if name == ctx.dir.file_name()?.to_string_lossy() {
47                return None;
48            }
49
50            let display = display_name(&name);
51            let description = crate_metadata.description.clone().unwrap_or_default();
52
53            let rel = relative_path(ctx.dir, crate_dir);
54
55            Some(UsedByEntry {
56                display,
57                path: rel,
58                description,
59            })
60        })
61        .collect();
62
63    sort_by_name(&mut entries);
64
65    entries
66}
67
68fn relative_path(from_dir: &Path, to_dir: &Path) -> String {
69    if let (Some(_), Some(to_name)) = (
70        from_dir.file_name().map(|n| n.to_string_lossy()),
71        to_dir.file_name().map(|n| n.to_string_lossy()),
72    ) {
73        if from_dir.parent() == to_dir.parent() {
74            return format!("../{to_name}");
75        }
76    }
77
78    let from = normalize(from_dir);
79    let to = normalize(to_dir);
80    let common = common_ancestor(&from, &to);
81    let ups = from
82        .strip_prefix(&common)
83        .map_or(1, |path| path.components().count());
84    let tail = to
85        .strip_prefix(&common)
86        .map(|path| path.to_string_lossy().to_string())
87        .unwrap_or_default();
88
89    format!("{}{tail}", "../".repeat(ups))
90}
91
92fn common_ancestor(a: &Path, b: &Path) -> PathBuf {
93    let mut common = PathBuf::new();
94
95    for (ac, bc) in a.components().zip(b.components()) {
96        if ac == bc {
97            common.push(ac.as_os_str());
98        } else {
99            break;
100        }
101    }
102
103    common
104}
105
106#[cfg(test)]
107mod tests {
108    use googletest::prelude::*;
109
110    use super::*;
111    use crate::{WorkspaceFile, WorkspaceIndex};
112
113    #[gtest]
114    fn common_ancestor_walks_shared_prefix() -> Result<()> {
115        let cases = [
116            ("/ws/crates/common", "/ws/crates/sentinel", "/ws/crates"),
117            ("/ws/crates/a", "/ws/crates/a", "/ws/crates/a"),
118            ("/ws/crates/a/b", "/ws/crates/a", "/ws/crates/a"),
119            ("/ws/a", "/other/b", "/"),
120            ("a/b", "a/c", "a"),
121        ];
122
123        for (a, b, expected) in cases {
124            let got = common_ancestor(Path::new(a), Path::new(b));
125
126            verify_that!(got.to_string_lossy().as_ref(), eq(expected))?;
127        }
128
129        Ok(())
130    }
131
132    #[gtest]
133    fn relative_path_siblings() -> Result<()> {
134        let from = Path::new("/ws/crates/common");
135        let to = Path::new("/ws/crates/sentinel");
136
137        verify_eq!(relative_path(from, to), "../sentinel")
138    }
139
140    #[gtest]
141    fn gather_finds_reverse_edges_from_workspace_inherited_dependencies() -> Result<()> {
142        let root = Path::new("/workspace");
143        let shared_dir = root.join("crates/shared");
144        let workspace = WorkspaceIndex::new(
145            root.to_path_buf(),
146            vec![
147                WorkspaceFile {
148                    path: root.join("crates/Cargo.toml"),
149                    contents: Some(
150                        r#"
151[workspace]
152
153[workspace.dependencies]
154wowlab-shared = { path = "shared" }
155"#
156                        .into(),
157                    ),
158                },
159                WorkspaceFile {
160                    path: root.join("crates/member/Cargo.toml"),
161                    contents: Some(
162                        r#"
163[package]
164name = "wowlab-member"
165description = "workspace member"
166
167[dependencies]
168wowlab-shared.workspace = true
169"#
170                        .into(),
171                    ),
172                },
173                WorkspaceFile {
174                    path: shared_dir.join("Cargo.toml"),
175                    contents: Some(
176                        r#"
177[package]
178name = "wowlab-shared"
179description = "shared crate"
180"#
181                        .into(),
182                    ),
183                },
184            ],
185        );
186        let metadata = workspace.metadata(&shared_dir).or_fail()?;
187        let context = RenderCtx {
188            workspace: workspace.clone(),
189            root,
190            dir: &shared_dir,
191            rel_dir: "crates/shared",
192            metadata,
193            output_stem: "CLAUDE",
194        };
195
196        let used_by = gather(&context);
197
198        verify_that!(used_by, len(eq(1)))?;
199        verify_eq!(used_by[0].display, "member")?;
200
201        verify_eq!(used_by[0].path, "../member")
202    }
203}