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
21pub type Duration = u32;
23
24pub type Port = u16;
26
27pub type ParaId = u32;
29
30#[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 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#[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#[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#[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#[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#[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#[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 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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
548#[serde(untagged)]
549pub enum JsonOverrides {
550 Location(AssetLocation),
552 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 let valid = "\"--foo=bar\"";
636 let result: Result<Arg, _> = serde_json::from_str(valid);
637 assert_eq!(result.unwrap(), expected);
638
639 let valid = "\"--foo bar\"";
641 let result: Result<Arg, _> = serde_json::from_str(valid);
642 assert_eq!(result.unwrap(), expected);
643
644 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 let valid = "\"--foo=[bar,baz]\"";
657 let result: Result<Arg, _> = serde_json::from_str(valid);
658 assert_eq!(result.unwrap(), expected);
659
660 let valid = "\"--foo [bar,baz]\"";
662 let result: Result<Arg, _> = serde_json::from_str(valid);
663 assert_eq!(result.unwrap(), expected);
664
665 let valid = "\"--foo [bar , baz]\"";
667 let result: Result<Arg, _> = serde_json::from_str(valid);
668 assert_eq!(result.unwrap(), expected);
669
670 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 let invalid = "\"--foo[bar]\"";
681 let result: Result<Arg, _> = serde_json::from_str(invalid);
682 assert!(result.is_err());
683
684 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}