use crate::error::{Result, WorkspaceError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct WorkspaceInfo {
pub root: PathBuf,
pub workspace_config: WorkspaceConfig,
pub packages: HashMap<String, PackageInfo>,
pub internal_dependencies: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub members: Vec<String>,
pub package: Option<WorkspacePackage>,
pub dependencies: Option<HashMap<String, toml::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspacePackage {
pub version: Option<String>,
pub edition: Option<String>,
#[serde(flatten)]
pub other: HashMap<String, toml::Value>,
}
#[derive(Debug, Clone)]
pub struct PackageInfo {
pub name: String,
pub version: String,
pub path: PathBuf,
pub absolute_path: PathBuf,
pub cargo_toml_path: PathBuf,
pub config: PackageConfig,
pub workspace_dependencies: Vec<String>,
pub all_dependencies: HashMap<String, DependencySpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: toml::Value,
pub edition: Option<toml::Value>,
pub description: Option<String>,
pub license: Option<toml::Value>,
pub authors: Option<toml::Value>,
pub homepage: Option<toml::Value>,
pub repository: Option<toml::Value>,
#[serde(flatten)]
pub other: HashMap<String, toml::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencySpec {
pub version: Option<String>,
pub path: Option<String>,
pub git: Option<String>,
pub rev: Option<String>,
pub features: Option<Vec<String>>,
pub optional: Option<bool>,
pub default_features: Option<bool>,
}
impl WorkspaceInfo {
pub fn analyze<P: AsRef<Path>>(start_dir: P) -> Result<Self> {
let workspace_root = Self::find_workspace_root(start_dir)?;
let workspace_config = Self::parse_workspace_config(&workspace_root)?;
let packages = Self::enumerate_packages(&workspace_root, &workspace_config)?;
let internal_dependencies = Self::build_internal_dependency_map(&packages)?;
Ok(Self {
root: workspace_root,
workspace_config,
packages,
internal_dependencies,
})
}
fn find_workspace_root<P: AsRef<Path>>(start_dir: P) -> Result<PathBuf> {
let mut current_dir = start_dir.as_ref().canonicalize()?;
loop {
let cargo_toml = current_dir.join("Cargo.toml");
if cargo_toml.exists() {
let content = std::fs::read_to_string(&cargo_toml)?;
let parsed: toml::Value = toml::from_str(&content)?;
if parsed.get("workspace").is_some() {
return Ok(current_dir);
}
}
match current_dir.parent() {
Some(parent) => current_dir = parent.to_path_buf(),
None => return Err(WorkspaceError::RootNotFound.into()),
}
}
}
fn parse_workspace_config(workspace_root: &Path) -> Result<WorkspaceConfig> {
let cargo_toml_path = workspace_root.join("Cargo.toml");
let content = std::fs::read_to_string(&cargo_toml_path)?;
let parsed: toml::Value = toml::from_str(&content)?;
let workspace_table = parsed
.get("workspace")
.ok_or_else(|| WorkspaceError::InvalidStructure {
reason: "No [workspace] section found in root Cargo.toml".to_string(),
})?;
let workspace_config: WorkspaceConfig = workspace_table.clone().try_into()
.map_err(|e| WorkspaceError::InvalidStructure {
reason: format!("Failed to parse workspace configuration: {}", e),
})?;
Ok(workspace_config)
}
fn enumerate_packages(
workspace_root: &Path,
workspace_config: &WorkspaceConfig,
) -> Result<HashMap<String, PackageInfo>> {
let mut packages = HashMap::new();
for member in &workspace_config.members {
let member_path = workspace_root.join(member);
if !member_path.exists() {
continue; }
let cargo_toml_path = member_path.join("Cargo.toml");
if !cargo_toml_path.exists() {
return Err(WorkspaceError::MissingCargoToml {
path: cargo_toml_path,
}.into());
}
let package_info = Self::parse_package_info(
workspace_root,
&member_path,
&cargo_toml_path,
)?;
packages.insert(package_info.name.clone(), package_info);
}
Ok(packages)
}
fn parse_package_info(
workspace_root: &Path,
package_path: &Path,
cargo_toml_path: &Path,
) -> Result<PackageInfo> {
let content = std::fs::read_to_string(cargo_toml_path)?;
let parsed: toml::Value = toml::from_str(&content)?;
let package_table = parsed
.get("package")
.ok_or_else(|| WorkspaceError::InvalidPackage {
package: package_path.display().to_string(),
reason: "No [package] section found".to_string(),
})?;
let config: PackageConfig = package_table.clone().try_into()
.map_err(|e| WorkspaceError::InvalidPackage {
package: package_path.display().to_string(),
reason: format!("Failed to parse package configuration: {}", e),
})?;
let version = match &config.version {
toml::Value::String(v) => v.clone(),
toml::Value::Table(table) if table.get("workspace") == Some(&toml::Value::Boolean(true)) => {
let workspace_cargo_toml = workspace_root.join("Cargo.toml");
let workspace_content = std::fs::read_to_string(&workspace_cargo_toml)?;
let workspace_parsed: toml::Value = toml::from_str(&workspace_content)?;
workspace_parsed
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.ok_or_else(|| WorkspaceError::InvalidPackage {
package: config.name.clone(),
reason: "Workspace version not found".to_string(),
})?
.to_string()
}
_ => return Err(WorkspaceError::InvalidPackage {
package: config.name.clone(),
reason: "Invalid version specification".to_string(),
}.into()),
};
let all_dependencies = Self::parse_dependencies(&parsed)?;
let workspace_dependencies = Self::extract_workspace_dependencies(&all_dependencies);
let relative_path = package_path.strip_prefix(workspace_root)
.map_err(|_| WorkspaceError::InvalidPackage {
package: config.name.clone(),
reason: "Package path not within workspace".to_string(),
})?
.to_path_buf();
Ok(PackageInfo {
name: config.name.clone(),
version,
path: relative_path,
absolute_path: package_path.to_path_buf(),
cargo_toml_path: cargo_toml_path.to_path_buf(),
config,
workspace_dependencies,
all_dependencies,
})
}
fn parse_dependencies(parsed: &toml::Value) -> Result<HashMap<String, DependencySpec>> {
let mut dependencies = HashMap::new();
if let Some(deps) = parsed.get("dependencies").and_then(|d| d.as_table()) {
for (name, spec) in deps {
dependencies.insert(name.clone(), Self::parse_dependency_spec(spec)?);
}
}
if let Some(dev_deps) = parsed.get("dev-dependencies").and_then(|d| d.as_table()) {
for (name, spec) in dev_deps {
dependencies.insert(
format!("dev:{}", name),
Self::parse_dependency_spec(spec)?,
);
}
}
if let Some(build_deps) = parsed.get("build-dependencies").and_then(|d| d.as_table()) {
for (name, spec) in build_deps {
dependencies.insert(
format!("build:{}", name),
Self::parse_dependency_spec(spec)?,
);
}
}
Ok(dependencies)
}
fn parse_dependency_spec(spec: &toml::Value) -> Result<DependencySpec> {
match spec {
toml::Value::String(version) => Ok(DependencySpec {
version: Some(version.clone()),
path: None,
git: None,
rev: None,
features: None,
optional: None,
default_features: None,
}),
toml::Value::Table(table) => {
let spec: DependencySpec = table.clone().try_into()
.map_err(|e| WorkspaceError::InvalidStructure {
reason: format!("Failed to parse dependency spec: {}", e),
})?;
Ok(spec)
}
_ => Err(WorkspaceError::InvalidStructure {
reason: "Invalid dependency specification".to_string(),
}.into()),
}
}
fn extract_workspace_dependencies(all_dependencies: &HashMap<String, DependencySpec>) -> Vec<String> {
all_dependencies
.iter()
.filter_map(|(name, spec)| {
if spec.path.is_some() && !name.contains(':') {
Some(name.clone())
} else {
None
}
})
.collect()
}
fn build_internal_dependency_map(
packages: &HashMap<String, PackageInfo>,
) -> Result<HashMap<String, Vec<String>>> {
let package_names: std::collections::HashSet<_> = packages.keys().cloned().collect();
let mut internal_deps = HashMap::new();
for (package_name, package_info) in packages {
let mut deps = Vec::new();
for dep_name in &package_info.workspace_dependencies {
if package_names.contains(dep_name) {
deps.push(dep_name.clone());
}
}
internal_deps.insert(package_name.clone(), deps);
}
Ok(internal_deps)
}
pub fn get_package(&self, name: &str) -> Result<&PackageInfo> {
self.packages
.get(name)
.ok_or_else(|| WorkspaceError::PackageNotFound {
name: name.to_string(),
}.into())
}
pub fn workspace_version(&self) -> Result<String> {
self.workspace_config
.package
.as_ref()
.and_then(|p| p.version.as_ref())
.ok_or_else(|| WorkspaceError::InvalidStructure {
reason: "No workspace version found".to_string(),
}.into())
.map(|v| v.clone())
}
pub fn package_names(&self) -> Vec<String> {
self.packages.keys().cloned().collect()
}
pub fn has_package(&self, name: &str) -> bool {
self.packages.contains_key(name)
}
}