use std::path::{Path, PathBuf};
use crate::workspace::{
traits::Package,
types::{DependencyKind, ExternalDependency, WorkspaceDependency},
};
#[derive(Debug, Clone)]
pub struct CargoPackage {
pub(crate) name: String,
pub(crate) version: Option<String>,
pub(crate) path: PathBuf,
pub(crate) workspace_deps: Vec<WorkspaceDependency>,
pub(crate) external_deps: Vec<ExternalDependency>,
}
impl CargoPackage {
#[must_use]
pub fn new(
name: impl Into<String>,
version: Option<String>,
path: impl Into<PathBuf>,
workspace_deps: Vec<WorkspaceDependency>,
external_deps: Vec<ExternalDependency>,
) -> Self {
Self {
name: name.into(),
version,
path: path.into(),
workspace_deps,
external_deps,
}
}
#[must_use]
pub fn minimal(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
name: name.into(),
version: None,
path: path.into(),
workspace_deps: Vec::new(),
external_deps: Vec::new(),
}
}
}
impl Package for CargoPackage {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> Option<&str> {
self.version.as_deref()
}
fn path(&self) -> &Path {
&self.path
}
fn workspace_dependencies(&self) -> &[WorkspaceDependency] {
&self.workspace_deps
}
fn external_dependencies(&self) -> &[ExternalDependency] {
&self.external_deps
}
}
#[must_use]
pub fn parse_dependencies(
cargo_toml: &toml::Value,
workspace_members: &std::collections::BTreeSet<String>,
) -> (Vec<WorkspaceDependency>, Vec<ExternalDependency>) {
let mut workspace_deps = Vec::new();
let mut external_deps = Vec::new();
for (section, kind) in [
("dependencies", DependencyKind::Normal),
("dev-dependencies", DependencyKind::Dev),
("build-dependencies", DependencyKind::Build),
] {
if let Some(deps) = cargo_toml.get(section).and_then(|d| d.as_table()) {
for (name, value) in deps {
let (is_workspace, is_optional) = parse_dep_attributes(value);
if is_workspace || workspace_members.contains(name) {
workspace_deps.push(WorkspaceDependency::new(name.clone(), kind));
} else {
external_deps.push(ExternalDependency::new(name.clone(), kind, is_optional));
}
}
}
}
if let Some(target) = cargo_toml.get("target").and_then(|t| t.as_table()) {
for (_target_spec, target_deps) in target {
for (section, kind) in [
("dependencies", DependencyKind::Normal),
("dev-dependencies", DependencyKind::Dev),
("build-dependencies", DependencyKind::Build),
] {
if let Some(deps) = target_deps.get(section).and_then(|d| d.as_table()) {
for (name, value) in deps {
let (is_workspace, is_optional) = parse_dep_attributes(value);
if is_workspace || workspace_members.contains(name) {
if !workspace_deps.iter().any(|d| d.name == *name) {
workspace_deps.push(WorkspaceDependency::new(name.clone(), kind));
}
} else if !external_deps.iter().any(|d| d.name == *name) {
external_deps.push(ExternalDependency::new(
name.clone(),
kind,
is_optional,
));
}
}
}
}
}
}
(workspace_deps, external_deps)
}
fn parse_dep_attributes(value: &toml::Value) -> (bool, bool) {
match value {
toml::Value::Table(table) => {
let is_workspace = table
.get("workspace")
.and_then(toml::Value::as_bool)
.unwrap_or(false);
let is_optional = table
.get("optional")
.and_then(toml::Value::as_bool)
.unwrap_or(false);
let has_path = table.get("path").is_some();
(is_workspace || has_path, is_optional)
}
_ => (false, false),
}
}
pub async fn read_package_name(cargo_toml_path: &Path) -> Option<String> {
let content = switchy_fs::unsync::read_to_string(cargo_toml_path)
.await
.ok()?;
let toml: toml::Value = toml::from_str(&content).ok()?;
toml.get("package")?.get("name")?.as_str().map(String::from)
}
pub async fn read_package_version(cargo_toml_path: &Path) -> Option<String> {
let content = switchy_fs::unsync::read_to_string(cargo_toml_path)
.await
.ok()?;
let toml: toml::Value = toml::from_str(&content).ok()?;
toml.get("package")?
.get("version")?
.as_str()
.map(String::from)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
#[test]
fn test_parse_dependencies_simple() {
let toml_str = r#"
[package]
name = "test-pkg"
version = "0.1.0"
[dependencies]
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }
[dev-dependencies]
insta = "1.0"
"#;
let toml: toml::Value = toml::from_str(toml_str).unwrap();
let workspace_members = BTreeSet::new();
let (workspace_deps, external_deps) = parse_dependencies(&toml, &workspace_members);
assert!(workspace_deps.is_empty());
assert_eq!(external_deps.len(), 3);
assert!(external_deps.iter().any(|d| d.name == "serde"));
assert!(external_deps.iter().any(|d| d.name == "tokio"));
assert!(
external_deps
.iter()
.any(|d| d.name == "insta" && d.kind == DependencyKind::Dev)
);
}
#[test]
fn test_parse_dependencies_workspace() {
let toml_str = r#"
[package]
name = "test-pkg"
version = "0.1.0"
[dependencies]
serde = "1.0"
my-lib = { workspace = true }
"#;
let toml: toml::Value = toml::from_str(toml_str).unwrap();
let workspace_members = BTreeSet::new();
let (workspace_deps, external_deps) = parse_dependencies(&toml, &workspace_members);
assert_eq!(workspace_deps.len(), 1);
assert_eq!(workspace_deps[0].name, "my-lib");
assert_eq!(external_deps.len(), 1);
assert_eq!(external_deps[0].name, "serde");
}
#[test]
fn test_parse_dependencies_optional() {
let toml_str = r#"
[package]
name = "test-pkg"
version = "0.1.0"
[dependencies]
serde = "1.0"
optional-dep = { version = "1.0", optional = true }
"#;
let toml: toml::Value = toml::from_str(toml_str).unwrap();
let workspace_members = BTreeSet::new();
let (_, external_deps) = parse_dependencies(&toml, &workspace_members);
assert_eq!(external_deps.len(), 2);
let optional = external_deps
.iter()
.find(|d| d.name == "optional-dep")
.unwrap();
assert!(optional.is_optional);
}
}