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#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_arg_flag_roundtrip() {
549 let arg = Arg::from("verbose");
550 let serialized = serde_json::to_string(&arg).unwrap();
551 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
552 assert_eq!(arg, deserialized);
553 }
554 #[test]
555 fn test_arg_option_roundtrip() {
556 let arg = Arg::from(("mode", "fast"));
557 let serialized = serde_json::to_string(&arg).unwrap();
558 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
559 assert_eq!(arg, deserialized);
560 }
561
562 #[test]
563 fn test_arg_array_roundtrip() {
564 let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
565
566 let serialized = serde_json::to_string(&arg).unwrap();
567 println!("serialized = {serialized}");
568 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
569 assert_eq!(arg, deserialized);
570 }
571
572 #[test]
573 fn test_arg_option_valid_input() {
574 let expected = Arg::from(("--foo", "bar"));
575
576 let valid = "\"--foo=bar\"";
578 let result: Result<Arg, _> = serde_json::from_str(valid);
579 assert_eq!(result.unwrap(), expected);
580
581 let valid = "\"--foo bar\"";
583 let result: Result<Arg, _> = serde_json::from_str(valid);
584 assert_eq!(result.unwrap(), expected);
585
586 let expected = Arg::from(("--foo", "bar=baz"));
588 let valid = "\"--foo=bar=baz\"";
589 let result: Result<Arg, _> = serde_json::from_str(valid);
590 assert_eq!(result.unwrap(), expected);
591 }
592
593 #[test]
594 fn test_arg_array_valid_input() {
595 let expected = Arg::from(("--foo", vec!["bar", "baz"]));
596
597 let valid = "\"--foo=[bar,baz]\"";
599 let result: Result<Arg, _> = serde_json::from_str(valid);
600 assert_eq!(result.unwrap(), expected);
601
602 let valid = "\"--foo [bar,baz]\"";
604 let result: Result<Arg, _> = serde_json::from_str(valid);
605 assert_eq!(result.unwrap(), expected);
606
607 let valid = "\"--foo [bar , baz]\"";
609 let result: Result<Arg, _> = serde_json::from_str(valid);
610 assert_eq!(result.unwrap(), expected);
611
612 let expected = Arg::from(("--foo", Vec::<&str>::new()));
614 let valid = "\"--foo []\"";
615 let result: Result<Arg, _> = serde_json::from_str(valid);
616 assert_eq!(result.unwrap(), expected);
617 }
618
619 #[test]
620 fn test_arg_invalid_input() {
621 let invalid = "\"--foo[bar]\"";
623 let result: Result<Arg, _> = serde_json::from_str(invalid);
624 assert!(result.is_err());
625
626 let invalid = "\"--foo=bar baz\"";
628 let result: Result<Arg, _> = serde_json::from_str(invalid);
629 println!("result = {result:?}");
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
635 let got: Result<Chain, ConversionError> = "mychain".try_into();
636
637 assert_eq!(got.unwrap().as_str(), "mychain");
638 }
639
640 #[test]
641 fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
642 let got: Result<Image, ConversionError> = "myimage".try_into();
643
644 assert_eq!(got.unwrap().as_str(), "myimage");
645 }
646
647 #[test]
648 fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
649 let got: Result<Image, ConversionError> = "myimage:version".try_into();
650
651 assert_eq!(got.unwrap().as_str(), "myimage:version");
652 }
653
654 #[test]
655 fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
656 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
657
658 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
659 }
660
661 #[test]
662 fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
663 {
664 let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
665
666 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
667 }
668
669 #[test]
670 fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
671 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
672
673 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
674 }
675
676 #[test]
677 fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
678 let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
679
680 assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
681 }
682
683 #[test]
684 fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
685 let got: Result<Command, ConversionError> = "mycommand".try_into();
686
687 assert_eq!(got.unwrap().as_str(), "mycommand");
688 }
689
690 #[test]
691 fn converting_an_url_into_an_asset_location_should_succeeds() {
692 let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
693 let got: AssetLocation = url.clone().into();
694
695 assert!(matches!(got, AssetLocation::Url(value) if value == url));
696 }
697
698 #[test]
699 fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
700 let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
701 let got: AssetLocation = pathbuf.clone().into();
702
703 assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
704 }
705
706 #[test]
707 fn converting_a_str_into_an_url_asset_location_should_succeeds() {
708 let url = "https://mycloudstorage.com/path/to/my/file.tgz";
709 let got: AssetLocation = url.into();
710
711 assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
712 }
713
714 #[test]
715 fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
716 let filepath = "/tmp/path/to/my/file";
717 let got: AssetLocation = filepath.into();
718
719 assert!(matches!(
720 got,
721 AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
722 ));
723 }
724
725 #[test]
726 fn converting_a_str_into_an_flag_arg_should_succeeds() {
727 let got: Arg = "myflag".into();
728
729 assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
730 }
731
732 #[test]
733 fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
734 let got: Arg = ("name", "value").into();
735
736 assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
737 }
738
739 #[test]
740 fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
741 let got: Result<Chain, ConversionError> = "my chain".try_into();
742
743 assert!(matches!(
744 got.clone().unwrap_err(),
745 ConversionError::ContainsWhitespaces(_)
746 ));
747 assert_eq!(
748 got.unwrap_err().to_string(),
749 "'my chain' shouldn't contains whitespace"
750 );
751 }
752
753 #[test]
754 fn converting_an_empty_str_into_a_chain_should_fails() {
755 let got: Result<Chain, ConversionError> = "".try_into();
756
757 assert!(matches!(
758 got.clone().unwrap_err(),
759 ConversionError::CantBeEmpty
760 ));
761 assert_eq!(got.unwrap_err().to_string(), "can't be empty");
762 }
763
764 #[test]
765 fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
766 let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
767
768 assert!(matches!(
769 got.clone().unwrap_err(),
770 ConversionError::DoesntMatchRegex { value: _, regex: _ }
771 ));
772 assert_eq!(
773 got.unwrap_err().to_string(),
774 "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
775 );
776 }
777
778 #[test]
779 fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
780 let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
781
782 assert!(matches!(
783 got.clone().unwrap_err(),
784 ConversionError::DoesntMatchRegex { value: _, regex: _ }
785 ));
786 assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
787 }
788
789 #[test]
790 fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
791 let got: Result<Image, ConversionError> = "myrepository.com".try_into();
792
793 assert!(matches!(
794 got.clone().unwrap_err(),
795 ConversionError::DoesntMatchRegex { value: _, regex: _ }
796 ));
797 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
798 }
799
800 #[test]
801 fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
802 let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
803
804 assert!(matches!(
805 got.clone().unwrap_err(),
806 ConversionError::DoesntMatchRegex { value: _, regex: _ }
807 ));
808 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
809 }
810
811 #[test]
812 fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
813 let got: Result<Command, ConversionError> = "my command".try_into();
814
815 assert!(matches!(
816 got.clone().unwrap_err(),
817 ConversionError::ContainsWhitespaces(_)
818 ));
819 assert_eq!(
820 got.unwrap_err().to_string(),
821 "'my command' shouldn't contains whitespace"
822 );
823 }
824}