use std::path::{Path, PathBuf};
use cabin_manifest::ParsedManifest;
use crate::error::WorkspaceError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredManifest {
pub manifest_path: PathBuf,
pub workspace_dir: PathBuf,
}
pub fn discover_workspace_root(start: &Path) -> Result<Option<DiscoveredManifest>, WorkspaceError> {
let mut current = start.to_path_buf();
if current.is_relative()
&& let Ok(abs) = std::env::current_dir().map(|cwd| cwd.join(¤t))
{
current = abs;
}
let mut found: Vec<DiscoveredManifest> = Vec::new();
loop {
let candidate = current.join("cabin.toml");
if candidate.is_file() {
let parsed = parse_manifest(&candidate)?;
if parsed.workspace.is_some() {
found.push(DiscoveredManifest {
manifest_path: candidate,
workspace_dir: current.clone(),
});
}
}
match current.parent() {
Some(parent) if parent != current => {
current = parent.to_path_buf();
}
_ => break,
}
}
match found.as_slice() {
[] => Ok(None),
[_only] => Ok(Some(found.into_iter().next().unwrap())),
_ => {
let nearest = found.first().unwrap();
let outer = found.last().unwrap();
Err(WorkspaceError::NestedWorkspaceDiscovery {
nearest: nearest.manifest_path.clone(),
outer: outer.manifest_path.clone(),
})
}
}
}
fn parse_manifest(path: &Path) -> Result<ParsedManifest, WorkspaceError> {
cabin_manifest::load_manifest(path).map_err(|source| WorkspaceError::Manifest {
path: path.to_path_buf(),
source: Box::new(source),
})
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::TempDir;
use assert_fs::prelude::*;
#[test]
fn finds_workspace_root_from_member_dir() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[workspace]
members = ["packages/app"]
"#,
)
.unwrap();
dir.child("packages/app/cabin.toml")
.write_str("[package]\nname = \"app\"\nversion = \"0.1.0\"\n")
.unwrap();
let found = discover_workspace_root(&dir.path().join("packages/app"))
.unwrap()
.expect("workspace root should be discovered");
assert_eq!(found.workspace_dir, dir.path());
assert_eq!(found.manifest_path, dir.path().join("cabin.toml"));
}
#[test]
fn finds_workspace_root_from_root_dir() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r"[workspace]
members = []
",
)
.unwrap();
let found = discover_workspace_root(dir.path()).unwrap().unwrap();
assert_eq!(found.workspace_dir, dir.path());
}
#[test]
fn returns_none_when_no_workspace_root() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str("[package]\nname = \"only\"\nversion = \"0.1.0\"\n")
.unwrap();
assert!(discover_workspace_root(dir.path()).unwrap().is_none());
}
#[test]
fn skips_non_workspace_manifests_and_keeps_walking() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[workspace]
members = ["packages/app"]
"#,
)
.unwrap();
dir.child("packages/app/cabin.toml")
.write_str("[package]\nname = \"app\"\nversion = \"0.1.0\"\n")
.unwrap();
let found = discover_workspace_root(&dir.path().join("packages/app"))
.unwrap()
.expect("workspace root should be discovered");
assert_eq!(found.manifest_path, dir.path().join("cabin.toml"));
}
#[test]
fn nested_workspace_errors_when_starting_inside_nested() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r#"[workspace]
members = ["nested"]
"#,
)
.unwrap();
dir.child("nested/cabin.toml")
.write_str(
r"[workspace]
members = []
",
)
.unwrap();
let err = discover_workspace_root(&dir.path().join("nested"))
.expect_err("expected NestedWorkspaceDiscovery");
match err {
WorkspaceError::NestedWorkspaceDiscovery { nearest, outer } => {
assert_eq!(nearest, dir.path().join("nested/cabin.toml"));
assert_eq!(outer, dir.path().join("cabin.toml"));
}
other => panic!("expected NestedWorkspaceDiscovery, got {other:?}"),
}
}
#[test]
fn nested_workspace_errors_even_when_outer_does_not_list_nested() {
let dir = TempDir::new().unwrap();
dir.child("cabin.toml")
.write_str(
r"[workspace]
members = []
",
)
.unwrap();
dir.child("nested/cabin.toml")
.write_str(
r"[workspace]
members = []
",
)
.unwrap();
let err = discover_workspace_root(&dir.path().join("nested"))
.expect_err("expected NestedWorkspaceDiscovery");
assert!(matches!(
err,
WorkspaceError::NestedWorkspaceDiscovery { .. }
));
}
}