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};
#[derive(Debug)]
pub(super) enum CargoToml {
Flat(CargoTomlInner),
Workspace(CargoWorkspace),
}
#[derive(Debug, Deserialize)]
pub(super) struct CargoWorkspace {
workspace: CargoTomlInner,
}
#[derive(Debug, 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(Debug, Deserialize)]
struct CargoTomlPackage {
#[serde(default)]
name: String,
#[serde(default = "default_version")]
version: String,
}
impl Manifest for CargoToml {
type Value = toml::Value;
fn read(path: impl AsRef<Path>) -> Result<ManifestBox> {
let contents = fs::read_to_string(&path)?;
if let Some(mut manifest) = parse_str(&contents) {
if let CargoToml::Workspace(it) = &mut manifest
&& it.workspace.package.name.trim().is_empty()
{
"workspace".clone_into(&mut it.workspace.package.name);
}
Ok(Box::new(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 mut contents = fs::read_to_string(&package.path)?;
update(self, &mut contents, targets)?;
fs::write(&package.path, contents)?;
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)?)
}
}
pub(super) fn parse_str(contents: &str) -> Option<CargoToml> {
if let Ok(manifest) = toml::from_str::<CargoWorkspace>(contents) {
Some(CargoToml::Workspace(manifest))
} else if let Ok(manifest) = toml::from_str::<CargoTomlInner>(contents) {
Some(CargoToml::Flat(manifest))
} else {
None
}
}
pub(super) fn update(
manifest: &CargoToml,
contents: &mut String,
targets: &[dependency::Target],
) -> Result<()> {
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 manifest {
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);
}
}
}
*contents = doc.to_string();
Ok(())
}
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
}