mol-core 0.3.0

Core Libraray for mol crate
Documentation
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::path::Path;

use itertools::Itertools;
use tokio::{fs, io::AsyncWriteExt};

use crate::bump::PackageBump;
use crate::changeset::Changeset;
use crate::semantic::Semantic;
use crate::version::{Version, VersionMod, Versioned};

fn capitalize(s: &str) -> String {
  let mut c = s.chars();
  match c.next() {
    None => String::new(),
    Some(f) => f.to_uppercase().chain(c).collect(),
  }
}

fn fill_output<V: AsChangelogFmt + Versioned + Ord>(
  next_version: &Version<V>,
  patches: &HashMap<VersionMod<V>, Vec<String>>,
) -> String {
  let mut output = String::new();

  output.push_str(&next_version.as_changelog_fmt());

  for (version, changes) in patches.iter().sorted_by(|(a, _), (b, _)| Ord::cmp(&b, &a)) {
    output.push('\n');
    output.push_str(&version.as_changelog_fmt());
    output.push('\n');

    output.push_str(&changes.join("\n"));
  }

  output
}

fn create_patches<V>(
  package_name: &str,
  changesets: Vec<&Changeset<V>>,
) -> HashMap<VersionMod<V>, Vec<String>>
where
  V: AsChangelogFmt + Clone + Hash + Ord + Versioned,
{
  let mut patches: HashMap<VersionMod<V>, Vec<String>> = HashMap::new();

  for changset in changesets {
    let changeset_summary = changset.as_changelog_fmt();

    if let Some(version) = changset.packages.get(package_name) {
      if let Some(changes) = patches.get_mut(version) {
        changes.push(changeset_summary);
      } else {
        patches.insert(version.clone(), vec![changeset_summary]);
      }
    }
  }

  patches
}

pub struct Changelog;

impl Changelog {
  pub async fn update_changelog<T, V>(
    changelog_path: T,
    next_version: Version<V>,
    package_bump: &PackageBump<'_, V>,
    dry_run: bool,
  ) -> std::io::Result<()>
  where
    T: AsRef<Path> + Debug,
    V: AsChangelogFmt + Clone + Hash + Ord + Versioned,
  {
    let package_name = package_bump.name();

    if let Some(patches) = package_bump
      .changesets()
      .map(|changesets| create_patches(package_name, changesets))
    {
      if dry_run {
        println!(
          "dry_run - update changelog {:?}\n{}",
          changelog_path,
          fill_output(&next_version, &patches)
            .split('\n')
            .map(|val| format!("dry_run: + {}", val))
            .join("\n")
        );
      } else {
        let changelog = fs::read_to_string(&changelog_path)
          .await
          .unwrap_or_else(|_| format!("# {}\n", package_name));

        let mut changelog_lines = changelog.split('\n');

        if let Some(title) = changelog_lines.next() {
          let mut output = String::new();

          output.push_str(title);
          output.push('\n');
          output.push('\n');

          output.push_str(&fill_output(&next_version, &patches));

          let mut changelog = fs::File::create(&changelog_path).await?;

          changelog.write(output.as_bytes()).await?;

          changelog
            .write(changelog_lines.join("\n").as_bytes())
            .await?;
        }
      }
    }

    Ok(())
  }
}

pub trait AsChangelogFmt: Sized {
  fn as_changelog_fmt(&self) -> String;
}

impl<T> AsChangelogFmt for Changeset<T> {
  fn as_changelog_fmt(&self) -> String {
    let mut changeset_summary = String::new();

    let mut parts = self.message.split('\n');

    if let Some(value) = parts.next() {
      changeset_summary.push_str("- ");
      changeset_summary.push_str(value);
      changeset_summary.push('\n');

      for part in parts {
        changeset_summary.push_str("  ");
        changeset_summary.push_str(part);
        changeset_summary.push('\n');
      }
    }

    changeset_summary
  }
}

impl AsChangelogFmt for Semantic {
  fn as_changelog_fmt(&self) -> String {
    capitalize(&self.to_string())
  }
}

impl<T: AsChangelogFmt> AsChangelogFmt for VersionMod<T> {
  fn as_changelog_fmt(&self) -> String {
    format!("### {} Changes\n", self.version.as_changelog_fmt())
  }
}

impl<T> AsChangelogFmt for Version<T> {
  fn as_changelog_fmt(&self) -> String {
    format!("## {}\n", self.value)
  }
}