#![allow(dead_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use glob::glob;
use crate::config::{CcgoConfig, WorkspaceConfig};
#[derive(Debug, Clone)]
pub struct WorkspaceMember {
pub name: String,
pub version: String,
pub path: PathBuf,
pub config: CcgoConfig,
}
impl WorkspaceMember {
pub fn load(path: &Path) -> Result<Self> {
let config_path = path.join("CCGO.toml");
let config = CcgoConfig::load_from(&config_path)
.with_context(|| format!("Failed to load member at {}", path.display()))?;
let package = config.package.as_ref()
.ok_or_else(|| anyhow::anyhow!("Workspace member must have [package] section"))?;
Ok(Self {
name: package.name.clone(),
version: package.version.clone(),
path: path.to_path_buf(),
config,
})
}
}
#[derive(Debug)]
pub struct WorkspaceMembers {
members: HashMap<String, WorkspaceMember>,
}
impl WorkspaceMembers {
pub fn discover(workspace_root: &Path, config: &WorkspaceConfig) -> Result<Self> {
let mut members = HashMap::new();
let member_paths = Self::expand_member_patterns(workspace_root, config)?;
for path in member_paths {
let member = WorkspaceMember::load(&path)?;
if members.contains_key(&member.name) {
anyhow::bail!(
"Duplicate workspace member name '{}' at {}",
member.name,
path.display()
);
}
members.insert(member.name.clone(), member);
}
if members.is_empty() {
println!(" ⚠️ No workspace members found");
}
Ok(Self { members })
}
fn expand_member_patterns(workspace_root: &Path, config: &WorkspaceConfig) -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
for pattern in &config.members {
let abs_pattern = workspace_root.join(pattern);
let pattern_str = abs_pattern.to_string_lossy();
if pattern_str.contains('*') || pattern_str.contains('?') || pattern_str.contains('[') {
for entry in glob(&pattern_str)
.with_context(|| format!("Invalid glob pattern: {}", pattern))?
{
let path = entry
.with_context(|| format!("Failed to read glob entry for {}", pattern))?;
if path.is_dir() {
if !Self::is_excluded(&path, workspace_root, &config.exclude) {
if path.join("CCGO.toml").exists() {
paths.push(path);
}
}
}
}
} else {
let path = workspace_root.join(pattern);
if !path.exists() {
anyhow::bail!("Workspace member not found: {}", pattern);
}
if !path.is_dir() {
anyhow::bail!("Workspace member must be a directory: {}", pattern);
}
if !path.join("CCGO.toml").exists() {
anyhow::bail!("Workspace member missing CCGO.toml: {}", pattern);
}
if !Self::is_excluded(&path, workspace_root, &config.exclude) {
paths.push(path);
}
}
}
Ok(paths)
}
fn is_excluded(path: &Path, workspace_root: &Path, exclude: &[String]) -> bool {
for pattern in exclude {
let abs_pattern = workspace_root.join(pattern);
if path == abs_pattern {
return true;
}
if path.starts_with(&abs_pattern) {
return true;
}
}
false
}
pub fn get(&self, name: &str) -> Option<&WorkspaceMember> {
self.members.get(name)
}
pub fn all(&self) -> Vec<&WorkspaceMember> {
self.members.values().collect()
}
pub fn len(&self) -> usize {
self.members.len()
}
pub fn is_empty(&self) -> bool {
self.members.is_empty()
}
pub fn names(&self) -> Vec<&str> {
self.members.keys().map(|s| s.as_str()).collect()
}
pub fn filter<F>(&self, predicate: F) -> Vec<&WorkspaceMember>
where
F: Fn(&WorkspaceMember) -> bool,
{
self.members.values().filter(|m| predicate(m)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_load_member() {
let temp_dir = TempDir::new().unwrap();
let member_dir = temp_dir.path().join("test-member");
fs::create_dir_all(&member_dir).unwrap();
fs::write(
member_dir.join("CCGO.toml"),
r#"
[package]
name = "test-member"
version = "1.0.0"
"#,
)
.unwrap();
let member = WorkspaceMember::load(&member_dir).unwrap();
assert_eq!(member.name, "test-member");
assert_eq!(member.version, "1.0.0");
}
#[test]
fn test_discover_members_direct() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let member1 = root.join("member1");
fs::create_dir_all(&member1).unwrap();
fs::write(
member1.join("CCGO.toml"),
r#"
[package]
name = "member1"
version = "1.0.0"
"#,
)
.unwrap();
let member2 = root.join("member2");
fs::create_dir_all(&member2).unwrap();
fs::write(
member2.join("CCGO.toml"),
r#"
[package]
name = "member2"
version = "2.0.0"
"#,
)
.unwrap();
let config = WorkspaceConfig {
members: vec!["member1".to_string(), "member2".to_string()],
exclude: vec![],
..Default::default()
};
let members = WorkspaceMembers::discover(root, &config).unwrap();
assert_eq!(members.len(), 2);
assert!(members.get("member1").is_some());
assert!(members.get("member2").is_some());
}
#[test]
fn test_discover_members_glob() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let foo = root.join("crates/foo");
fs::create_dir_all(&foo).unwrap();
fs::write(
foo.join("CCGO.toml"),
r#"
[package]
name = "foo"
version = "1.0.0"
"#,
)
.unwrap();
let bar = root.join("crates/bar");
fs::create_dir_all(&bar).unwrap();
fs::write(
bar.join("CCGO.toml"),
r#"
[package]
name = "bar"
version = "1.0.0"
"#,
)
.unwrap();
let config = WorkspaceConfig {
members: vec!["crates/*".to_string()],
exclude: vec![],
..Default::default()
};
let members = WorkspaceMembers::discover(root, &config).unwrap();
assert_eq!(members.len(), 2);
assert!(members.get("foo").is_some());
assert!(members.get("bar").is_some());
}
#[test]
fn test_exclude_members() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let member1 = root.join("member1");
fs::create_dir_all(&member1).unwrap();
fs::write(
member1.join("CCGO.toml"),
r#"
[package]
name = "member1"
version = "1.0.0"
"#,
)
.unwrap();
let member2 = root.join("member2");
fs::create_dir_all(&member2).unwrap();
fs::write(
member2.join("CCGO.toml"),
r#"
[package]
name = "member2"
version = "2.0.0"
"#,
)
.unwrap();
let config = WorkspaceConfig {
members: vec!["member*".to_string()],
exclude: vec!["member2".to_string()],
..Default::default()
};
let members = WorkspaceMembers::discover(root, &config).unwrap();
assert_eq!(members.len(), 1);
assert!(members.get("member1").is_some());
assert!(members.get("member2").is_none());
}
}