use std::{
error::Error,
fmt::{self, Display},
path::PathBuf,
str::FromStr,
};
use anyhow::anyhow;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize};
use support::constants::{INFAILABLE, PREFIX_CANT_BE_NONE, SHOULD_COMPILE, THIS_IS_A_BUG};
use url::Url;
use super::{errors::ConversionError, resources::Resources};
pub type Duration = u32;
pub type Port = u16;
pub type ParaId = u32;
#[derive(Default, Debug, Clone, PartialEq)]
pub struct U128(pub(crate) u128);
impl From<u128> for U128 {
fn from(value: u128) -> Self {
Self(value)
}
}
impl TryFrom<&str> for U128 {
type Error = Box<dyn Error>;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self(value.to_string().parse::<u128>()?))
}
}
impl Serialize for U128 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("U128%{}", self.0))
}
}
struct U128Visitor;
impl de::Visitor<'_> for U128Visitor {
type Value = U128;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer between 0 and 2^128 − 1.")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
v.try_into().map_err(de::Error::custom)
}
}
impl<'de> Deserialize<'de> for U128 {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(U128Visitor)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Chain(String);
impl TryFrom<&str> for Chain {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.contains(char::is_whitespace) {
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
}
if value.is_empty() {
return Err(ConversionError::CantBeEmpty);
}
Ok(Self(value.to_string()))
}
}
impl Chain {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Image(String);
impl TryFrom<&str> for Image {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
static IP_PART: &str = "((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))";
static HOSTNAME_PART: &str = "((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9]))";
static TAG_NAME_PART: &str = "([a-z0-9](-*[a-z0-9])*)";
static TAG_VERSION_PART: &str = "([a-z0-9_]([-._a-z0-9])*)";
lazy_static! {
static ref RE: Regex = Regex::new(&format!(
"^({IP_PART}|{HOSTNAME_PART}/)?{TAG_NAME_PART}(:{TAG_VERSION_PART})?$",
))
.expect(&format!("{}, {}", SHOULD_COMPILE, THIS_IS_A_BUG));
};
if !RE.is_match(value) {
return Err(ConversionError::DoesntMatchRegex {
value: value.to_string(),
regex: "^([ip]|[hostname]/)?[tag_name]:[tag_version]?$".to_string(),
});
}
Ok(Self(value.to_string()))
}
}
impl Image {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Command(String);
impl TryFrom<&str> for Command {
type Error = ConversionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.contains(char::is_whitespace) {
return Err(ConversionError::ContainsWhitespaces(value.to_string()));
}
Ok(Self(value.to_string()))
}
}
impl Default for Command {
fn default() -> Self {
Self(String::from("polkadot"))
}
}
impl Command {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AssetLocation {
Url(Url),
FilePath(PathBuf),
}
impl From<Url> for AssetLocation {
fn from(value: Url) -> Self {
Self::Url(value)
}
}
impl From<PathBuf> for AssetLocation {
fn from(value: PathBuf) -> Self {
Self::FilePath(value)
}
}
impl From<&str> for AssetLocation {
fn from(value: &str) -> Self {
if let Ok(parsed_url) = Url::parse(value) {
return Self::Url(parsed_url);
}
Self::FilePath(
PathBuf::from_str(value).expect(&format!("{}, {}", INFAILABLE, THIS_IS_A_BUG)),
)
}
}
impl Display for AssetLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AssetLocation::Url(value) => write!(f, "{}", value.as_str()),
AssetLocation::FilePath(value) => write!(f, "{}", value.display()),
}
}
}
impl AssetLocation {
pub async fn get_asset(&self) -> Result<Vec<u8>, anyhow::Error> {
let contents = match self {
AssetLocation::Url(location) => {
let res = reqwest::get(location.as_ref()).await.map_err(|err| {
anyhow!(
"Error dowinloding asset from url {} - {}",
location,
err.to_string()
)
})?;
res.bytes().await.unwrap().into()
},
AssetLocation::FilePath(filepath) => {
tokio::fs::read(filepath).await.map_err(|err| {
anyhow!(
"Error reading asset from path {} - {}",
filepath.to_string_lossy(),
err.to_string()
)
})?
},
};
Ok(contents)
}
}
impl Serialize for AssetLocation {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
struct AssetLocationVisitor;
impl de::Visitor<'_> for AssetLocationVisitor {
type Value = AssetLocation;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(AssetLocation::from(v))
}
}
impl<'de> Deserialize<'de> for AssetLocation {
fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(AssetLocationVisitor)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Arg {
Flag(String),
Option(String, String),
}
impl From<&str> for Arg {
fn from(flag: &str) -> Self {
Self::Flag(flag.to_owned())
}
}
impl From<(&str, &str)> for Arg {
fn from((option, value): (&str, &str)) -> Self {
Self::Option(option.to_owned(), value.to_owned())
}
}
impl Serialize for Arg {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Arg::Flag(value) => serializer.serialize_str(value),
Arg::Option(option, value) => {
serializer.serialize_str(&format!("{}={}", option, value))
},
}
}
}
struct ArgVisitor;
impl de::Visitor<'_> for ArgVisitor {
type Value = Arg;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if v.starts_with("-l") || v.starts_with("-log") {
return Ok(Arg::Flag(v.to_string()));
}
let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>.+))?$").unwrap();
let captures = re.captures(v);
if let Some(captures) = captures {
if let Some(value) = captures.name("value") {
return Ok(Arg::Option(
captures
.name("name_prefix")
.expect(&format!("{} {}", PREFIX_CANT_BE_NONE, THIS_IS_A_BUG))
.as_str()
.to_string(),
value.as_str().to_string(),
));
}
if let Some(name_prefix) = captures.name("name_prefix") {
return Ok(Arg::Flag(name_prefix.as_str().to_string()));
}
}
Err(de::Error::custom(
"the provided argument is invalid and doesn't match Arg::Option or Arg::Flag",
))
}
}
impl<'de> Deserialize<'de> for Arg {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(ArgVisitor)
}
}
#[derive(Debug, Default, Clone)]
pub struct ValidationContext {
pub used_ports: Vec<Port>,
pub used_nodes_names: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
pub struct ChainDefaultContext {
pub(crate) default_command: Option<Command>,
pub(crate) default_image: Option<Image>,
pub(crate) default_resources: Option<Resources>,
pub(crate) default_db_snapshot: Option<AssetLocation>,
#[serde(default)]
pub(crate) default_args: Vec<Arg>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
let got: Result<Chain, ConversionError> = "mychain".try_into();
assert_eq!(got.unwrap().as_str(), "mychain");
}
#[test]
fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myimage");
}
#[test]
fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "myimage:version");
}
#[test]
fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
}
#[test]
fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
{
let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
}
#[test]
fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
}
#[test]
fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
}
#[test]
fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
let got: Result<Command, ConversionError> = "mycommand".try_into();
assert_eq!(got.unwrap().as_str(), "mycommand");
}
#[test]
fn converting_an_url_into_an_asset_location_should_succeeds() {
let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
let got: AssetLocation = url.clone().into();
assert!(matches!(got, AssetLocation::Url(value) if value == url));
}
#[test]
fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
let got: AssetLocation = pathbuf.clone().into();
assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
}
#[test]
fn converting_a_str_into_an_url_asset_location_should_succeeds() {
let url = "https://mycloudstorage.com/path/to/my/file.tgz";
let got: AssetLocation = url.into();
assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
}
#[test]
fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
let filepath = "/tmp/path/to/my/file";
let got: AssetLocation = filepath.into();
assert!(matches!(
got,
AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
));
}
#[test]
fn converting_a_str_into_an_flag_arg_should_succeeds() {
let got: Arg = "myflag".into();
assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
}
#[test]
fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
let got: Arg = ("name", "value").into();
assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
}
#[test]
fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
let got: Result<Chain, ConversionError> = "my chain".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::ContainsWhitespaces(_)
));
assert_eq!(
got.unwrap_err().to_string(),
"'my chain' shouldn't contains whitespace"
);
}
#[test]
fn converting_an_empty_str_into_a_chain_should_fails() {
let got: Result<Chain, ConversionError> = "".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::CantBeEmpty
));
assert_eq!(got.unwrap_err().to_string(), "can't be empty");
}
#[test]
fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(
got.unwrap_err().to_string(),
"'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
);
}
#[test]
fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "myrepository.com".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::DoesntMatchRegex { value: _, regex: _ }
));
assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
}
#[test]
fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
let got: Result<Command, ConversionError> = "my command".try_into();
assert!(matches!(
got.clone().unwrap_err(),
ConversionError::ContainsWhitespaces(_)
));
assert_eq!(
got.unwrap_err().to_string(),
"'my command' shouldn't contains whitespace"
);
}
}