depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Info module.
//!
//! Any info struct is needed to specify used Actions and Pipelines by some shortcut.
//! So, info is just simple form of anything's name and its version.
//!
//! Deployer has short name and version validation.
//!
//! Note: version isn't satisfy `semver` requirements.

use anyhow::bail;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::LazyLock;

use crate::rw::log;

/// Short name.
///
/// You can use anything with numbers, English letters and `-`, `_` characters.
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShortName(String);

impl ShortName {
  /// Validates and creates a short name.
  pub fn new(short_name: impl AsRef<str>) -> anyhow::Result<Self> {
    if !validate_short_name(short_name.as_ref()) {
      bail!("Short names must only contain English characters and `_` and `-` characters.")
    }
    Ok(Self(short_name.as_ref().to_string()))
  }

  /// Represents as `&str`.
  pub fn as_str(&self) -> &str {
    self.0.as_str()
  }
}

impl std::fmt::Display for ShortName {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{}", self.0.as_str())
  }
}

/// Version.
///
/// You can use any version like these: `X`, `X.Y`, `X.Y.Z`;
/// for example: `1.0`, `4.21.9`, etc.
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version(String);

impl Version {
  /// Validates and creates a version.
  pub fn new(version: impl AsRef<str>) -> anyhow::Result<Self> {
    if !validate_version(version.as_ref()) {
      bail!("Versions must be SemVer 2.0 compatible.")
    }
    Ok(Self(version.as_ref().to_string()))
  }

  /// Validates and creates a version, but allows use `latest` version to
  /// use with `UseFromStorage` action type.
  pub fn new_for_using(version: impl AsRef<str>) -> anyhow::Result<Self> {
    if !version.as_ref().eq("latest") && !validate_version(version.as_ref()) {
      bail!("Versions must be SemVer 2.0 compatible, `latest` also allowed.")
    }
    Ok(Self(version.as_ref().to_string()))
  }

  /// Represents as `&str`.
  pub fn as_str(&self) -> &str {
    self.0.as_str()
  }
}

/// Any notable entity info.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Info {
  /// Short name.
  short_name: ShortName,
  /// Version.
  version: Version,
}

/// `StrToInfo` extension trait.
pub trait StrToInfo {
  /// Try to convert this to Info.
  fn to_info(&self) -> anyhow::Result<Info>;
}

impl StrToInfo for String {
  fn to_info(&self) -> anyhow::Result<Info> {
    Info::try_from_str(self)
  }
}
impl StrToInfo for &String {
  fn to_info(&self) -> anyhow::Result<Info> {
    Info::try_from_str(self)
  }
}
impl StrToInfo for &str {
  fn to_info(&self) -> anyhow::Result<Info> {
    Info::try_from_str(self)
  }
}

impl<'de> Deserialize<'de> for Info {
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    str2info(deserializer)
  }
}

impl Serialize for Info {
  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    info2str(self, serializer)
  }
}

static SHORT_NAME_VALIDATOR: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[a-zA-Z_0-9-_]*$").unwrap());

/// Validates the short name.
pub fn validate_short_name(short_name: &str) -> bool {
  SHORT_NAME_VALIDATOR.is_match(short_name)
}

/// Validates the version.
pub fn validate_version(version: &str) -> bool {
  if semver::Version::parse(version).is_err() {
    log(format!(
      "Version `{version}` is invalid! You should specify `SemVer 2.0` version."
    ));
    false
  } else {
    true
  }
}

impl Info {
  /// Validates short name and version of entity and creates info object.
  pub fn new(short_name: impl AsRef<str>, version: impl AsRef<str>) -> anyhow::Result<Self> {
    let short_name = match ShortName::new(short_name) {
      Err(_) => bail!("Short names must only contain English characters and `_` and `-` characters."),
      Ok(v) => v,
    };
    let version = match Version::new(version) {
      Err(_) => bail!("Versions must be SemVer 2.0 compatible."),
      Ok(v) => v,
    };
    Ok(Self::from(short_name, version))
  }

  /// Validates short name and version of entity and creates info object.
  ///
  /// Allows use `latest` version for `UseFromStorage` Action.
  pub fn new_for_using(short_name: impl AsRef<str>, version: impl AsRef<str>) -> anyhow::Result<Self> {
    let short_name = match ShortName::new(short_name) {
      Err(_) => bail!("Short names must only contain English characters and `_` and `-` characters."),
      Ok(v) => v,
    };
    let version = match Version::new_for_using(version) {
      Err(_) => bail!("Versions must be SemVer 2.0 compatible, `latest` also allowed."),
      Ok(v) => v,
    };
    Ok(Self::from(short_name, version))
  }

  /// Constructs info object from `ShortName` and `Version` objects.
  pub fn from(short_name: ShortName, version: Version) -> Self {
    Self { short_name, version }
  }

  /// Returns shortname.
  pub fn short_name(&self) -> &str {
    self.short_name.as_str()
  }

  /// Returns version.
  pub fn version(&self) -> &str {
    self.version.as_str()
  }

  /// Represents whole entity info as a single string.
  ///
  /// Returns a string formatted like this: `short-name@version`.
  pub fn to_str(&self) -> String {
    format!("{}@{}", self.short_name.as_str(), self.version.as_str())
  }

  /// Try to convert a string to entity info object.
  pub fn try_from_str(short_name_and_ver: &str) -> anyhow::Result<Self> {
    let vals = short_name_and_ver.split('@').collect::<Vec<_>>();
    if let Some(short_name) = vals.first()
      && let Some(version) = vals.get(1)
    {
      Info::new(short_name, version)
    } else {
      bail!("Short name and version must be divided by `@` character!")
    }
  }

  /// Try to convert a string to entity info object.
  pub fn try_from_str_wl(short_name_and_ver: &str) -> anyhow::Result<Self> {
    let vals = short_name_and_ver.split('@').collect::<Vec<_>>();
    if let Some(short_name) = vals.first()
      && let Some(version) = vals.get(1)
    {
      Info::new_for_using(short_name, version)
    } else {
      bail!("Short name and version must be divided by `@` character!")
    }
  }
}

/// Action info.
pub type ActionInfo = Info;
/// Pipeline info.
pub type PipelineInfo = Info;
/// Content info.
pub type ContentInfo = Info;

pub(crate) fn str2info<'de, D>(deserializer: D) -> Result<Info, D::Error>
where
  D: serde::Deserializer<'de>,
{
  use serde::de::Error;
  String::deserialize(deserializer).and_then(|string| match Info::try_from_str(string.as_str()) {
    Ok(v) => Ok(v),
    Err(_) => Err(Error::invalid_value(
      serde::de::Unexpected::Str(&string),
      &"some-short-name@{semver2.0-specified-version}",
    )),
  })
}

pub(crate) fn str2info_wl<'de, D>(deserializer: D) -> Result<Info, D::Error>
where
  D: serde::Deserializer<'de>,
{
  use serde::de::Error;
  String::deserialize(deserializer).and_then(|string| match Info::try_from_str_wl(string.as_str()) {
    Ok(v) => Ok(v),
    Err(_) => Err(Error::invalid_value(
      serde::de::Unexpected::Str(&string),
      &"some-short-name@{{semver2.0-specified-version} OR `latest`}",
    )),
  })
}

pub(crate) fn str2info_wl_opt<'de, D>(deserializer: D) -> Result<Option<Info>, D::Error>
where
  D: serde::Deserializer<'de>,
{
  use serde::de::Error;
  Option::<String>::deserialize(deserializer).and_then(|val| match val {
    None => Ok(None),
    Some(string) => match Info::try_from_str_wl(string.as_str()) {
      Ok(v) => Ok(Some(v)),
      Err(_) => Err(Error::invalid_value(
        serde::de::Unexpected::Str(&string),
        &"some-short-name@{{semver2.0-specified-version} OR `latest`}",
      )),
    },
  })
}

pub(crate) fn info2str<S>(v: &Info, serializer: S) -> Result<S::Ok, S::Error>
where
  S: serde::Serializer,
{
  serializer.serialize_str(v.to_str().as_str())
}

pub(crate) fn info2str_opt<S>(v: &Option<Info>, serializer: S) -> Result<S::Ok, S::Error>
where
  S: serde::Serializer,
{
  if let Some(v) = v.as_ref() {
    serializer.serialize_some(v.to_str().as_str())
  } else {
    serializer.serialize_none()
  }
}