mol-core 0.3.0

Core Libraray for mol crate
Documentation
use std::collections::HashMap;
use std::path::Path;
use std::str::FromStr;

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

use crate::error::ChangesetParseError;
use crate::version::{VersionMod, Versioned};

#[derive(Debug, Default)]
pub struct Changeset<T> {
  pub packages: HashMap<String, VersionMod<T>>,
  pub message: String,
}

impl<T> Changeset<T> {
  fn find_changeset_start(
    lines: &mut dyn Iterator<Item = &str>,
  ) -> Result<(), ChangesetParseError> {
    for line in lines {
      match line {
        "" => {}
        "---" => return Ok(()),
        _ => return Err(ChangesetParseError::HeaderNotFound),
      }
    }

    Err(ChangesetParseError::HeaderNotFound)
  }

  fn parse_package_name(value: &str) -> &str {
    if value.starts_with('\"') {
      let mut chars = value.chars();
      chars.next();
      chars.next_back();
      chars.as_str()
    } else {
      value
    }
  }
}

impl<T> Changeset<T>
where
  T: FromStr + Ord + Versioned,
{
  pub fn parse(value: &str) -> Result<Self, <Self as FromStr>::Err> {
    Changeset::from_str(value)
  }

  pub async fn save<P: AsRef<Path>>(self, output: P) -> std::io::Result<()> {
    let mut file = File::create(output).await?;

    file.write_all(self.to_string().as_bytes()).await?;

    Ok(())
  }
}

impl<T> FromStr for Changeset<T>
where
  T: FromStr,
{
  type Err = ChangesetParseError;
  fn from_str(value: &str) -> Result<Self, Self::Err> {
    let mut packages = HashMap::new();
    let mut lines = value.split('\n').map(|line| line.trim_end());

    Self::find_changeset_start(&mut lines)?;

    for line in &mut lines {
      match line {
        "---" => break,
        value => {
          let change_value: Vec<&str> = value.split(':').map(|val| val.trim()).collect();

          match change_value.len() {
            2 => {
              let (package, version) = (
                Self::parse_package_name(change_value[0]),
                VersionMod::from_str(change_value[1]),
              );

              if let Ok(version) = version {
                packages.insert(package.to_string(), version);
              } else {
                return Err(ChangesetParseError::HeaderParsing);
              }
            }
            _ => return Err(ChangesetParseError::HeaderParsing),
          }
        }
      }
    }

    Ok(Self {
      packages,
      message: lines.collect::<Vec<&str>>().join("\n").trim().to_owned(),
    })
  }
}

impl<T> ToString for Changeset<T>
where
  T: Versioned + Ord + ToString,
{
  fn to_string(&self) -> String {
    let mut output = vec![];

    output.extend(b"---\n");
    for (package, version) in self.packages.iter().sorted() {
      output.extend(format!("\"{}\": {}\n", package, version.to_string()).as_bytes())
    }
    output.extend(b"---\n\n");
    output.extend(self.message.as_bytes());
    output.push(b'\n');

    String::from_utf8(output).unwrap()
  }
}

#[cfg(test)]
mod tests {

  use super::*;
  use crate::semantic::Semantic;

  #[test]
  fn from_str() {
    let changeset = Changeset::from_str(
      "
---
\"mol\": minor
---

Do cool stuff
",
    );

    assert!(changeset.is_ok());

    let changeset = changeset.unwrap();

    assert_eq!(
      changeset.packages,
      vec![("mol".to_string(), VersionMod::new(Semantic::minor()))]
        .into_iter()
        .collect()
    );
    assert_eq!(changeset.message, "Do cool stuff");
  }

  #[test]
  fn from_str_multiple() {
    let changeset = Changeset::from_str(
      "
---
\"mol\": minor
\"mol-core\": major
---

Do cool stuff
",
    )
    .unwrap();

    assert_eq!(
      changeset.packages,
      vec![
        ("mol".to_string(), VersionMod::new(Semantic::minor())),
        ("mol-core".to_string(), VersionMod::new(Semantic::major()))
      ]
      .into_iter()
      .collect()
    );
  }

  #[test]
  fn to_str() {
    let changeset = Changeset {
      packages: vec![("mol".to_owned(), VersionMod::new(Semantic::minor()))]
        .into_iter()
        .collect(),
      message: "Do cool stuff".to_string(),
    };

    assert_eq!(
      changeset.to_string(),
      "---
\"mol\": minor
---

Do cool stuff
"
    )
  }

  #[test]
  fn to_str_multiple() {
    let changeset = Changeset {
      packages: vec![
        ("mol".to_owned(), VersionMod::new(Semantic::minor())),
        ("mol-core".to_owned(), VersionMod::new(Semantic::major())),
      ]
      .into_iter()
      .collect(),
      message: "Do cool stuff".to_string(),
    };

    assert_eq!(
      changeset.to_string(),
      "---
\"mol\": minor
\"mol-core\": major
---

Do cool stuff
"
    )
  }
}