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)]
410pub enum Arg {
411 Flag(String),
412 Option(String, String),
413 Array(String, Vec<String>),
414}
415
416impl From<&str> for Arg {
417 fn from(flag: &str) -> Self {
418 Self::Flag(flag.to_owned())
419 }
420}
421
422impl From<(&str, &str)> for Arg {
423 fn from((option, value): (&str, &str)) -> Self {
424 Self::Option(option.to_owned(), value.to_owned())
425 }
426}
427
428impl<T> From<(&str, &[T])> for Arg
429where
430 T: AsRef<str> + Clone,
431{
432 fn from((option, values): (&str, &[T])) -> Self {
433 Self::Array(
434 option.to_owned(),
435 values.iter().map(|v| v.as_ref().to_string()).collect(),
436 )
437 }
438}
439
440impl<T> From<(&str, Vec<T>)> for Arg
441where
442 T: AsRef<str>,
443{
444 fn from((option, values): (&str, Vec<T>)) -> Self {
445 Self::Array(
446 option.to_owned(),
447 values.into_iter().map(|v| v.as_ref().to_string()).collect(),
448 )
449 }
450}
451
452impl Serialize for Arg {
453 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
454 where
455 S: serde::Serializer,
456 {
457 match self {
458 Arg::Flag(value) => serializer.serialize_str(value),
459 Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
460 Arg::Array(option, values) => {
461 serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
462 },
463 }
464 }
465}
466
467struct ArgVisitor;
468
469impl de::Visitor<'_> for ArgVisitor {
470 type Value = Arg;
471
472 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
473 formatter.write_str("a string")
474 }
475
476 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
477 where
478 E: de::Error,
479 {
480 if v.starts_with("-l") || v.starts_with("-log") {
483 return Ok(Arg::Flag(v.to_string()));
484 }
485 if v.starts_with("-:") {
487 return Ok(Arg::Flag(v.to_string()));
488 }
489 let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
490
491 let captures = re.captures(v);
492 if let Some(captures) = captures {
493 if let Some(value) = captures.name("value") {
494 let name_prefix = captures
495 .name("name_prefix")
496 .expect("BUG: name_prefix capture group missing")
497 .as_str()
498 .to_string();
499
500 let val = value.as_str();
501 if val.starts_with('[') && val.ends_with(']') {
502 let inner = &val[1..val.len() - 1];
504 let items: Vec<String> = inner
505 .split(',')
506 .map(|s| s.trim().to_string())
507 .filter(|s| !s.is_empty())
508 .collect();
509 return Ok(Arg::Array(name_prefix, items));
510 } else {
511 return Ok(Arg::Option(name_prefix, val.to_string()));
512 }
513 }
514 if let Some(name_prefix) = captures.name("name_prefix") {
515 return Ok(Arg::Flag(name_prefix.as_str().to_string()));
516 }
517 }
518
519 Err(de::Error::custom(
520 "the provided argument is invalid and doesn't match Arg::Option, Arg::Flag or Arg::Array",
521 ))
522 }
523}
524
525impl<'de> Deserialize<'de> for Arg {
526 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
527 where
528 D: Deserializer<'de>,
529 {
530 deserializer.deserialize_any(ArgVisitor)
531 }
532}
533
534#[derive(Debug, Default, Clone)]
535pub struct ValidationContext {
536 pub used_ports: Vec<Port>,
537 pub used_nodes_names: HashSet<String>,
538 pub used_para_ids: HashMap<ParaId, u8>,
540}
541
542#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
543pub struct ChainDefaultContext {
544 pub(crate) default_command: Option<Command>,
545 pub(crate) default_image: Option<Image>,
546 pub(crate) default_resources: Option<Resources>,
547 pub(crate) default_db_snapshot: Option<AssetLocation>,
548 #[serde(default)]
549 pub(crate) default_args: Vec<Arg>,
550}
551
552#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
555pub struct ChainSpecRuntime {
556 pub location: AssetLocation,
557 pub preset: Option<String>,
558}
559
560impl ChainSpecRuntime {
561 pub fn new(location: AssetLocation) -> Self {
562 ChainSpecRuntime {
563 location,
564 preset: None,
565 }
566 }
567
568 pub fn with_preset(location: AssetLocation, preset: impl Into<String>) -> Self {
569 ChainSpecRuntime {
570 location,
571 preset: Some(preset.into()),
572 }
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
581#[serde(untagged)]
582pub enum JsonOverrides {
583 Location(AssetLocation),
585 Json(serde_json::Value),
587}
588
589impl From<AssetLocation> for JsonOverrides {
590 fn from(value: AssetLocation) -> Self {
591 Self::Location(value)
592 }
593}
594
595impl From<serde_json::Value> for JsonOverrides {
596 fn from(value: serde_json::Value) -> Self {
597 Self::Json(value)
598 }
599}
600
601impl From<&str> for JsonOverrides {
602 fn from(value: &str) -> Self {
603 Self::Location(AssetLocation::from(value))
604 }
605}
606
607impl Display for JsonOverrides {
608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609 match self {
610 JsonOverrides::Location(location) => write!(f, "{location}"),
611 JsonOverrides::Json(json) => write!(f, "{json}"),
612 }
613 }
614}
615
616impl JsonOverrides {
617 pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
618 let contents = match self {
619 Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
620 .map_err(|err| anyhow!("Error converting asset to json {location} - {err}")),
621 Self::Json(json) => Ok(json.clone()),
622 };
623
624 contents
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn test_arg_flag_roundtrip() {
634 let arg = Arg::from("verbose");
635 let serialized = serde_json::to_string(&arg).unwrap();
636 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
637 assert_eq!(arg, deserialized);
638 }
639 #[test]
640 fn test_arg_option_roundtrip() {
641 let arg = Arg::from(("mode", "fast"));
642 let serialized = serde_json::to_string(&arg).unwrap();
643 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
644 assert_eq!(arg, deserialized);
645 }
646
647 #[test]
648 fn test_arg_array_roundtrip() {
649 let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
650
651 let serialized = serde_json::to_string(&arg).unwrap();
652 println!("serialized = {serialized}");
653 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
654 assert_eq!(arg, deserialized);
655 }
656
657 #[test]
658 fn test_arg_option_valid_input() {
659 let expected = Arg::from(("--foo", "bar"));
660
661 let valid = "\"--foo=bar\"";
663 let result: Result<Arg, _> = serde_json::from_str(valid);
664 assert_eq!(result.unwrap(), expected);
665
666 let valid = "\"--foo bar\"";
668 let result: Result<Arg, _> = serde_json::from_str(valid);
669 assert_eq!(result.unwrap(), expected);
670
671 let expected = Arg::from(("--foo", "bar=baz"));
673 let valid = "\"--foo=bar=baz\"";
674 let result: Result<Arg, _> = serde_json::from_str(valid);
675 assert_eq!(result.unwrap(), expected);
676 }
677
678 #[test]
679 fn test_arg_array_valid_input() {
680 let expected = Arg::from(("--foo", vec!["bar", "baz"]));
681
682 let valid = "\"--foo=[bar,baz]\"";
684 let result: Result<Arg, _> = serde_json::from_str(valid);
685 assert_eq!(result.unwrap(), expected);
686
687 let valid = "\"--foo [bar,baz]\"";
689 let result: Result<Arg, _> = serde_json::from_str(valid);
690 assert_eq!(result.unwrap(), expected);
691
692 let valid = "\"--foo [bar , baz]\"";
694 let result: Result<Arg, _> = serde_json::from_str(valid);
695 assert_eq!(result.unwrap(), expected);
696
697 let expected = Arg::from(("--foo", Vec::<&str>::new()));
699 let valid = "\"--foo []\"";
700 let result: Result<Arg, _> = serde_json::from_str(valid);
701 assert_eq!(result.unwrap(), expected);
702 }
703
704 #[test]
705 fn test_arg_invalid_input() {
706 let invalid = "\"--foo[bar]\"";
708 let result: Result<Arg, _> = serde_json::from_str(invalid);
709 assert!(result.is_err());
710
711 let invalid = "\"--foo=bar baz\"";
713 let result: Result<Arg, _> = serde_json::from_str(invalid);
714 println!("result = {result:?}");
715 assert!(result.is_err());
716 }
717
718 #[test]
719 fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
720 let got: Result<Chain, ConversionError> = "mychain".try_into();
721
722 assert_eq!(got.unwrap().as_str(), "mychain");
723 }
724
725 #[test]
726 fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
727 let got: Result<Image, ConversionError> = "myimage".try_into();
728
729 assert_eq!(got.unwrap().as_str(), "myimage");
730 }
731
732 #[test]
733 fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
734 let got: Result<Image, ConversionError> = "myimage:version".try_into();
735
736 assert_eq!(got.unwrap().as_str(), "myimage:version");
737 }
738
739 #[test]
740 fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
741 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
742
743 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
744 }
745
746 #[test]
747 fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
748 {
749 let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
750
751 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
752 }
753
754 #[test]
755 fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
756 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
757
758 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
759 }
760
761 #[test]
762 fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
763 let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
764
765 assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
766 }
767
768 #[test]
769 fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
770 let got: Result<Command, ConversionError> = "mycommand".try_into();
771
772 assert_eq!(got.unwrap().as_str(), "mycommand");
773 }
774
775 #[test]
776 fn converting_an_url_into_an_asset_location_should_succeeds() {
777 let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
778 let got: AssetLocation = url.clone().into();
779
780 assert!(matches!(got, AssetLocation::Url(value) if value == url));
781 }
782
783 #[test]
784 fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
785 let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
786 let got: AssetLocation = pathbuf.clone().into();
787
788 assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
789 }
790
791 #[test]
792 fn converting_a_str_into_an_url_asset_location_should_succeeds() {
793 let url = "https://mycloudstorage.com/path/to/my/file.tgz";
794 let got: AssetLocation = url.into();
795
796 assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
797 }
798
799 #[test]
800 fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
801 let filepath = "/tmp/path/to/my/file";
802 let got: AssetLocation = filepath.into();
803
804 assert!(matches!(
805 got,
806 AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
807 ));
808 }
809
810 #[test]
811 fn converting_a_str_into_an_flag_arg_should_succeeds() {
812 let got: Arg = "myflag".into();
813
814 assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
815 }
816
817 #[test]
818 fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
819 let got: Arg = ("name", "value").into();
820
821 assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
822 }
823
824 #[test]
825 fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
826 let got: Result<Chain, ConversionError> = "my chain".try_into();
827
828 assert!(matches!(
829 got.clone().unwrap_err(),
830 ConversionError::ContainsWhitespaces(_)
831 ));
832 assert_eq!(
833 got.unwrap_err().to_string(),
834 "'my chain' shouldn't contains whitespace"
835 );
836 }
837
838 #[test]
839 fn converting_an_empty_str_into_a_chain_should_fails() {
840 let got: Result<Chain, ConversionError> = "".try_into();
841
842 assert!(matches!(
843 got.clone().unwrap_err(),
844 ConversionError::CantBeEmpty
845 ));
846 assert_eq!(got.unwrap_err().to_string(), "can't be empty");
847 }
848
849 #[test]
850 fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
851 let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
852
853 assert!(matches!(
854 got.clone().unwrap_err(),
855 ConversionError::DoesntMatchRegex { value: _, regex: _ }
856 ));
857 assert_eq!(
858 got.unwrap_err().to_string(),
859 "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
860 );
861 }
862
863 #[test]
864 fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
865 let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
866
867 assert!(matches!(
868 got.clone().unwrap_err(),
869 ConversionError::DoesntMatchRegex { value: _, regex: _ }
870 ));
871 assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
872 }
873
874 #[test]
875 fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
876 let got: Result<Image, ConversionError> = "myrepository.com".try_into();
877
878 assert!(matches!(
879 got.clone().unwrap_err(),
880 ConversionError::DoesntMatchRegex { value: _, regex: _ }
881 ));
882 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
883 }
884
885 #[test]
886 fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
887 let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
888
889 assert!(matches!(
890 got.clone().unwrap_err(),
891 ConversionError::DoesntMatchRegex { value: _, regex: _ }
892 ));
893 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
894 }
895
896 #[test]
897 fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
898 let got: Result<Command, ConversionError> = "my command".try_into();
899
900 assert!(matches!(
901 got.clone().unwrap_err(),
902 ConversionError::ContainsWhitespaces(_)
903 ));
904 assert_eq!(
905 got.unwrap_err().to_string(),
906 "'my command' shouldn't contains whitespace"
907 );
908 }
909
910 #[test]
911 fn test_convert_to_json_overrides() {
912 let url: AssetLocation = "https://example.com/overrides.json".into();
913 assert!(matches!(
914 url.into(),
915 JsonOverrides::Location(AssetLocation::Url(_))
916 ));
917
918 let path: AssetLocation = "/path/to/overrides.json".into();
919 assert!(matches!(
920 path.into(),
921 JsonOverrides::Location(AssetLocation::FilePath(_))
922 ));
923
924 let inline = serde_json::json!({ "para_id": 2000});
925 assert!(matches!(
926 inline.into(),
927 JsonOverrides::Json(serde_json::Value::Object(_))
928 ));
929 }
930}