use std::fs::read_to_string;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use anyhow::anyhow;
use cargo_metadata::Metadata;
use cargo_metadata::MetadataCommand;
use cargo_metadata::Package;
use toml::Table;
use toml::Value;
use crate::finding::model::manifest_dependency::ManifestDependency;
use crate::settings::config::Config;
use crate::settings::scanned_package::ScannedPackage;
pub struct ManifestResolver;
impl ManifestResolver {
pub const MANIFEST: &'static str = "Cargo.toml";
pub fn workspace_package_names(config: &Config) -> Result<Vec<String>> {
Ok(Self::metadata(config)?
.packages
.iter()
.map(|package| package.name.to_string())
.collect())
}
pub fn packages(config: &Config) -> Result<Vec<ScannedPackage>> {
let metadata = Self::metadata(config)?;
Ok(Self::selected(&metadata, config)?
.into_iter()
.map(|package| {
ScannedPackage::new(
package.name.as_str(),
Self::manifest_dir(package.manifest_path.as_std_path()),
package.license.clone(),
)
})
.collect())
}
pub fn workspace_dependencies(config: &Config) -> Option<Vec<ManifestDependency>> {
let metadata = Self::metadata(config).ok()?;
let root = metadata.workspace_root.as_std_path();
if !Self::is_workspace(&root.join(Self::MANIFEST)) {
return None;
}
Some(
metadata
.packages
.iter()
.flat_map(|package| {
let path = package.manifest_path.as_std_path();
Self::declared_in(path, &Self::relative_to(root, path))
})
.collect(),
)
}
fn is_workspace(manifest: &Path) -> bool {
Self::parsed(manifest).is_some_and(|table| table.contains_key("workspace"))
}
fn parsed(manifest: &Path) -> Option<Table> {
read_to_string(manifest).ok()?.parse::<Table>().ok()
}
fn declared_in(manifest: &Path, shown: &str) -> Vec<ManifestDependency> {
let Some(table) = Self::parsed(manifest) else {
return Vec::new();
};
ManifestDependency::SECTIONS
.into_iter()
.flat_map(|section| Self::in_section(&table, section, shown))
.collect()
}
fn in_section(table: &Table, section: &str, shown: &str) -> Vec<ManifestDependency> {
table
.get(section)
.and_then(Value::as_table)
.map(|declared| {
declared
.iter()
.map(|(name, value)| {
let takes = value
.as_table()
.and_then(|entry| entry.get("workspace"))
.and_then(Value::as_bool)
.unwrap_or(false);
ManifestDependency::new(shown, name, section, takes)
})
.collect()
})
.unwrap_or_default()
}
pub fn workspace_root(config: &Config) -> Option<PathBuf> {
Self::metadata(config)
.ok()
.map(|metadata| metadata.workspace_root.as_std_path().to_path_buf())
}
fn metadata(config: &Config) -> Result<Metadata> {
let mut command = MetadataCommand::new();
command.no_deps();
if let Some(manifest_path) = &config.manifest_path {
command.manifest_path(manifest_path);
}
Ok(command.exec()?)
}
fn selected<'a>(metadata: &'a Metadata, config: &Config) -> Result<Vec<&'a Package>> {
if config.packages.is_empty() {
return Ok(metadata.packages.iter().collect());
}
config
.packages
.iter()
.map(|name| {
metadata
.packages
.iter()
.find(|package| package.name.as_str() == name.as_str())
.ok_or_else(|| anyhow!("package `{name}` is not in this workspace"))
})
.collect()
}
pub fn relative_to(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
fn manifest_dir(manifest_path: &Path) -> PathBuf {
manifest_path
.parent()
.map_or_else(|| manifest_path.to_path_buf(), Path::to_path_buf)
}
}