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_arg_option_roundtrip() {
664 let arg = Arg::from(("mode", "fast"));
665 let serialized = serde_json::to_string(&arg).unwrap();
666 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
667 assert_eq!(arg, deserialized);
668 }
669
670 #[test]
671 fn test_arg_array_roundtrip() {
672 let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
673
674 let serialized = serde_json::to_string(&arg).unwrap();
675 println!("serialized = {serialized}");
676 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
677 assert_eq!(arg, deserialized);
678 }
679
680 #[test]
681 fn test_arg_option_valid_input() {
682 let expected = Arg::from(("--foo", "bar"));
683
684 let valid = "\"--foo=bar\"";
686 let result: Result<Arg, _> = serde_json::from_str(valid);
687 assert_eq!(result.unwrap(), expected);
688
689 let valid = "\"--foo bar\"";
691 let result: Result<Arg, _> = serde_json::from_str(valid);
692 assert_eq!(result.unwrap(), expected);
693
694 let expected = Arg::from(("--foo", "bar=baz"));
696 let valid = "\"--foo=bar=baz\"";
697 let result: Result<Arg, _> = serde_json::from_str(valid);
698 assert_eq!(result.unwrap(), expected);
699 }
700
701 #[test]
702 fn test_arg_array_valid_input() {
703 let expected = Arg::from(("--foo", vec!["bar", "baz"]));
704
705 let valid = "\"--foo=[bar,baz]\"";
707 let result: Result<Arg, _> = serde_json::from_str(valid);
708 assert_eq!(result.unwrap(), expected);
709
710 let valid = "\"--foo [bar,baz]\"";
712 let result: Result<Arg, _> = serde_json::from_str(valid);
713 assert_eq!(result.unwrap(), expected);
714
715 let valid = "\"--foo [bar , baz]\"";
717 let result: Result<Arg, _> = serde_json::from_str(valid);
718 assert_eq!(result.unwrap(), expected);
719
720 let expected = Arg::from(("--foo", Vec::<&str>::new()));
722 let valid = "\"--foo []\"";
723 let result: Result<Arg, _> = serde_json::from_str(valid);
724 assert_eq!(result.unwrap(), expected);
725 }
726
727 #[test]
728 fn test_arg_positional_input() {
729 let input = "\"--foo[bar]\"";
733 let result: Result<Arg, _> = serde_json::from_str(input);
734 assert_eq!(result.unwrap(), Arg::Positional("--foo[bar]".to_string()));
735
736 let input = "\"--foo=bar baz\"";
738 let result: Result<Arg, _> = serde_json::from_str(input);
739 assert_eq!(
740 result.unwrap(),
741 Arg::Positional("--foo=bar baz".to_string())
742 );
743 }
744
745 #[test]
746 fn test_arg_positional_valid_input() {
747 let expected = Arg::Positional("scripts/assign-cores.sh".to_string());
749 let valid = "\"scripts/assign-cores.sh\"";
750 let result: Result<Arg, _> = serde_json::from_str(valid);
751 assert_eq!(result.unwrap(), expected);
752
753 let expected = Arg::Positional("ws://127.0.0.1:10000".to_string());
755 let valid = "\"ws://127.0.0.1:10000\"";
756 let result: Result<Arg, _> = serde_json::from_str(valid);
757 assert_eq!(result.unwrap(), expected);
758
759 let expected = Arg::Positional("42".to_string());
761 let valid = "\"42\"";
762 let result: Result<Arg, _> = serde_json::from_str(valid);
763 assert_eq!(result.unwrap(), expected);
764 }
765
766 #[test]
767 fn test_arg_positional_roundtrip() {
768 let arg = Arg::Positional("script.sh".to_string());
770 let serialized = serde_json::to_string(&arg).unwrap();
771 assert_eq!(serialized, "\"script.sh\"");
772 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
773 assert_eq!(arg, deserialized);
774 }
775
776 #[test]
777 fn test_arg_positional_to_vec() {
778 let arg = Arg::Positional("scripts/test.sh".to_string());
779 assert_eq!(arg.to_vec(), vec!["scripts/test.sh".to_string()]);
780 }
781
782 #[test]
783 fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
784 let got: Result<Chain, ConversionError> = "mychain".try_into();
785
786 assert_eq!(got.unwrap().as_str(), "mychain");
787 }
788
789 #[test]
790 fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
791 let got: Result<Image, ConversionError> = "myimage".try_into();
792
793 assert_eq!(got.unwrap().as_str(), "myimage");
794 }
795
796 #[test]
797 fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
798 let got: Result<Image, ConversionError> = "myimage:version".try_into();
799
800 assert_eq!(got.unwrap().as_str(), "myimage:version");
801 }
802
803 #[test]
804 fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
805 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
806
807 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
808 }
809
810 #[test]
811 fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
812 {
813 let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
814
815 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
816 }
817
818 #[test]
819 fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
820 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
821
822 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
823 }
824
825 #[test]
826 fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
827 let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
828
829 assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
830 }
831
832 #[test]
833 fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
834 let got: Result<Command, ConversionError> = "mycommand".try_into();
835
836 assert_eq!(got.unwrap().as_str(), "mycommand");
837 }
838
839 #[test]
840 fn converting_an_url_into_an_asset_location_should_succeeds() {
841 let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
842 let got: AssetLocation = url.clone().into();
843
844 assert!(matches!(got, AssetLocation::Url(value) if value == url));
845 }
846
847 #[test]
848 fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
849 let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
850 let got: AssetLocation = pathbuf.clone().into();
851
852 assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
853 }
854
855 #[test]
856 fn converting_a_str_into_an_url_asset_location_should_succeeds() {
857 let url = "https://mycloudstorage.com/path/to/my/file.tgz";
858 let got: AssetLocation = url.into();
859
860 assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
861 }
862
863 #[test]
864 fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
865 let filepath = "/tmp/path/to/my/file";
866 let got: AssetLocation = filepath.into();
867
868 assert!(matches!(
869 got,
870 AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
871 ));
872 }
873
874 #[test]
875 fn converting_a_str_into_an_flag_arg_should_succeeds() {
876 let got: Arg = "myflag".into();
877
878 assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
879 }
880
881 #[test]
882 fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
883 let got: Arg = ("name", "value").into();
884
885 assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
886 }
887
888 #[test]
889 fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
890 let got: Result<Chain, ConversionError> = "my chain".try_into();
891
892 assert!(matches!(
893 got.clone().unwrap_err(),
894 ConversionError::ContainsWhitespaces(_)
895 ));
896 assert_eq!(
897 got.unwrap_err().to_string(),
898 "'my chain' shouldn't contains whitespace"
899 );
900 }
901
902 #[test]
903 fn converting_an_empty_str_into_a_chain_should_fails() {
904 let got: Result<Chain, ConversionError> = "".try_into();
905
906 assert!(matches!(
907 got.clone().unwrap_err(),
908 ConversionError::CantBeEmpty
909 ));
910 assert_eq!(got.unwrap_err().to_string(), "can't be empty");
911 }
912
913 #[test]
914 fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
915 let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
916
917 assert!(matches!(
918 got.clone().unwrap_err(),
919 ConversionError::DoesntMatchRegex { value: _, regex: _ }
920 ));
921 assert_eq!(
922 got.unwrap_err().to_string(),
923 "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
924 );
925 }
926
927 #[test]
928 fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
929 let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
930
931 assert!(matches!(
932 got.clone().unwrap_err(),
933 ConversionError::DoesntMatchRegex { value: _, regex: _ }
934 ));
935 assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
936 }
937
938 #[test]
939 fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
940 let got: Result<Image, ConversionError> = "myrepository.com".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(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
947 }
948
949 #[test]
950 fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
951 let got: Result<Image, ConversionError> = "myrepository.com:version".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:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
958 }
959
960 #[test]
961 fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
962 let got: Result<Command, ConversionError> = "my command".try_into();
963
964 assert!(matches!(
965 got.clone().unwrap_err(),
966 ConversionError::ContainsWhitespaces(_)
967 ));
968 assert_eq!(
969 got.unwrap_err().to_string(),
970 "'my command' shouldn't contains whitespace"
971 );
972 }
973
974 #[test]
975 fn test_convert_to_json_overrides() {
976 let url: AssetLocation = "https://example.com/overrides.json".into();
977 assert!(matches!(
978 url.into(),
979 JsonOverrides::Location(AssetLocation::Url(_))
980 ));
981
982 let path: AssetLocation = "/path/to/overrides.json".into();
983 assert!(matches!(
984 path.into(),
985 JsonOverrides::Location(AssetLocation::FilePath(_))
986 ));
987
988 let inline = serde_json::json!({ "para_id": 2000});
989 assert!(matches!(
990 inline.into(),
991 JsonOverrides::Json(serde_json::Value::Object(_))
992 ));
993 }
994}