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 tokio::fs;
18use url::Url;
19
20use super::{errors::ConversionError, resources::Resources};
21
22pub type Duration = u32;
24
25pub type Port = u16;
27
28pub type ParaId = u32;
30
31#[derive(Default, Debug, Clone, PartialEq)]
34pub struct U128(pub(crate) u128);
35
36impl From<u128> for U128 {
37 fn from(value: u128) -> Self {
38 Self(value)
39 }
40}
41
42impl TryFrom<&str> for U128 {
43 type Error = Box<dyn Error>;
44
45 fn try_from(value: &str) -> Result<Self, Self::Error> {
46 Ok(Self(value.to_string().parse::<u128>()?))
47 }
48}
49
50impl Serialize for U128 {
51 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
52 where
53 S: serde::Serializer,
54 {
55 serializer.serialize_str(&format!("U128%{}", self.0))
58 }
59}
60
61struct U128Visitor;
62
63impl de::Visitor<'_> for U128Visitor {
64 type Value = U128;
65
66 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
67 formatter.write_str("an integer between 0 and 2^128 − 1.")
68 }
69
70 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
71 where
72 E: de::Error,
73 {
74 v.try_into().map_err(de::Error::custom)
75 }
76}
77
78impl<'de> Deserialize<'de> for U128 {
79 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80 where
81 D: Deserializer<'de>,
82 {
83 deserializer.deserialize_str(U128Visitor)
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct Chain(String);
104
105impl TryFrom<&str> for Chain {
106 type Error = ConversionError;
107
108 fn try_from(value: &str) -> Result<Self, Self::Error> {
109 if value.contains(char::is_whitespace) {
110 return Err(ConversionError::ContainsWhitespaces(value.to_string()));
111 }
112
113 if value.is_empty() {
114 return Err(ConversionError::CantBeEmpty);
115 }
116
117 Ok(Self(value.to_string()))
118 }
119}
120
121impl Chain {
122 pub fn as_str(&self) -> &str {
123 &self.0
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub struct Image(String);
146
147impl TryFrom<&str> for Image {
148 type Error = ConversionError;
149
150 fn try_from(value: &str) -> Result<Self, Self::Error> {
151 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]))";
152 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]))";
153 static TAG_NAME_PART: &str = "([a-z0-9](-*[a-z0-9])*)";
154 static TAG_VERSION_PART: &str = "([a-z0-9_]([-._a-z0-9])*)";
155 lazy_static! {
156 static ref RE: Regex = Regex::new(&format!(
157 "^({IP_PART}|{HOSTNAME_PART}/)?{TAG_NAME_PART}(:{TAG_VERSION_PART})?$",
158 ))
159 .expect(&format!("{SHOULD_COMPILE}, {THIS_IS_A_BUG}"));
160 };
161
162 if !RE.is_match(value) {
163 return Err(ConversionError::DoesntMatchRegex {
164 value: value.to_string(),
165 regex: "^([ip]|[hostname]/)?[tag_name]:[tag_version]?$".to_string(),
166 });
167 }
168
169 Ok(Self(value.to_string()))
170 }
171}
172
173impl Image {
174 pub fn as_str(&self) -> &str {
175 &self.0
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
193pub struct Command(String);
194
195impl TryFrom<&str> for Command {
196 type Error = ConversionError;
197
198 fn try_from(value: &str) -> Result<Self, Self::Error> {
199 if value.contains(char::is_whitespace) {
200 return Err(ConversionError::ContainsWhitespaces(value.to_string()));
201 }
202
203 Ok(Self(value.to_string()))
204 }
205}
206impl Default for Command {
207 fn default() -> Self {
208 Self(String::from("polkadot"))
209 }
210}
211
212impl Command {
213 pub fn as_str(&self) -> &str {
214 &self.0
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
232pub struct CommandWithCustomArgs(Command, Vec<Arg>);
233
234impl TryFrom<&str> for CommandWithCustomArgs {
235 type Error = ConversionError;
236
237 fn try_from(value: &str) -> Result<Self, Self::Error> {
238 if value.is_empty() {
239 return Err(ConversionError::CantBeEmpty);
240 }
241
242 let mut parts = value.split_whitespace().collect::<Vec<&str>>();
243 let cmd = parts.remove(0).try_into().unwrap();
244 let args = parts
245 .iter()
246 .map(|x| {
247 Arg::deserialize(x.into_deserializer()).map_err(|_: serde_json::Error| {
248 ConversionError::DeserializeError(String::from(*x))
249 })
250 })
251 .collect::<Result<Vec<Arg>, _>>()?;
252
253 Ok(Self(cmd, args))
254 }
255}
256impl Default for CommandWithCustomArgs {
257 fn default() -> Self {
258 Self("polkadot".try_into().unwrap(), vec![])
259 }
260}
261
262impl CommandWithCustomArgs {
263 pub fn cmd(&self) -> &Command {
264 &self.0
265 }
266
267 pub fn args(&self) -> &Vec<Arg> {
268 &self.1
269 }
270}
271
272#[derive(Debug, Clone, PartialEq)]
292pub enum AssetLocation {
293 Url(Url),
294 FilePath(PathBuf),
295}
296
297impl From<Url> for AssetLocation {
298 fn from(value: Url) -> Self {
299 Self::Url(value)
300 }
301}
302
303impl From<PathBuf> for AssetLocation {
304 fn from(value: PathBuf) -> Self {
305 Self::FilePath(value)
306 }
307}
308
309impl From<&str> for AssetLocation {
310 fn from(value: &str) -> Self {
311 if let Ok(parsed_url) = Url::parse(value) {
312 return Self::Url(parsed_url);
313 }
314
315 Self::FilePath(PathBuf::from_str(value).expect(&format!("{INFAILABLE}, {THIS_IS_A_BUG}")))
316 }
317}
318
319impl Display for AssetLocation {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 match self {
322 AssetLocation::Url(value) => write!(f, "{}", value.as_str()),
323 AssetLocation::FilePath(value) => write!(f, "{}", value.display()),
324 }
325 }
326}
327
328impl AssetLocation {
329 pub async fn get_asset(&self) -> Result<Vec<u8>, anyhow::Error> {
331 let contents = match self {
332 AssetLocation::Url(location) => {
333 let res = reqwest::get(location.as_ref()).await.map_err(|err| {
334 anyhow!("Error dowinloding asset from url {location} - {err}")
335 })?;
336
337 res.bytes().await.unwrap().into()
338 },
339 AssetLocation::FilePath(filepath) => {
340 tokio::fs::read(filepath).await.map_err(|err| {
341 anyhow!(
342 "Error reading asset from path {} - {}",
343 filepath.to_string_lossy(),
344 err
345 )
346 })?
347 },
348 };
349
350 Ok(contents)
351 }
352
353 pub async fn dump_asset(&self, dst_path: impl Into<PathBuf>) -> Result<(), anyhow::Error> {
355 let contents = self.get_asset().await?;
356 fs::write(dst_path.into(), contents).await?;
357 Ok(())
358 }
359}
360
361impl Serialize for AssetLocation {
362 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
363 where
364 S: serde::Serializer,
365 {
366 serializer.serialize_str(&self.to_string())
367 }
368}
369
370struct AssetLocationVisitor;
371
372impl de::Visitor<'_> for AssetLocationVisitor {
373 type Value = AssetLocation;
374
375 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
376 formatter.write_str("a string")
377 }
378
379 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
380 where
381 E: de::Error,
382 {
383 Ok(AssetLocation::from(v))
384 }
385}
386
387impl<'de> Deserialize<'de> for AssetLocation {
388 fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
389 where
390 D: Deserializer<'de>,
391 {
392 deserializer.deserialize_any(AssetLocationVisitor)
393 }
394}
395
396#[derive(Debug, Clone, PartialEq)]
415pub enum Arg {
416 Flag(String),
417 Option(String, String),
418 Array(String, Vec<String>),
419 Positional(String),
420}
421
422impl From<&str> for Arg {
423 fn from(flag: &str) -> Self {
424 Self::Flag(flag.to_owned())
425 }
426}
427
428impl From<(&str, &str)> for Arg {
429 fn from((option, value): (&str, &str)) -> Self {
430 Self::Option(option.to_owned(), value.to_owned())
431 }
432}
433
434impl<T> From<(&str, &[T])> for Arg
435where
436 T: AsRef<str> + Clone,
437{
438 fn from((option, values): (&str, &[T])) -> Self {
439 Self::Array(
440 option.to_owned(),
441 values.iter().map(|v| v.as_ref().to_string()).collect(),
442 )
443 }
444}
445
446impl<T> From<(&str, Vec<T>)> for Arg
447where
448 T: AsRef<str>,
449{
450 fn from((option, values): (&str, Vec<T>)) -> Self {
451 Self::Array(
452 option.to_owned(),
453 values.into_iter().map(|v| v.as_ref().to_string()).collect(),
454 )
455 }
456}
457
458impl Arg {
459 pub fn to_vec(&self) -> Vec<String> {
461 match self {
462 Arg::Flag(arg) => vec![arg.to_string()],
463 Arg::Option(k, v) => vec![k.to_string(), v.to_string()],
464 Arg::Array(k, items) => [
465 vec![k.to_string()],
466 items.iter().map(|x| x.to_string()).collect::<Vec<String>>(),
467 ]
468 .concat(),
469 Arg::Positional(value) => vec![value.to_string()],
470 }
471 }
472}
473impl Serialize for Arg {
474 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
475 where
476 S: serde::Serializer,
477 {
478 match self {
479 Arg::Flag(value) => serializer.serialize_str(value),
480 Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
481 Arg::Array(option, values) => {
482 serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
483 },
484 Arg::Positional(value) => serializer.serialize_str(value),
485 }
486 }
487}
488
489struct ArgVisitor;
490
491impl de::Visitor<'_> for ArgVisitor {
492 type Value = Arg;
493
494 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
495 formatter.write_str("a string")
496 }
497
498 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
499 where
500 E: de::Error,
501 {
502 if v.starts_with("-l") || v.starts_with("-log") {
505 return Ok(Arg::Flag(v.to_string()));
506 }
507 if v.starts_with("-:") {
509 return Ok(Arg::Flag(v.to_string()));
510 }
511
512 let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
513
514 let captures = re.captures(v);
515 if let Some(captures) = captures {
516 if let Some(value) = captures.name("value") {
517 let name_prefix = captures
518 .name("name_prefix")
519 .expect("BUG: name_prefix capture group missing")
520 .as_str()
521 .to_string();
522
523 let val = value.as_str();
524 if val.starts_with('[') && val.ends_with(']') {
525 let inner = &val[1..val.len() - 1];
527 let items: Vec<String> = inner
528 .split(',')
529 .map(|s| s.trim().to_string())
530 .filter(|s| !s.is_empty())
531 .collect();
532 return Ok(Arg::Array(name_prefix, items));
533 } else {
534 return Ok(Arg::Option(name_prefix, val.to_string()));
535 }
536 }
537 if let Some(name_prefix) = captures.name("name_prefix") {
538 return Ok(Arg::Flag(name_prefix.as_str().to_string()));
539 }
540 }
541
542 Ok(Arg::Positional(v.to_string()))
544 }
545}
546
547impl<'de> Deserialize<'de> for Arg {
548 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549 where
550 D: Deserializer<'de>,
551 {
552 deserializer.deserialize_any(ArgVisitor)
553 }
554}
555
556#[derive(Debug, Default, Clone)]
557pub struct ValidationContext {
558 pub used_ports: Vec<Port>,
559 pub used_nodes_names: HashSet<String>,
560 pub used_para_ids: HashMap<ParaId, u8>,
562}
563
564#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
565pub struct ChainDefaultContext {
566 pub(crate) default_command: Option<Command>,
567 pub(crate) default_image: Option<Image>,
568 pub(crate) default_resources: Option<Resources>,
569 pub(crate) default_db_snapshot: Option<AssetLocation>,
570 #[serde(default)]
571 pub(crate) default_args: Vec<Arg>,
572}
573
574#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577pub struct ChainSpecRuntime {
578 pub location: AssetLocation,
579 pub preset: Option<String>,
580}
581
582impl ChainSpecRuntime {
583 pub fn new(location: AssetLocation) -> Self {
584 ChainSpecRuntime {
585 location,
586 preset: None,
587 }
588 }
589
590 pub fn with_preset(location: AssetLocation, preset: impl Into<String>) -> Self {
591 ChainSpecRuntime {
592 location,
593 preset: Some(preset.into()),
594 }
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
603#[serde(untagged)]
604pub enum JsonOverrides {
605 Location(AssetLocation),
607 Json(serde_json::Value),
609}
610
611impl From<AssetLocation> for JsonOverrides {
612 fn from(value: AssetLocation) -> Self {
613 Self::Location(value)
614 }
615}
616
617impl From<serde_json::Value> for JsonOverrides {
618 fn from(value: serde_json::Value) -> Self {
619 Self::Json(value)
620 }
621}
622
623impl From<&str> for JsonOverrides {
624 fn from(value: &str) -> Self {
625 Self::Location(AssetLocation::from(value))
626 }
627}
628
629impl Display for JsonOverrides {
630 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
631 match self {
632 JsonOverrides::Location(location) => write!(f, "{location}"),
633 JsonOverrides::Json(json) => write!(f, "{json}"),
634 }
635 }
636}
637
638impl JsonOverrides {
639 pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
640 let contents = match self {
641 Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
642 .map_err(|err| anyhow!("Error converting asset to json {location} - {err}")),
643 Self::Json(json) => Ok(json.clone()),
644 };
645
646 contents
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_arg_flag_roundtrip() {
656 let arg = Arg::from("verbose");
657 let serialized = serde_json::to_string(&arg).unwrap();
658 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
659 assert_eq!(arg, deserialized);
660 }
661
662 #[test]
663 fn test_urls_as_arg() {
664 let arg = Arg::from("ws://127.0.0.1:10000");
665 assert_eq!(Arg::Flag(String::from("ws://127.0.0.1:10000")), arg);
666 }
667 #[test]
668 fn test_script_as_arg() {
669 let arg = Arg::from("scripts/assign-cores.sh");
670 assert_eq!(Arg::Flag(String::from("scripts/assign-cores.sh")), arg);
671 }
672
673 #[test]
674 fn test_arg_option_roundtrip() {
675 let arg = Arg::from(("mode", "fast"));
676 let serialized = serde_json::to_string(&arg).unwrap();
677 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
678 assert_eq!(arg, deserialized);
679 }
680
681 #[test]
682 fn test_arg_array_roundtrip() {
683 let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
684
685 let serialized = serde_json::to_string(&arg).unwrap();
686 println!("serialized = {serialized}");
687 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
688 assert_eq!(arg, deserialized);
689 }
690
691 #[test]
692 fn test_arg_option_valid_input() {
693 let expected = Arg::from(("--foo", "bar"));
694
695 let valid = "\"--foo=bar\"";
697 let result: Result<Arg, _> = serde_json::from_str(valid);
698 assert_eq!(result.unwrap(), expected);
699
700 let valid = "\"--foo bar\"";
702 let result: Result<Arg, _> = serde_json::from_str(valid);
703 assert_eq!(result.unwrap(), expected);
704
705 let expected = Arg::from(("--foo", "bar=baz"));
707 let valid = "\"--foo=bar=baz\"";
708 let result: Result<Arg, _> = serde_json::from_str(valid);
709 assert_eq!(result.unwrap(), expected);
710 }
711
712 #[test]
713 fn test_arg_array_valid_input() {
714 let expected = Arg::from(("--foo", vec!["bar", "baz"]));
715
716 let valid = "\"--foo=[bar,baz]\"";
718 let result: Result<Arg, _> = serde_json::from_str(valid);
719 assert_eq!(result.unwrap(), expected);
720
721 let valid = "\"--foo [bar,baz]\"";
723 let result: Result<Arg, _> = serde_json::from_str(valid);
724 assert_eq!(result.unwrap(), expected);
725
726 let valid = "\"--foo [bar , baz]\"";
728 let result: Result<Arg, _> = serde_json::from_str(valid);
729 assert_eq!(result.unwrap(), expected);
730
731 let expected = Arg::from(("--foo", Vec::<&str>::new()));
733 let valid = "\"--foo []\"";
734 let result: Result<Arg, _> = serde_json::from_str(valid);
735 assert_eq!(result.unwrap(), expected);
736 }
737
738 #[test]
739 fn test_arg_positional_input() {
740 let input = "\"--foo[bar]\"";
744 let result: Result<Arg, _> = serde_json::from_str(input);
745 assert_eq!(result.unwrap(), Arg::Positional("--foo[bar]".to_string()));
746
747 let input = "\"--foo=bar baz\"";
749 let result: Result<Arg, _> = serde_json::from_str(input);
750 assert_eq!(
751 result.unwrap(),
752 Arg::Positional("--foo=bar baz".to_string())
753 );
754 }
755
756 #[test]
757 fn test_arg_positional_valid_input() {
758 let expected = Arg::Positional("scripts/assign-cores.sh".to_string());
760 let valid = "\"scripts/assign-cores.sh\"";
761 let result: Result<Arg, _> = serde_json::from_str(valid);
762 assert_eq!(result.unwrap(), expected);
763
764 let expected = Arg::Positional("ws://127.0.0.1:10000".to_string());
766 let valid = "\"ws://127.0.0.1:10000\"";
767 let result: Result<Arg, _> = serde_json::from_str(valid);
768 assert_eq!(result.unwrap(), expected);
769
770 let expected = Arg::Positional("42".to_string());
772 let valid = "\"42\"";
773 let result: Result<Arg, _> = serde_json::from_str(valid);
774 assert_eq!(result.unwrap(), expected);
775 }
776
777 #[test]
778 fn test_arg_positional_roundtrip() {
779 let arg = Arg::Positional("script.sh".to_string());
781 let serialized = serde_json::to_string(&arg).unwrap();
782 assert_eq!(serialized, "\"script.sh\"");
783 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
784 assert_eq!(arg, deserialized);
785 }
786
787 #[test]
788 fn test_arg_positional_to_vec() {
789 let arg = Arg::Positional("scripts/test.sh".to_string());
790 assert_eq!(arg.to_vec(), vec!["scripts/test.sh".to_string()]);
791 }
792
793 #[test]
794 fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
795 let got: Result<Chain, ConversionError> = "mychain".try_into();
796
797 assert_eq!(got.unwrap().as_str(), "mychain");
798 }
799
800 #[test]
801 fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
802 let got: Result<Image, ConversionError> = "myimage".try_into();
803
804 assert_eq!(got.unwrap().as_str(), "myimage");
805 }
806
807 #[test]
808 fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
809 let got: Result<Image, ConversionError> = "myimage:version".try_into();
810
811 assert_eq!(got.unwrap().as_str(), "myimage:version");
812 }
813
814 #[test]
815 fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
816 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
817
818 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
819 }
820
821 #[test]
822 fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
823 {
824 let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
825
826 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
827 }
828
829 #[test]
830 fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
831 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
832
833 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
834 }
835
836 #[test]
837 fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
838 let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
839
840 assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
841 }
842
843 #[test]
844 fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
845 let got: Result<Command, ConversionError> = "mycommand".try_into();
846
847 assert_eq!(got.unwrap().as_str(), "mycommand");
848 }
849
850 #[test]
851 fn converting_an_url_into_an_asset_location_should_succeeds() {
852 let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
853 let got: AssetLocation = url.clone().into();
854
855 assert!(matches!(got, AssetLocation::Url(value) if value == url));
856 }
857
858 #[test]
859 fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
860 let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
861 let got: AssetLocation = pathbuf.clone().into();
862
863 assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
864 }
865
866 #[test]
867 fn converting_a_str_into_an_url_asset_location_should_succeeds() {
868 let url = "https://mycloudstorage.com/path/to/my/file.tgz";
869 let got: AssetLocation = url.into();
870
871 assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
872 }
873
874 #[test]
875 fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
876 let filepath = "/tmp/path/to/my/file";
877 let got: AssetLocation = filepath.into();
878
879 assert!(matches!(
880 got,
881 AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
882 ));
883 }
884
885 #[test]
886 fn converting_a_str_into_an_flag_arg_should_succeeds() {
887 let got: Arg = "myflag".into();
888
889 assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
890 }
891
892 #[test]
893 fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
894 let got: Arg = ("name", "value").into();
895
896 assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
897 }
898
899 #[test]
900 fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
901 let got: Result<Chain, ConversionError> = "my chain".try_into();
902
903 assert!(matches!(
904 got.clone().unwrap_err(),
905 ConversionError::ContainsWhitespaces(_)
906 ));
907 assert_eq!(
908 got.unwrap_err().to_string(),
909 "'my chain' shouldn't contains whitespace"
910 );
911 }
912
913 #[test]
914 fn converting_an_empty_str_into_a_chain_should_fails() {
915 let got: Result<Chain, ConversionError> = "".try_into();
916
917 assert!(matches!(
918 got.clone().unwrap_err(),
919 ConversionError::CantBeEmpty
920 ));
921 assert_eq!(got.unwrap_err().to_string(), "can't be empty");
922 }
923
924 #[test]
925 fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
926 let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
927
928 assert!(matches!(
929 got.clone().unwrap_err(),
930 ConversionError::DoesntMatchRegex { value: _, regex: _ }
931 ));
932 assert_eq!(
933 got.unwrap_err().to_string(),
934 "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
935 );
936 }
937
938 #[test]
939 fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
940 let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
941
942 assert!(matches!(
943 got.clone().unwrap_err(),
944 ConversionError::DoesntMatchRegex { value: _, regex: _ }
945 ));
946 assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
947 }
948
949 #[test]
950 fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
951 let got: Result<Image, ConversionError> = "myrepository.com".try_into();
952
953 assert!(matches!(
954 got.clone().unwrap_err(),
955 ConversionError::DoesntMatchRegex { value: _, regex: _ }
956 ));
957 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
958 }
959
960 #[test]
961 fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
962 let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
963
964 assert!(matches!(
965 got.clone().unwrap_err(),
966 ConversionError::DoesntMatchRegex { value: _, regex: _ }
967 ));
968 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
969 }
970
971 #[test]
972 fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
973 let got: Result<Command, ConversionError> = "my command".try_into();
974
975 assert!(matches!(
976 got.clone().unwrap_err(),
977 ConversionError::ContainsWhitespaces(_)
978 ));
979 assert_eq!(
980 got.unwrap_err().to_string(),
981 "'my command' shouldn't contains whitespace"
982 );
983 }
984
985 #[test]
986 fn test_convert_to_json_overrides() {
987 let url: AssetLocation = "https://example.com/overrides.json".into();
988 assert!(matches!(
989 url.into(),
990 JsonOverrides::Location(AssetLocation::Url(_))
991 ));
992
993 let path: AssetLocation = "/path/to/overrides.json".into();
994 assert!(matches!(
995 path.into(),
996 JsonOverrides::Location(AssetLocation::FilePath(_))
997 ));
998
999 let inline = serde_json::json!({ "para_id": 2000});
1000 assert!(matches!(
1001 inline.into(),
1002 JsonOverrides::Json(serde_json::Value::Object(_))
1003 ));
1004 }
1005}