use anyhow::bail;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::LazyLock;
use crate::rw::log;
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShortName(String);
impl ShortName {
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()))
}
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())
}
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Version(String);
impl 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()))
}
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()))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Info {
short_name: ShortName,
version: Version,
}
pub trait StrToInfo {
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());
pub fn validate_short_name(short_name: &str) -> bool {
SHORT_NAME_VALIDATOR.is_match(short_name)
}
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 {
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))
}
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))
}
pub fn from(short_name: ShortName, version: Version) -> Self {
Self { short_name, version }
}
pub fn short_name(&self) -> &str {
self.short_name.as_str()
}
pub fn version(&self) -> &str {
self.version.as_str()
}
pub fn to_str(&self) -> String {
format!("{}@{}", self.short_name.as_str(), self.version.as_str())
}
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!")
}
}
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!")
}
}
}
pub type ActionInfo = Info;
pub type PipelineInfo = 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()
}
}