zombienet_configuration/shared/
types.rs

1use std::{
2    collections::{HashMap, HashSet},
3    error::Error,
4    fmt::{self, Display},
5    path::PathBuf,
6    str::FromStr,
7};
8
9use anyhow::anyhow;
10use lazy_static::lazy_static;
11use regex::Regex;
12use serde::{
13    de::{self, IntoDeserializer},
14    Deserialize, Deserializer, Serialize,
15};
16use support::constants::{INFAILABLE, SHOULD_COMPILE, THIS_IS_A_BUG};
17use url::Url;
18
19use super::{errors::ConversionError, resources::Resources};
20
21/// An alias for a duration in seconds.
22pub type Duration = u32;
23
24/// An alias for a port.
25pub type Port = u16;
26
27/// An alias for a parachain ID.
28pub type ParaId = u32;
29
30/// Custom type wrapping u128 to add custom Serialization/Deserialization logic because it's not supported
31/// issue tracking the problem: <https://github.com/toml-rs/toml/issues/540>
32#[derive(Default, Debug, Clone, PartialEq)]
33pub struct U128(pub(crate) u128);
34
35impl From<u128> for U128 {
36    fn from(value: u128) -> Self {
37        Self(value)
38    }
39}
40
41impl TryFrom<&str> for U128 {
42    type Error = Box<dyn Error>;
43
44    fn try_from(value: &str) -> Result<Self, Self::Error> {
45        Ok(Self(value.to_string().parse::<u128>()?))
46    }
47}
48
49impl Serialize for U128 {
50    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
51    where
52        S: serde::Serializer,
53    {
54        // here we add a prefix to the string to be able to replace the wrapped
55        // value with "" to a value without "" in the TOML string
56        serializer.serialize_str(&format!("U128%{}", self.0))
57    }
58}
59
60struct U128Visitor;
61
62impl de::Visitor<'_> for U128Visitor {
63    type Value = U128;
64
65    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
66        formatter.write_str("an integer between 0 and 2^128 − 1.")
67    }
68
69    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
70    where
71        E: de::Error,
72    {
73        v.try_into().map_err(de::Error::custom)
74    }
75}
76
77impl<'de> Deserialize<'de> for U128 {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: Deserializer<'de>,
81    {
82        deserializer.deserialize_str(U128Visitor)
83    }
84}
85
86/// A chain name.
87/// It can be constructed for an `&str`, if it fails, it will returns a [`ConversionError`].
88///
89/// # Examples:
90/// ```
91/// use zombienet_configuration::shared::types::Chain;
92///
93/// let polkadot: Chain = "polkadot".try_into().unwrap();
94/// let kusama: Chain = "kusama".try_into().unwrap();
95/// let myparachain: Chain = "myparachain".try_into().unwrap();
96///
97/// assert_eq!(polkadot.as_str(), "polkadot");
98/// assert_eq!(kusama.as_str(), "kusama");
99/// assert_eq!(myparachain.as_str(), "myparachain");
100/// ```
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102pub struct Chain(String);
103
104impl TryFrom<&str> for Chain {
105    type Error = ConversionError;
106
107    fn try_from(value: &str) -> Result<Self, Self::Error> {
108        if value.contains(char::is_whitespace) {
109            return Err(ConversionError::ContainsWhitespaces(value.to_string()));
110        }
111
112        if value.is_empty() {
113            return Err(ConversionError::CantBeEmpty);
114        }
115
116        Ok(Self(value.to_string()))
117    }
118}
119
120impl Chain {
121    pub fn as_str(&self) -> &str {
122        &self.0
123    }
124}
125
126/// A container image.
127/// It can be constructed from an `&str` including a combination of name, version, IPv4 or/and hostname, if it fails, it will returns a [`ConversionError`].
128///
129/// # Examples:
130/// ```
131/// use zombienet_configuration::shared::types::Image;
132///
133/// let image1: Image = "name".try_into().unwrap();
134/// let image2: Image = "name:version".try_into().unwrap();
135/// let image3: Image = "myrepo.com/name:version".try_into().unwrap();
136/// let image4: Image = "10.15.43.155/name:version".try_into().unwrap();
137///
138/// assert_eq!(image1.as_str(), "name");
139/// assert_eq!(image2.as_str(), "name:version");
140/// assert_eq!(image3.as_str(), "myrepo.com/name:version");
141/// assert_eq!(image4.as_str(), "10.15.43.155/name:version");
142/// ```
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct Image(String);
145
146impl TryFrom<&str> for Image {
147    type Error = ConversionError;
148
149    fn try_from(value: &str) -> Result<Self, Self::Error> {
150        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]))";
151        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]))";
152        static TAG_NAME_PART: &str = "([a-z0-9](-*[a-z0-9])*)";
153        static TAG_VERSION_PART: &str = "([a-z0-9_]([-._a-z0-9])*)";
154        lazy_static! {
155            static ref RE: Regex = Regex::new(&format!(
156                "^({IP_PART}|{HOSTNAME_PART}/)?{TAG_NAME_PART}(:{TAG_VERSION_PART})?$",
157            ))
158            .expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
159        };
160
161        if !RE.is_match(value) {
162            return Err(ConversionError::DoesntMatchRegex {
163                value: value.to_string(),
164                regex: "^([ip]|[hostname]/)?[tag_name]:[tag_version]?$".to_string(),
165            });
166        }
167
168        Ok(Self(value.to_string()))
169    }
170}
171
172impl Image {
173    pub fn as_str(&self) -> &str {
174        &self.0
175    }
176}
177
178/// A command that will be executed natively (native provider) or in a container (podman/k8s).
179/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
180///
181/// # Examples:
182/// ```
183/// use zombienet_configuration::shared::types::Command;
184///
185/// let command1: Command = "mycommand".try_into().unwrap();
186/// let command2: Command = "myothercommand".try_into().unwrap();
187///
188/// assert_eq!(command1.as_str(), "mycommand");
189/// assert_eq!(command2.as_str(), "myothercommand");
190/// ```
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub struct Command(String);
193
194impl TryFrom<&str> for Command {
195    type Error = ConversionError;
196
197    fn try_from(value: &str) -> Result<Self, Self::Error> {
198        if value.contains(char::is_whitespace) {
199            return Err(ConversionError::ContainsWhitespaces(value.to_string()));
200        }
201
202        Ok(Self(value.to_string()))
203    }
204}
205impl Default for Command {
206    fn default() -> Self {
207        Self(String::from("polkadot"))
208    }
209}
210
211impl Command {
212    pub fn as_str(&self) -> &str {
213        &self.0
214    }
215}
216
217/// A command with optional custom arguments, the command will be executed natively (native provider) or in a container (podman/k8s).
218/// It can be constructed from an `&str`, if it fails, it will returns a [`ConversionError`].
219///
220/// # Examples:
221/// ```
222/// use zombienet_configuration::shared::types::CommandWithCustomArgs;
223///
224/// let command1: CommandWithCustomArgs = "mycommand --demo=2 --other-flag".try_into().unwrap();
225/// let command2: CommandWithCustomArgs = "my_other_cmd_without_args".try_into().unwrap();
226///
227/// assert_eq!(command1.cmd().as_str(), "mycommand");
228/// assert_eq!(command2.cmd().as_str(), "my_other_cmd_without_args");
229/// ```
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub struct CommandWithCustomArgs(Command, Vec<Arg>);
232
233impl TryFrom<&str> for CommandWithCustomArgs {
234    type Error = ConversionError;
235
236    fn try_from(value: &str) -> Result<Self, Self::Error> {
237        if value.is_empty() {
238            return Err(ConversionError::CantBeEmpty);
239        }
240
241        let mut parts = value.split_whitespace().collect::<Vec<&str>>();
242        let cmd = parts.remove(0).try_into().unwrap();
243        let args = parts
244            .iter()
245            .map(|x| {
246                Arg::deserialize(x.into_deserializer()).map_err(|_: serde_json::Error| {
247                    ConversionError::DeserializeError(String::from(*x))
248                })
249            })
250            .collect::<Result<Vec<Arg>, _>>()?;
251
252        Ok(Self(cmd, args))
253    }
254}
255impl Default for CommandWithCustomArgs {
256    fn default() -> Self {
257        Self("polkadot".try_into().unwrap(), vec![])
258    }
259}
260
261impl CommandWithCustomArgs {
262    pub fn cmd(&self) -> &Command {
263        &self.0
264    }
265
266    pub fn args(&self) -> &Vec<Arg> {
267        &self.1
268    }
269}
270
271/// A location for a locally or remotely stored asset.
272/// It can be constructed from an [`url::Url`], a [`std::path::PathBuf`] or an `&str`.
273///
274/// # Examples:
275/// ```
276/// use url::Url;
277/// use std::{path::PathBuf, str::FromStr};
278/// use zombienet_configuration::shared::types::AssetLocation;
279///
280/// let url_location: AssetLocation = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap().into();
281/// let url_location2: AssetLocation = "https://mycloudstorage.com/path/to/my/file.tgz".into();
282/// let path_location: AssetLocation = PathBuf::from_str("/tmp/path/to/my/file").unwrap().into();
283/// let path_location2: AssetLocation = "/tmp/path/to/my/file".into();
284///
285/// assert!(matches!(url_location, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
286/// assert!(matches!(url_location2, AssetLocation::Url(value) if value.as_str() == "https://mycloudstorage.com/path/to/my/file.tgz"));
287/// assert!(matches!(path_location, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
288/// assert!(matches!(path_location2, AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/path/to/my/file"));
289/// ```
290#[derive(Debug, Clone, PartialEq)]
291pub enum AssetLocation {
292    Url(Url),
293    FilePath(PathBuf),
294}
295
296impl From<Url> for AssetLocation {
297    fn from(value: Url) -> Self {
298        Self::Url(value)
299    }
300}
301
302impl From<PathBuf> for AssetLocation {
303    fn from(value: PathBuf) -> Self {
304        Self::FilePath(value)
305    }
306}
307
308impl From<&str> for AssetLocation {
309    fn from(value: &str) -> Self {
310        if let Ok(parsed_url) = Url::parse(value) {
311            return Self::Url(parsed_url);
312        }
313
314        Self::FilePath(PathBuf::from_str(value).expect(&format!("{INFAILABLE}, {THIS_IS_A_BUG}")))
315    }
316}
317
318impl Display for AssetLocation {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        match self {
321            AssetLocation::Url(value) => write!(f, "{}", value.as_str()),
322            AssetLocation::FilePath(value) => write!(f, "{}", value.display()),
323        }
324    }
325}
326
327impl AssetLocation {
328    pub async fn get_asset(&self) -> Result<Vec<u8>, anyhow::Error> {
329        let contents = match self {
330            AssetLocation::Url(location) => {
331                let res = reqwest::get(location.as_ref()).await.map_err(|err| {
332                    anyhow!(
333                        "Error dowinloding asset from url {} - {}",
334                        location,
335                        err.to_string()
336                    )
337                })?;
338
339                res.bytes().await.unwrap().into()
340            },
341            AssetLocation::FilePath(filepath) => {
342                tokio::fs::read(filepath).await.map_err(|err| {
343                    anyhow!(
344                        "Error reading asset from path {} - {}",
345                        filepath.to_string_lossy(),
346                        err.to_string()
347                    )
348                })?
349            },
350        };
351
352        Ok(contents)
353    }
354}
355
356impl Serialize for AssetLocation {
357    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
358    where
359        S: serde::Serializer,
360    {
361        serializer.serialize_str(&self.to_string())
362    }
363}
364
365struct AssetLocationVisitor;
366
367impl de::Visitor<'_> for AssetLocationVisitor {
368    type Value = AssetLocation;
369
370    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
371        formatter.write_str("a string")
372    }
373
374    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
375    where
376        E: de::Error,
377    {
378        Ok(AssetLocation::from(v))
379    }
380}
381
382impl<'de> Deserialize<'de> for AssetLocation {
383    fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
384    where
385        D: Deserializer<'de>,
386    {
387        deserializer.deserialize_any(AssetLocationVisitor)
388    }
389}
390
391/// A CLI argument passed to an executed command, can be an option with an assigned value or a simple flag to enable/disable a feature.
392/// A flag arg can be constructed from a `&str` and a option arg can be constructed from a `(&str, &str)`.
393///
394/// # Examples:
395/// ```
396/// use zombienet_configuration::shared::types::Arg;
397///
398/// let flag_arg: Arg = "myflag".into();
399/// let option_arg: Arg = ("name", "value").into();
400///
401/// assert!(matches!(flag_arg, Arg::Flag(value) if value == "myflag"));
402/// assert!(matches!(option_arg, Arg::Option(name, value) if name == "name" && value == "value"));
403/// ```
404#[derive(Debug, Clone, PartialEq)]
405pub enum Arg {
406    Flag(String),
407    Option(String, String),
408    Array(String, Vec<String>),
409}
410
411impl From<&str> for Arg {
412    fn from(flag: &str) -> Self {
413        Self::Flag(flag.to_owned())
414    }
415}
416
417impl From<(&str, &str)> for Arg {
418    fn from((option, value): (&str, &str)) -> Self {
419        Self::Option(option.to_owned(), value.to_owned())
420    }
421}
422
423impl<T> From<(&str, &[T])> for Arg
424where
425    T: AsRef<str> + Clone,
426{
427    fn from((option, values): (&str, &[T])) -> Self {
428        Self::Array(
429            option.to_owned(),
430            values.iter().map(|v| v.as_ref().to_string()).collect(),
431        )
432    }
433}
434
435impl<T> From<(&str, Vec<T>)> for Arg
436where
437    T: AsRef<str>,
438{
439    fn from((option, values): (&str, Vec<T>)) -> Self {
440        Self::Array(
441            option.to_owned(),
442            values.into_iter().map(|v| v.as_ref().to_string()).collect(),
443        )
444    }
445}
446
447impl Serialize for Arg {
448    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
449    where
450        S: serde::Serializer,
451    {
452        match self {
453            Arg::Flag(value) => serializer.serialize_str(value),
454            Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
455            Arg::Array(option, values) => {
456                serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
457            },
458        }
459    }
460}
461
462struct ArgVisitor;
463
464impl de::Visitor<'_> for ArgVisitor {
465    type Value = Arg;
466
467    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
468        formatter.write_str("a string")
469    }
470
471    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
472    where
473        E: de::Error,
474    {
475        // covers the "-lruntime=debug,parachain=trace" case
476        // TODO: Make this more generic by adding the scenario in the regex below
477        if v.starts_with("-l") || v.starts_with("-log") {
478            return Ok(Arg::Flag(v.to_string()));
479        }
480        let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
481
482        let captures = re.captures(v);
483        if let Some(captures) = captures {
484            if let Some(value) = captures.name("value") {
485                let name_prefix = captures
486                    .name("name_prefix")
487                    .expect("BUG: name_prefix capture group missing")
488                    .as_str()
489                    .to_string();
490
491                let val = value.as_str();
492                if val.starts_with('[') && val.ends_with(']') {
493                    // Remove brackets and split by comma
494                    let inner = &val[1..val.len() - 1];
495                    let items: Vec<String> = inner
496                        .split(',')
497                        .map(|s| s.trim().to_string())
498                        .filter(|s| !s.is_empty())
499                        .collect();
500                    return Ok(Arg::Array(name_prefix, items));
501                } else {
502                    return Ok(Arg::Option(name_prefix, val.to_string()));
503                }
504            }
505            if let Some(name_prefix) = captures.name("name_prefix") {
506                return Ok(Arg::Flag(name_prefix.as_str().to_string()));
507            }
508        }
509
510        Err(de::Error::custom(
511            "the provided argument is invalid and doesn't match Arg::Option, Arg::Flag or Arg::Array",
512        ))
513    }
514}
515
516impl<'de> Deserialize<'de> for Arg {
517    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
518    where
519        D: Deserializer<'de>,
520    {
521        deserializer.deserialize_any(ArgVisitor)
522    }
523}
524
525#[derive(Debug, Default, Clone)]
526pub struct ValidationContext {
527    pub used_ports: Vec<Port>,
528    pub used_nodes_names: HashSet<String>,
529    // Store para_id already used
530    pub used_para_ids: HashMap<ParaId, u8>,
531}
532
533#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
534pub struct ChainDefaultContext {
535    pub(crate) default_command: Option<Command>,
536    pub(crate) default_image: Option<Image>,
537    pub(crate) default_resources: Option<Resources>,
538    pub(crate) default_db_snapshot: Option<AssetLocation>,
539    #[serde(default)]
540    pub(crate) default_args: Vec<Arg>,
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn test_arg_flag_roundtrip() {
549        let arg = Arg::from("verbose");
550        let serialized = serde_json::to_string(&arg).unwrap();
551        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
552        assert_eq!(arg, deserialized);
553    }
554    #[test]
555    fn test_arg_option_roundtrip() {
556        let arg = Arg::from(("mode", "fast"));
557        let serialized = serde_json::to_string(&arg).unwrap();
558        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
559        assert_eq!(arg, deserialized);
560    }
561
562    #[test]
563    fn test_arg_array_roundtrip() {
564        let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
565
566        let serialized = serde_json::to_string(&arg).unwrap();
567        println!("serialized = {serialized}");
568        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
569        assert_eq!(arg, deserialized);
570    }
571
572    #[test]
573    fn test_arg_option_valid_input() {
574        let expected = Arg::from(("--foo", "bar"));
575
576        // name and value delimited with =
577        let valid = "\"--foo=bar\"";
578        let result: Result<Arg, _> = serde_json::from_str(valid);
579        assert_eq!(result.unwrap(), expected);
580
581        // name and value delimited with space
582        let valid = "\"--foo bar\"";
583        let result: Result<Arg, _> = serde_json::from_str(valid);
584        assert_eq!(result.unwrap(), expected);
585
586        // value contains =
587        let expected = Arg::from(("--foo", "bar=baz"));
588        let valid = "\"--foo=bar=baz\"";
589        let result: Result<Arg, _> = serde_json::from_str(valid);
590        assert_eq!(result.unwrap(), expected);
591    }
592
593    #[test]
594    fn test_arg_array_valid_input() {
595        let expected = Arg::from(("--foo", vec!["bar", "baz"]));
596
597        // name and values delimited with =
598        let valid = "\"--foo=[bar,baz]\"";
599        let result: Result<Arg, _> = serde_json::from_str(valid);
600        assert_eq!(result.unwrap(), expected);
601
602        // name and values delimited with space
603        let valid = "\"--foo [bar,baz]\"";
604        let result: Result<Arg, _> = serde_json::from_str(valid);
605        assert_eq!(result.unwrap(), expected);
606
607        // values delimited with commas and space
608        let valid = "\"--foo [bar , baz]\"";
609        let result: Result<Arg, _> = serde_json::from_str(valid);
610        assert_eq!(result.unwrap(), expected);
611
612        // empty values array
613        let expected = Arg::from(("--foo", Vec::<&str>::new()));
614        let valid = "\"--foo []\"";
615        let result: Result<Arg, _> = serde_json::from_str(valid);
616        assert_eq!(result.unwrap(), expected);
617    }
618
619    #[test]
620    fn test_arg_invalid_input() {
621        // missing = or space
622        let invalid = "\"--foo[bar]\"";
623        let result: Result<Arg, _> = serde_json::from_str(invalid);
624        assert!(result.is_err());
625
626        // value contains space
627        let invalid = "\"--foo=bar baz\"";
628        let result: Result<Arg, _> = serde_json::from_str(invalid);
629        println!("result = {result:?}");
630        assert!(result.is_err());
631    }
632
633    #[test]
634    fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
635        let got: Result<Chain, ConversionError> = "mychain".try_into();
636
637        assert_eq!(got.unwrap().as_str(), "mychain");
638    }
639
640    #[test]
641    fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
642        let got: Result<Image, ConversionError> = "myimage".try_into();
643
644        assert_eq!(got.unwrap().as_str(), "myimage");
645    }
646
647    #[test]
648    fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
649        let got: Result<Image, ConversionError> = "myimage:version".try_into();
650
651        assert_eq!(got.unwrap().as_str(), "myimage:version");
652    }
653
654    #[test]
655    fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
656        let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
657
658        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
659    }
660
661    #[test]
662    fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
663    {
664        let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
665
666        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
667    }
668
669    #[test]
670    fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
671        let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
672
673        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
674    }
675
676    #[test]
677    fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
678        let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
679
680        assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
681    }
682
683    #[test]
684    fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
685        let got: Result<Command, ConversionError> = "mycommand".try_into();
686
687        assert_eq!(got.unwrap().as_str(), "mycommand");
688    }
689
690    #[test]
691    fn converting_an_url_into_an_asset_location_should_succeeds() {
692        let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
693        let got: AssetLocation = url.clone().into();
694
695        assert!(matches!(got, AssetLocation::Url(value) if value == url));
696    }
697
698    #[test]
699    fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
700        let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
701        let got: AssetLocation = pathbuf.clone().into();
702
703        assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
704    }
705
706    #[test]
707    fn converting_a_str_into_an_url_asset_location_should_succeeds() {
708        let url = "https://mycloudstorage.com/path/to/my/file.tgz";
709        let got: AssetLocation = url.into();
710
711        assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
712    }
713
714    #[test]
715    fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
716        let filepath = "/tmp/path/to/my/file";
717        let got: AssetLocation = filepath.into();
718
719        assert!(matches!(
720            got,
721            AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
722        ));
723    }
724
725    #[test]
726    fn converting_a_str_into_an_flag_arg_should_succeeds() {
727        let got: Arg = "myflag".into();
728
729        assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
730    }
731
732    #[test]
733    fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
734        let got: Arg = ("name", "value").into();
735
736        assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
737    }
738
739    #[test]
740    fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
741        let got: Result<Chain, ConversionError> = "my chain".try_into();
742
743        assert!(matches!(
744            got.clone().unwrap_err(),
745            ConversionError::ContainsWhitespaces(_)
746        ));
747        assert_eq!(
748            got.unwrap_err().to_string(),
749            "'my chain' shouldn't contains whitespace"
750        );
751    }
752
753    #[test]
754    fn converting_an_empty_str_into_a_chain_should_fails() {
755        let got: Result<Chain, ConversionError> = "".try_into();
756
757        assert!(matches!(
758            got.clone().unwrap_err(),
759            ConversionError::CantBeEmpty
760        ));
761        assert_eq!(got.unwrap_err().to_string(), "can't be empty");
762    }
763
764    #[test]
765    fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
766        let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
767
768        assert!(matches!(
769            got.clone().unwrap_err(),
770            ConversionError::DoesntMatchRegex { value: _, regex: _ }
771        ));
772        assert_eq!(
773            got.unwrap_err().to_string(),
774            "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
775        );
776    }
777
778    #[test]
779    fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
780        let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
781
782        assert!(matches!(
783            got.clone().unwrap_err(),
784            ConversionError::DoesntMatchRegex { value: _, regex: _ }
785        ));
786        assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
787    }
788
789    #[test]
790    fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
791        let got: Result<Image, ConversionError> = "myrepository.com".try_into();
792
793        assert!(matches!(
794            got.clone().unwrap_err(),
795            ConversionError::DoesntMatchRegex { value: _, regex: _ }
796        ));
797        assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
798    }
799
800    #[test]
801    fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
802        let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
803
804        assert!(matches!(
805            got.clone().unwrap_err(),
806            ConversionError::DoesntMatchRegex { value: _, regex: _ }
807        ));
808        assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
809    }
810
811    #[test]
812    fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
813        let got: Result<Command, ConversionError> = "my command".try_into();
814
815        assert!(matches!(
816            got.clone().unwrap_err(),
817            ConversionError::ContainsWhitespaces(_)
818        ));
819        assert_eq!(
820            got.unwrap_err().to_string(),
821            "'my command' shouldn't contains whitespace"
822        );
823    }
824}