miho 8.0.1

Repository management tools
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
}