sampo_core/
workspace.rs

1use crate::adapters::PackageAdapter;
2use crate::errors::WorkspaceError;
3use crate::types::Workspace;
4use std::path::{Path, PathBuf};
5
6type Result<T> = std::result::Result<T, WorkspaceError>;
7
8/// Discover workspace packages using registered ecosystem adapters.
9pub fn discover_workspace(start_dir: &Path) -> Result<Workspace> {
10    let mut root = None;
11    let mut all_members = Vec::new();
12
13    // Try each registered adapter (static dispatch, zero-cost)
14    for adapter in PackageAdapter::all() {
15        if adapter.can_discover(start_dir) {
16            // Find the workspace root by walking up from start_dir
17            let discovered_root = find_workspace_root_for_adapter(start_dir, *adapter)?;
18
19            // Discover packages in this ecosystem
20            let packages = adapter.discover(&discovered_root)?;
21
22            // Use the first discovered root as the workspace root
23            root.get_or_insert(discovered_root);
24            all_members.extend(packages);
25        }
26    }
27
28    let workspace_root = root.ok_or(WorkspaceError::NotFound)?;
29
30    Ok(Workspace {
31        root: workspace_root,
32        members: all_members,
33    })
34}
35
36/// Find the workspace root for a given adapter by walking up the directory tree.
37fn find_workspace_root_for_adapter(start_dir: &Path, adapter: PackageAdapter) -> Result<PathBuf> {
38    let mut current = start_dir;
39    loop {
40        if adapter.can_discover(current) {
41            return Ok(current.to_path_buf());
42        }
43        current = current.parent().ok_or(WorkspaceError::NotFound)?;
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::types::PackageKind;
51    use std::fs;
52
53    #[test]
54    fn discover_workspace_finds_cargo_packages() {
55        let temp = tempfile::tempdir().unwrap();
56        let root = temp.path();
57
58        // Create workspace
59        fs::write(
60            root.join("Cargo.toml"),
61            "[workspace]\nmembers = [\"crates/*\"]\n",
62        )
63        .unwrap();
64
65        // Create crates
66        let crates_dir = root.join("crates");
67        fs::create_dir_all(crates_dir.join("pkg-a")).unwrap();
68        fs::create_dir_all(crates_dir.join("pkg-b")).unwrap();
69        fs::write(
70            crates_dir.join("pkg-a/Cargo.toml"),
71            "[package]\nname = \"pkg-a\"\nversion = \"0.1.0\"\n",
72        )
73        .unwrap();
74        fs::write(
75            crates_dir.join("pkg-b/Cargo.toml"),
76            "[package]\nname = \"pkg-b\"\nversion = \"0.2.0\"\n",
77        )
78        .unwrap();
79
80        let ws = discover_workspace(root).unwrap();
81        assert_eq!(ws.members.len(), 2);
82
83        let mut names: Vec<_> = ws.members.iter().map(|p| p.name.as_str()).collect();
84        names.sort();
85        assert_eq!(names, vec!["pkg-a", "pkg-b"]);
86    }
87
88    #[test]
89    fn discover_workspace_detects_internal_deps() {
90        let temp = tempfile::tempdir().unwrap();
91        let root = temp.path();
92
93        // workspace
94        fs::write(
95            root.join("Cargo.toml"),
96            "[workspace]\nmembers = [\"crates/*\"]\n",
97        )
98        .unwrap();
99
100        // crates: x depends on y via path, and on z via workspace
101        let crates_dir = root.join("crates");
102        fs::create_dir_all(crates_dir.join("x")).unwrap();
103        fs::create_dir_all(crates_dir.join("y")).unwrap();
104        fs::create_dir_all(crates_dir.join("z")).unwrap();
105        fs::write(
106            crates_dir.join("x/Cargo.toml"),
107            format!(
108                "{}{}{}",
109                "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
110                "[dependencies]\n",
111                "y={ path=\"../y\" }\n z={ workspace=true }\n"
112            ),
113        )
114        .unwrap();
115        fs::write(
116            crates_dir.join("y/Cargo.toml"),
117            "[package]\nname=\"y\"\nversion=\"0.1.0\"\n",
118        )
119        .unwrap();
120        fs::write(
121            crates_dir.join("z/Cargo.toml"),
122            "[package]\nname=\"z\"\nversion=\"0.1.0\"\n",
123        )
124        .unwrap();
125
126        let ws = discover_workspace(root).unwrap();
127        let x = ws.members.iter().find(|c| c.name == "x").unwrap();
128        assert!(x.internal_deps.contains("cargo/y"));
129        assert!(x.internal_deps.contains("cargo/z"));
130    }
131
132    #[test]
133    fn discover_workspace_returns_only_cargo_packages() {
134        // This test verifies that when a workspace is discovered, only Cargo packages
135        // are returned (since that's currently the only supported ecosystem).
136        // In the future, when more ecosystems are added, this test demonstrates that
137        // the abstraction correctly aggregates packages from multiple discoverers.
138
139        let temp = tempfile::tempdir().unwrap();
140        let root = temp.path();
141
142        // Create a Cargo workspace
143        fs::write(
144            root.join("Cargo.toml"),
145            "[workspace]\nmembers = [\"crates/*\"]\n",
146        )
147        .unwrap();
148
149        let crates_dir = root.join("crates");
150        fs::create_dir_all(crates_dir.join("cargo-pkg")).unwrap();
151        fs::write(
152            crates_dir.join("cargo-pkg/Cargo.toml"),
153            "[package]\nname = \"cargo-pkg\"\nversion = \"1.0.0\"\n",
154        )
155        .unwrap();
156
157        let ws = discover_workspace(root).unwrap();
158
159        // Should discover the Cargo package
160        assert_eq!(ws.members.len(), 1);
161        assert_eq!(ws.members[0].name, "cargo-pkg");
162        assert_eq!(ws.members[0].kind, PackageKind::Cargo);
163    }
164
165    #[test]
166    fn discover_workspace_handles_empty_workspace() {
167        // Test that an empty workspace (workspace defined but no packages) is valid
168        let temp = tempfile::tempdir().unwrap();
169        let root = temp.path();
170
171        fs::write(root.join("Cargo.toml"), "[workspace]\nmembers = []\n").unwrap();
172
173        let ws = discover_workspace(root).unwrap();
174        assert_eq!(ws.members.len(), 0);
175        assert_eq!(ws.root, root);
176    }
177
178    #[test]
179    fn discover_workspace_fails_when_no_workspace_found() {
180        // Test that we get an error when there's no workspace at all
181        let temp = tempfile::tempdir().unwrap();
182        let root = temp.path();
183
184        // No Cargo.toml, no workspace
185        let result = discover_workspace(root);
186        assert!(result.is_err());
187
188        // Verify it's the right error
189        match result {
190            Err(WorkspaceError::NotFound) => {}
191            _ => panic!("Expected WorkspaceError::NotFound"),
192        }
193    }
194}