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/// Represents a set of JSON overrides for a configuration.
544///
545/// The overrides can be provided as an inline JSON object or loaded from a
546/// separate file via a path or URL.
547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
548#[serde(untagged)]
549pub enum JsonOverrides {
550    /// A path or URL pointing to a JSON file containing the overrides.
551    Location(AssetLocation),
552    /// An inline JSON value representing the overrides.
553    Json(serde_json::Value),
554}
555
556impl From<AssetLocation> for JsonOverrides {
557    fn from(value: AssetLocation) -> Self {
558        Self::Location(value)
559    }
560}
561
562impl From<serde_json::Value> for JsonOverrides {
563    fn from(value: serde_json::Value) -> Self {
564        Self::Json(value)
565    }
566}
567
568impl From<&str> for JsonOverrides {
569    fn from(value: &str) -> Self {
570        Self::Location(AssetLocation::from(value))
571    }
572}
573
574impl Display for JsonOverrides {
575    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576        match self {
577            JsonOverrides::Location(location) => write!(f, "{location}"),
578            JsonOverrides::Json(json) => write!(f, "{json}"),
579        }
580    }
581}
582
583impl JsonOverrides {
584    pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
585        let contents = match self {
586            Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
587                .map_err(|err| {
588                    anyhow!(
589                        "Error converting asset to json {} - {}",
590                        location,
591                        err.to_string()
592                    )
593                }),
594            Self::Json(json) => Ok(json.clone()),
595        };
596
597        contents
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_arg_flag_roundtrip() {
607        let arg = Arg::from("verbose");
608        let serialized = serde_json::to_string(&arg).unwrap();
609        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
610        assert_eq!(arg, deserialized);
611    }
612    #[test]
613    fn test_arg_option_roundtrip() {
614        let arg = Arg::from(("mode", "fast"));
615        let serialized = serde_json::to_string(&arg).unwrap();
616        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
617        assert_eq!(arg, deserialized);
618    }
619
620    #[test]
621    fn test_arg_array_roundtrip() {
622        let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
623
624        let serialized = serde_json::to_string(&arg).unwrap();
625        println!("serialized = {serialized}");
626        let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
627        assert_eq!(arg, deserialized);
628    }
629
630    #[test]
631    fn test_arg_option_valid_input() {
632        let expected = Arg::from(("--foo", "bar"));
633
634        // name and value delimited with =
635        let valid = "\"--foo=bar\"";
636        let result: Result<Arg, _> = serde_json::from_str(valid);
637        assert_eq!(result.unwrap(), expected);
638
639        // name and value delimited with space
640        let valid = "\"--foo bar\"";
641        let result: Result<Arg, _> = serde_json::from_str(valid);
642        assert_eq!(result.unwrap(), expected);
643
644        // value contains =
645        let expected = Arg::from(("--foo", "bar=baz"));
646        let valid = "\"--foo=bar=baz\"";
647        let result: Result<Arg, _> = serde_json::from_str(valid);
648        assert_eq!(result.unwrap(), expected);
649    }
650
651    #[test]
652    fn test_arg_array_valid_input() {
653        let expected = Arg::from(("--foo", vec!["bar", "baz"]));
654
655        // name and values delimited with =
656        let valid = "\"--foo=[bar,baz]\"";
657        let result: Result<Arg, _> = serde_json::from_str(valid);
658        assert_eq!(result.unwrap(), expected);
659
660        // name and values delimited with space
661        let valid = "\"--foo [bar,baz]\"";
662        let result: Result<Arg, _> = serde_json::from_str(valid);
663        assert_eq!(result.unwrap(), expected);
664
665        // values delimited with commas and space
666        let valid = "\"--foo [bar , baz]\"";
667        let result: Result<Arg, _> = serde_json::from_str(valid);
668        assert_eq!(result.unwrap(), expected);
669
670        // empty values array
671        let expected = Arg::from(("--foo", Vec::<&str>::new()));
672        let valid = "\"--foo []\"";
673        let result: Result<Arg, _> = serde_json::from_str(valid);
674        assert_eq!(result.unwrap(), expected);
675    }
676
677    #[test]
678    fn test_arg_invalid_input() {
679        // missing = or space
680        let invalid = "\"--foo[bar]\"";
681        let result: Result<Arg, _> = serde_json::from_str(invalid);
682        assert!(result.is_err());
683
684        // value contains space
685        let invalid = "\"--foo=bar baz\"";
686        let result: Result<Arg, _> = serde_json::from_str(invalid);
687        println!("result = {result:?}");
688        assert!(result.is_err());
689    }
690
691    #[test]
692    fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
693        let got: Result<Chain, ConversionError> = "mychain".try_into();
694
695        assert_eq!(got.unwrap().as_str(), "mychain");
696    }
697
698    #[test]
699    fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
700        let got: Result<Image, ConversionError> = "myimage".try_into();
701
702        assert_eq!(got.unwrap().as_str(), "myimage");
703    }
704
705    #[test]
706    fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
707        let got: Result<Image, ConversionError> = "myimage:version".try_into();
708
709        assert_eq!(got.unwrap().as_str(), "myimage:version");
710    }
711
712    #[test]
713    fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
714        let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
715
716        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
717    }
718
719    #[test]
720    fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
721    {
722        let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
723
724        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
725    }
726
727    #[test]
728    fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
729        let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
730
731        assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
732    }
733
734    #[test]
735    fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
736        let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
737
738        assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
739    }
740
741    #[test]
742    fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
743        let got: Result<Command, ConversionError> = "mycommand".try_into();
744
745        assert_eq!(got.unwrap().as_str(), "mycommand");
746    }
747
748    #[test]
749    fn converting_an_url_into_an_asset_location_should_succeeds() {
750        let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
751        let got: AssetLocation = url.clone().into();
752
753        assert!(matches!(got, AssetLocation::Url(value) if value == url));
754    }
755
756    #[test]
757    fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
758        let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
759        let got: AssetLocation = pathbuf.clone().into();
760
761        assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
762    }
763
764    #[test]
765    fn converting_a_str_into_an_url_asset_location_should_succeeds() {
766        let url = "https://mycloudstorage.com/path/to/my/file.tgz";
767        let got: AssetLocation = url.into();
768
769        assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
770    }
771
772    #[test]
773    fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
774        let filepath = "/tmp/path/to/my/file";
775        let got: AssetLocation = filepath.into();
776
777        assert!(matches!(
778            got,
779            AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
780        ));
781    }
782
783    #[test]
784    fn converting_a_str_into_an_flag_arg_should_succeeds() {
785        let got: Arg = "myflag".into();
786
787        assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
788    }
789
790    #[test]
791    fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
792        let got: Arg = ("name", "value").into();
793
794        assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
795    }
796
797    #[test]
798    fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
799        let got: Result<Chain, ConversionError> = "my chain".try_into();
800
801        assert!(matches!(
802            got.clone().unwrap_err(),
803            ConversionError::ContainsWhitespaces(_)
804        ));
805        assert_eq!(
806            got.unwrap_err().to_string(),
807            "'my chain' shouldn't contains whitespace"
808        );
809    }
810
811    #[test]
812    fn converting_an_empty_str_into_a_chain_should_fails() {
813        let got: Result<Chain, ConversionError> = "".try_into();
814
815        assert!(matches!(
816            got.clone().unwrap_err(),
817            ConversionError::CantBeEmpty
818        ));
819        assert_eq!(got.unwrap_err().to_string(), "can't be empty");
820    }
821
822    #[test]
823    fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
824        let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
825
826        assert!(matches!(
827            got.clone().unwrap_err(),
828            ConversionError::DoesntMatchRegex { value: _, regex: _ }
829        ));
830        assert_eq!(
831            got.unwrap_err().to_string(),
832            "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
833        );
834    }
835
836    #[test]
837    fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
838        let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
839
840        assert!(matches!(
841            got.clone().unwrap_err(),
842            ConversionError::DoesntMatchRegex { value: _, regex: _ }
843        ));
844        assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
845    }
846
847    #[test]
848    fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
849        let got: Result<Image, ConversionError> = "myrepository.com".try_into();
850
851        assert!(matches!(
852            got.clone().unwrap_err(),
853            ConversionError::DoesntMatchRegex { value: _, regex: _ }
854        ));
855        assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
856    }
857
858    #[test]
859    fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
860        let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
861
862        assert!(matches!(
863            got.clone().unwrap_err(),
864            ConversionError::DoesntMatchRegex { value: _, regex: _ }
865        ));
866        assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
867    }
868
869    #[test]
870    fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
871        let got: Result<Command, ConversionError> = "my command".try_into();
872
873        assert!(matches!(
874            got.clone().unwrap_err(),
875            ConversionError::ContainsWhitespaces(_)
876        ));
877        assert_eq!(
878            got.unwrap_err().to_string(),
879            "'my command' shouldn't contains whitespace"
880        );
881    }
882
883    #[test]
884    fn test_convert_to_json_overrides() {
885        let url: AssetLocation = "https://example.com/overrides.json".into();
886        assert!(matches!(
887            url.into(),
888            JsonOverrides::Location(AssetLocation::Url(_))
889        ));
890
891        let path: AssetLocation = "/path/to/overrides.json".into();
892        assert!(matches!(
893            path.into(),
894            JsonOverrides::Location(AssetLocation::FilePath(_))
895        ));
896
897        let inline = serde_json::json!({ "para_id": 2000});
898        assert!(matches!(
899            inline.into(),
900            JsonOverrides::Json(serde_json::Value::Object(_))
901        ));
902    }
903}