miho 8.1.0

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};

#[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
}