use super::default_version;
use crate::agent::Agent;
use crate::dependency::{self, DependencyKind, DependencyTree};
use crate::package::Package;
use crate::package::manifest::{Handler, Manifest, ManifestBox};
use anyhow::{Result, bail};
use semver::Version;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use toml::Value;
use toml_edit::{DocumentMut, Item};
pub(super) enum CargoToml {
Flat(CargoTomlInner),
Workspace(CargoWorkspace),
}
#[derive(Deserialize)]
pub(super) struct CargoWorkspace {
workspace: CargoTomlInner,
}
#[derive(Deserialize)]
pub(super) struct CargoTomlInner {
package: CargoTomlPackage,
#[serde(default)]
dependencies: HashMap<String, Value>,
#[serde(default, rename(deserialize = "dev-dependencies"))]
dev_dependencies: HashMap<String, Value>,
#[serde(default, rename(deserialize = "build-dependencies"))]
build_dependencies: HashMap<String, Value>,
}
#[derive(Deserialize)]
struct CargoTomlPackage {
#[serde(default)]
name: String,
#[serde(default = "default_version")]
version: String,
}
impl Manifest for CargoToml {
type Value = toml::Value;
const FILENAME: &'static str = "Cargo.toml";
fn read(path: impl AsRef<Path>) -> Result<ManifestBox> {
let contents = fs::read_to_string(&path)?;
if let Ok(mut manifest) = toml::from_str::<CargoWorkspace>(&contents) {
if manifest
.workspace
.package
.name
.trim()
.is_empty()
{
"workspace".clone_into(&mut manifest.workspace.package.name);
}
Ok(Box::new(CargoToml::Workspace(manifest)))
} else if let Ok(manifest) = toml::from_str::<CargoTomlInner>(&contents) {
Ok(Box::new(CargoToml::Flat(manifest)))
} else {
let path = path.as_ref().to_string_lossy();
bail!("failed to parse manifest at: {path}");
}
}
fn read_as_value(path: impl AsRef<Path>) -> Result<Self::Value> {
let contents = fs::read_to_string(path)?;
Ok(toml::from_str::<Self::Value>(&contents)?)
}
}
impl Handler for CargoToml {
fn agent(&self) -> Agent {
Agent::Cargo
}
fn bump(&self, package: &Package, version: Version) -> Result<()> {
let contents = fs::read_to_string(&package.path)?;
let mut doc = contents.parse::<DocumentMut>()?;
let version = toml_edit::value(version.to_string());
match self {
CargoToml::Flat(..) => {
doc["package"]["version"] = version;
}
CargoToml::Workspace(..) => {
doc["workspace"]["package"]["version"] = version;
}
}
fs::write(&package.path, doc.to_string())?;
Ok(())
}
fn dependency_tree(&self) -> DependencyTree {
let mut tree = DependencyTree::new(self.agent());
macro_rules! add {
($dependencies:expr, $kind:ident) => {
if !$dependencies.is_empty() {
let dependencies = parse_dependencies($dependencies);
tree.add_many(&dependencies, DependencyKind::$kind);
}
};
}
match self {
CargoToml::Flat(it) => {
add!(&it.dependencies, Normal);
add!(&it.dev_dependencies, Development);
add!(&it.build_dependencies, Build);
}
CargoToml::Workspace(it) => {
add!(&it.workspace.dependencies, Normal);
add!(&it.workspace.dev_dependencies, Development);
add!(&it.workspace.build_dependencies, Build);
}
}
tree
}
fn name(&self) -> &str {
match self {
CargoToml::Flat(it) => it.package.name.as_str(),
CargoToml::Workspace(it) => it.workspace.package.name.as_str(),
}
}
fn update(&self, package: &Package, targets: &[dependency::Target]) -> Result<()> {
let contents = fs::read_to_string(&package.path)?;
let mut doc = contents.parse::<DocumentMut>()?;
for target in targets {
let key = match target.dependency.kind {
DependencyKind::Normal => "dependencies",
DependencyKind::Development => "dev-dependencies",
DependencyKind::Build => "build-dependencies",
DependencyKind::Peer | DependencyKind::PackageManager => continue,
};
let version = match self {
CargoToml::Flat(..) => {
doc
.get_mut(key)
.and_then(Item::as_table_like_mut)
.and_then(|deps| deps.get_mut(&target.dependency.name))
}
CargoToml::Workspace(..) => {
doc
.get_mut("workspace")
.and_then(|workspace| workspace.get_mut(key))
.and_then(Item::as_table_like_mut)
.and_then(|deps| deps.get_mut(&target.dependency.name))
}
};
if let Some(value) = version {
let mut comparator = target.comparator.to_string();
if comparator.starts_with('^') {
comparator.remove(0);
}
let comparator = toml_edit::value(comparator.as_str());
if value.is_str() {
*value = comparator;
} else if let Some(value) = value.as_table_like_mut() {
value.insert("version", comparator);
}
}
}
fs::write(&package.path, doc.to_string())?;
Ok(())
}
fn version(&self) -> Result<Version> {
let version = match self {
CargoToml::Flat(it) => &it.package.version,
CargoToml::Workspace(it) => &it.workspace.package.version,
};
Ok(Version::parse(version)?)
}
}
fn parse_dependencies(deps: &HashMap<String, Value>) -> HashMap<String, String> {
let mut dependencies = HashMap::with_capacity(deps.len());
for (name, version) in deps {
if let Some(version) = parse_version(version) {
dependencies.insert(name.clone(), version.clone());
}
}
dependencies
}
fn parse_version(value: &Value) -> Option<&String> {
if let Value::String(version) = value {
return Some(version);
}
if let Value::String(version) = value.get("version")? {
if version == "*" {
return None;
}
let path = value.get("path");
let git = value.get("git");
if path.is_none() && git.is_none() {
return Some(version);
}
}
None
}