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, Eq, Hash)]
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 pub fn extract_name(&self) -> String {
362 match self {
363 AssetLocation::Url(url) => {
364 if let Some(mut segment) = url.path_segments() {
365 let last = segment.next_back().unwrap_or(url.as_str());
366 last.to_string()
367 } else {
368 url.as_str().to_string()
369 }
370 },
371 AssetLocation::FilePath(path_buf) => {
372 let name = path_buf.file_name().unwrap_or(path_buf.as_os_str());
373 name.to_str()
374 .expect("file path should be valid")
375 .to_string()
376 },
377 }
378 }
379}
380
381impl Serialize for AssetLocation {
382 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
383 where
384 S: serde::Serializer,
385 {
386 serializer.serialize_str(&self.to_string())
387 }
388}
389
390struct AssetLocationVisitor;
391
392impl de::Visitor<'_> for AssetLocationVisitor {
393 type Value = AssetLocation;
394
395 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
396 formatter.write_str("a string")
397 }
398
399 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
400 where
401 E: de::Error,
402 {
403 Ok(AssetLocation::from(v))
404 }
405}
406
407impl<'de> Deserialize<'de> for AssetLocation {
408 fn deserialize<D>(deserializer: D) -> Result<AssetLocation, D::Error>
409 where
410 D: Deserializer<'de>,
411 {
412 deserializer.deserialize_any(AssetLocationVisitor)
413 }
414}
415
416#[derive(Debug, Clone, PartialEq)]
435pub enum Arg {
436 Flag(String),
437 Option(String, String),
438 Array(String, Vec<String>),
439 Positional(String),
440}
441
442impl From<&str> for Arg {
443 fn from(flag: &str) -> Self {
444 Self::Flag(flag.to_owned())
445 }
446}
447
448impl From<(&str, &str)> for Arg {
449 fn from((option, value): (&str, &str)) -> Self {
450 Self::Option(option.to_owned(), value.to_owned())
451 }
452}
453
454impl<T> From<(&str, &[T])> for Arg
455where
456 T: AsRef<str> + Clone,
457{
458 fn from((option, values): (&str, &[T])) -> Self {
459 Self::Array(
460 option.to_owned(),
461 values.iter().map(|v| v.as_ref().to_string()).collect(),
462 )
463 }
464}
465
466impl<T> From<(&str, Vec<T>)> for Arg
467where
468 T: AsRef<str>,
469{
470 fn from((option, values): (&str, Vec<T>)) -> Self {
471 Self::Array(
472 option.to_owned(),
473 values.into_iter().map(|v| v.as_ref().to_string()).collect(),
474 )
475 }
476}
477
478impl Arg {
479 pub fn to_vec(&self) -> Vec<String> {
481 match self {
482 Arg::Flag(arg) => vec![arg.to_string()],
483 Arg::Option(k, v) => vec![k.to_string(), v.to_string()],
484 Arg::Array(k, items) => [
485 vec![k.to_string()],
486 items.iter().map(|x| x.to_string()).collect::<Vec<String>>(),
487 ]
488 .concat(),
489 Arg::Positional(value) => vec![value.to_string()],
490 }
491 }
492}
493impl Serialize for Arg {
494 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
495 where
496 S: serde::Serializer,
497 {
498 match self {
499 Arg::Flag(value) => serializer.serialize_str(value),
500 Arg::Option(option, value) => serializer.serialize_str(&format!("{option}={value}")),
501 Arg::Array(option, values) => {
502 serializer.serialize_str(&format!("{}=[{}]", option, values.join(",")))
503 },
504 Arg::Positional(value) => serializer.serialize_str(value),
505 }
506 }
507}
508
509struct ArgVisitor;
510
511impl de::Visitor<'_> for ArgVisitor {
512 type Value = Arg;
513
514 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
515 formatter.write_str("a string")
516 }
517
518 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
519 where
520 E: de::Error,
521 {
522 if v.starts_with("-l") || v.starts_with("-log") {
525 return Ok(Arg::Flag(v.to_string()));
526 }
527 if v.starts_with("-:") {
529 return Ok(Arg::Flag(v.to_string()));
530 }
531
532 let re = Regex::new("^(?<name_prefix>(?<prefix>-{1,2})?(?<name>[a-zA-Z]+(-[a-zA-Z]+)*))((?<separator>=| )(?<value>\\[[^\\]]*\\]|[^ ]+))?$").unwrap();
533
534 let captures = re.captures(v);
535 if let Some(captures) = captures {
536 if let Some(value) = captures.name("value") {
537 let name_prefix = captures
538 .name("name_prefix")
539 .expect("BUG: name_prefix capture group missing")
540 .as_str()
541 .to_string();
542
543 let val = value.as_str();
544 if val.starts_with('[') && val.ends_with(']') {
545 let inner = &val[1..val.len() - 1];
547 let items: Vec<String> = inner
548 .split(',')
549 .map(|s| s.trim().to_string())
550 .filter(|s| !s.is_empty())
551 .collect();
552 return Ok(Arg::Array(name_prefix, items));
553 } else {
554 return Ok(Arg::Option(name_prefix, val.to_string()));
555 }
556 }
557 if let Some(name_prefix) = captures.name("name_prefix") {
558 return Ok(Arg::Flag(name_prefix.as_str().to_string()));
559 }
560 }
561
562 Ok(Arg::Positional(v.to_string()))
564 }
565}
566
567impl<'de> Deserialize<'de> for Arg {
568 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
569 where
570 D: Deserializer<'de>,
571 {
572 deserializer.deserialize_any(ArgVisitor)
573 }
574}
575
576#[derive(Debug, Default, Clone)]
577pub struct ValidationContext {
578 pub used_ports: Vec<Port>,
579 pub used_nodes_names: HashSet<String>,
580 pub used_para_ids: HashMap<ParaId, u8>,
582}
583
584#[derive(Default, Debug, Clone, PartialEq, Deserialize)]
585pub struct ChainDefaultContext {
586 pub(crate) default_command: Option<Command>,
587 pub(crate) default_image: Option<Image>,
588 pub(crate) default_resources: Option<Resources>,
589 pub(crate) default_db_snapshot: Option<AssetLocation>,
590 #[serde(default)]
591 pub(crate) default_args: Vec<Arg>,
592}
593
594#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
597pub struct ChainSpecRuntime {
598 pub location: AssetLocation,
599 pub preset: Option<String>,
600}
601
602impl ChainSpecRuntime {
603 pub fn new(location: AssetLocation) -> Self {
604 ChainSpecRuntime {
605 location,
606 preset: None,
607 }
608 }
609
610 pub fn with_preset(location: AssetLocation, preset: impl Into<String>) -> Self {
611 ChainSpecRuntime {
612 location,
613 preset: Some(preset.into()),
614 }
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
623#[serde(untagged)]
624pub enum JsonOverrides {
625 Location(AssetLocation),
627 Json(serde_json::Value),
629}
630
631impl From<AssetLocation> for JsonOverrides {
632 fn from(value: AssetLocation) -> Self {
633 Self::Location(value)
634 }
635}
636
637impl From<serde_json::Value> for JsonOverrides {
638 fn from(value: serde_json::Value) -> Self {
639 Self::Json(value)
640 }
641}
642
643impl From<&str> for JsonOverrides {
644 fn from(value: &str) -> Self {
645 Self::Location(AssetLocation::from(value))
646 }
647}
648
649impl Display for JsonOverrides {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 match self {
652 JsonOverrides::Location(location) => write!(f, "{location}"),
653 JsonOverrides::Json(json) => write!(f, "{json}"),
654 }
655 }
656}
657
658impl JsonOverrides {
659 pub async fn get(&self) -> Result<serde_json::Value, anyhow::Error> {
660 let contents = match self {
661 Self::Location(location) => serde_json::from_slice(&location.get_asset().await?)
662 .map_err(|err| anyhow!("Error converting asset to json {location} - {err}")),
663 Self::Json(json) => Ok(json.clone()),
664 };
665
666 contents
667 }
668}
669
670#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
689#[serde(rename_all = "lowercase")]
690pub enum JamProtocolParameterType {
691 Full,
692 Mini,
693 Tiny,
694}
695
696impl TryFrom<&str> for JamProtocolParameterType {
697 type Error = ConversionError;
698
699 fn try_from(value: &str) -> Result<Self, Self::Error> {
700 if value.contains(char::is_whitespace) {
701 return Err(ConversionError::ContainsWhitespaces(value.to_string()));
702 }
703
704 if value.is_empty() {
705 return Err(ConversionError::CantBeEmpty);
706 }
707
708 let protocol_type = match value.to_ascii_lowercase().as_str() {
709 "full" => JamProtocolParameterType::Full,
710 "mini" => JamProtocolParameterType::Mini,
711 "tiny" => JamProtocolParameterType::Tiny,
712 _ => {
713 return Err(ConversionError::DoesntMatchRegex {
714 value: value.to_string(),
715 regex: "full|mini|tiny".to_string(),
716 });
717 },
718 };
719
720 Ok(protocol_type)
721 }
722}
723
724impl JamProtocolParameterType {
725 pub fn as_str(&self) -> &'static str {
726 match self {
727 JamProtocolParameterType::Full => "full",
728 JamProtocolParameterType::Mini => "mini",
729 JamProtocolParameterType::Tiny => "tiny",
730 }
731 }
732
733 pub fn validator_count(&self) -> usize {
734 match self {
735 JamProtocolParameterType::Full => 1023,
736 JamProtocolParameterType::Mini => 78,
737 JamProtocolParameterType::Tiny => 6,
738 }
739 }
740}
741
742#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
743#[serde(rename_all = "lowercase")]
744pub enum JamNodeMode {
745 #[default]
746 Validator,
747 Ordinary,
748 Proxy,
749}
750
751impl TryFrom<&str> for JamNodeMode {
752 type Error = ConversionError;
753
754 fn try_from(value: &str) -> Result<Self, Self::Error> {
755 if value.contains(char::is_whitespace) {
756 return Err(ConversionError::ContainsWhitespaces(value.to_string()));
757 }
758
759 if value.is_empty() {
760 return Err(ConversionError::CantBeEmpty);
761 }
762
763 let mode = match value.to_ascii_lowercase().as_str() {
764 "validator" => JamNodeMode::Validator,
765 "ordinary" => JamNodeMode::Ordinary,
766 "proxy" => JamNodeMode::Proxy,
767 _ => {
768 return Err(ConversionError::DoesntMatchRegex {
769 value: value.to_string(),
770 regex: "validator|ordinary|proxy".to_string(),
771 });
772 },
773 };
774
775 Ok(mode)
776 }
777}
778
779impl JamNodeMode {
780 pub fn as_str(&self) -> &'static str {
781 match self {
782 JamNodeMode::Validator => "validator",
783 JamNodeMode::Ordinary => "ordinary",
784 JamNodeMode::Proxy => "proxy",
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792
793 #[test]
794 fn test_arg_flag_roundtrip() {
795 let arg = Arg::from("verbose");
796 let serialized = serde_json::to_string(&arg).unwrap();
797 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
798 assert_eq!(arg, deserialized);
799 }
800
801 #[test]
802 fn test_urls_as_arg() {
803 let arg = Arg::from("ws://127.0.0.1:10000");
804 assert_eq!(Arg::Flag(String::from("ws://127.0.0.1:10000")), arg);
805 }
806 #[test]
807 fn test_script_as_arg() {
808 let arg = Arg::from("scripts/assign-cores.sh");
809 assert_eq!(Arg::Flag(String::from("scripts/assign-cores.sh")), arg);
810 }
811
812 #[test]
813 fn test_arg_option_roundtrip() {
814 let arg = Arg::from(("mode", "fast"));
815 let serialized = serde_json::to_string(&arg).unwrap();
816 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
817 assert_eq!(arg, deserialized);
818 }
819
820 #[test]
821 fn test_arg_array_roundtrip() {
822 let arg = Arg::from(("items", ["a", "b", "c"].as_slice()));
823
824 let serialized = serde_json::to_string(&arg).unwrap();
825 println!("serialized = {serialized}");
826 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
827 assert_eq!(arg, deserialized);
828 }
829
830 #[test]
831 fn test_arg_option_valid_input() {
832 let expected = Arg::from(("--foo", "bar"));
833
834 let valid = "\"--foo=bar\"";
836 let result: Result<Arg, _> = serde_json::from_str(valid);
837 assert_eq!(result.unwrap(), expected);
838
839 let valid = "\"--foo bar\"";
841 let result: Result<Arg, _> = serde_json::from_str(valid);
842 assert_eq!(result.unwrap(), expected);
843
844 let expected = Arg::from(("--foo", "bar=baz"));
846 let valid = "\"--foo=bar=baz\"";
847 let result: Result<Arg, _> = serde_json::from_str(valid);
848 assert_eq!(result.unwrap(), expected);
849 }
850
851 #[test]
852 fn test_arg_array_valid_input() {
853 let expected = Arg::from(("--foo", vec!["bar", "baz"]));
854
855 let valid = "\"--foo=[bar,baz]\"";
857 let result: Result<Arg, _> = serde_json::from_str(valid);
858 assert_eq!(result.unwrap(), expected);
859
860 let valid = "\"--foo [bar,baz]\"";
862 let result: Result<Arg, _> = serde_json::from_str(valid);
863 assert_eq!(result.unwrap(), expected);
864
865 let valid = "\"--foo [bar , baz]\"";
867 let result: Result<Arg, _> = serde_json::from_str(valid);
868 assert_eq!(result.unwrap(), expected);
869
870 let expected = Arg::from(("--foo", Vec::<&str>::new()));
872 let valid = "\"--foo []\"";
873 let result: Result<Arg, _> = serde_json::from_str(valid);
874 assert_eq!(result.unwrap(), expected);
875 }
876
877 #[test]
878 fn test_arg_positional_input() {
879 let input = "\"--foo[bar]\"";
883 let result: Result<Arg, _> = serde_json::from_str(input);
884 assert_eq!(result.unwrap(), Arg::Positional("--foo[bar]".to_string()));
885
886 let input = "\"--foo=bar baz\"";
888 let result: Result<Arg, _> = serde_json::from_str(input);
889 assert_eq!(
890 result.unwrap(),
891 Arg::Positional("--foo=bar baz".to_string())
892 );
893 }
894
895 #[test]
896 fn test_arg_positional_valid_input() {
897 let expected = Arg::Positional("scripts/assign-cores.sh".to_string());
899 let valid = "\"scripts/assign-cores.sh\"";
900 let result: Result<Arg, _> = serde_json::from_str(valid);
901 assert_eq!(result.unwrap(), expected);
902
903 let expected = Arg::Positional("ws://127.0.0.1:10000".to_string());
905 let valid = "\"ws://127.0.0.1:10000\"";
906 let result: Result<Arg, _> = serde_json::from_str(valid);
907 assert_eq!(result.unwrap(), expected);
908
909 let expected = Arg::Positional("42".to_string());
911 let valid = "\"42\"";
912 let result: Result<Arg, _> = serde_json::from_str(valid);
913 assert_eq!(result.unwrap(), expected);
914 }
915
916 #[test]
917 fn test_arg_positional_roundtrip() {
918 let arg = Arg::Positional("script.sh".to_string());
920 let serialized = serde_json::to_string(&arg).unwrap();
921 assert_eq!(serialized, "\"script.sh\"");
922 let deserialized: Arg = serde_json::from_str(&serialized).unwrap();
923 assert_eq!(arg, deserialized);
924 }
925
926 #[test]
927 fn test_arg_positional_to_vec() {
928 let arg = Arg::Positional("scripts/test.sh".to_string());
929 assert_eq!(arg.to_vec(), vec!["scripts/test.sh".to_string()]);
930 }
931
932 #[test]
933 fn converting_a_str_without_whitespaces_into_a_chain_should_succeeds() {
934 let got: Result<Chain, ConversionError> = "mychain".try_into();
935
936 assert_eq!(got.unwrap().as_str(), "mychain");
937 }
938
939 #[test]
940 fn converting_a_str_containing_tag_name_into_an_image_should_succeeds() {
941 let got: Result<Image, ConversionError> = "myimage".try_into();
942
943 assert_eq!(got.unwrap().as_str(), "myimage");
944 }
945
946 #[test]
947 fn converting_a_str_containing_tag_name_and_tag_version_into_an_image_should_succeeds() {
948 let got: Result<Image, ConversionError> = "myimage:version".try_into();
949
950 assert_eq!(got.unwrap().as_str(), "myimage:version");
951 }
952
953 #[test]
954 fn converting_a_str_containing_hostname_and_tag_name_into_an_image_should_succeeds() {
955 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
956
957 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
958 }
959
960 #[test]
961 fn converting_a_str_containing_hostname_tag_name_and_tag_version_into_an_image_should_succeeds()
962 {
963 let got: Result<Image, ConversionError> = "myrepository.com/myimage:version".try_into();
964
965 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage:version");
966 }
967
968 #[test]
969 fn converting_a_str_containing_ip_and_tag_name_into_an_image_should_succeeds() {
970 let got: Result<Image, ConversionError> = "myrepository.com/myimage".try_into();
971
972 assert_eq!(got.unwrap().as_str(), "myrepository.com/myimage");
973 }
974
975 #[test]
976 fn converting_a_str_containing_ip_tag_name_and_tag_version_into_an_image_should_succeeds() {
977 let got: Result<Image, ConversionError> = "127.0.0.1/myimage:version".try_into();
978
979 assert_eq!(got.unwrap().as_str(), "127.0.0.1/myimage:version");
980 }
981
982 #[test]
983 fn converting_a_str_without_whitespaces_into_a_command_should_succeeds() {
984 let got: Result<Command, ConversionError> = "mycommand".try_into();
985
986 assert_eq!(got.unwrap().as_str(), "mycommand");
987 }
988
989 #[test]
990 fn converting_an_url_into_an_asset_location_should_succeeds() {
991 let url = Url::from_str("https://mycloudstorage.com/path/to/my/file.tgz").unwrap();
992 let got: AssetLocation = url.clone().into();
993
994 assert!(matches!(got, AssetLocation::Url(value) if value == url));
995 }
996
997 #[test]
998 fn converting_a_pathbuf_into_an_asset_location_should_succeeds() {
999 let pathbuf = PathBuf::from_str("/tmp/path/to/my/file").unwrap();
1000 let got: AssetLocation = pathbuf.clone().into();
1001
1002 assert!(matches!(got, AssetLocation::FilePath(value) if value == pathbuf));
1003 }
1004
1005 #[test]
1006 fn converting_a_str_into_an_url_asset_location_should_succeeds() {
1007 let url = "https://mycloudstorage.com/path/to/my/file.tgz";
1008 let got: AssetLocation = url.into();
1009
1010 assert!(matches!(got, AssetLocation::Url(value) if value == Url::from_str(url).unwrap()));
1011 }
1012
1013 #[test]
1014 fn converting_a_str_into_an_filepath_asset_location_should_succeeds() {
1015 let filepath = "/tmp/path/to/my/file";
1016 let got: AssetLocation = filepath.into();
1017
1018 assert!(matches!(
1019 got,
1020 AssetLocation::FilePath(value) if value == PathBuf::from_str(filepath).unwrap()
1021 ));
1022 }
1023
1024 #[test]
1025 fn converting_a_str_into_an_flag_arg_should_succeeds() {
1026 let got: Arg = "myflag".into();
1027
1028 assert!(matches!(got, Arg::Flag(flag) if flag == "myflag"));
1029 }
1030
1031 #[test]
1032 fn converting_a_str_tuple_into_an_option_arg_should_succeeds() {
1033 let got: Arg = ("name", "value").into();
1034
1035 assert!(matches!(got, Arg::Option(name, value) if name == "name" && value == "value"));
1036 }
1037
1038 #[test]
1039 fn converting_a_str_with_whitespaces_into_a_chain_should_fails() {
1040 let got: Result<Chain, ConversionError> = "my chain".try_into();
1041
1042 assert!(matches!(
1043 got.clone().unwrap_err(),
1044 ConversionError::ContainsWhitespaces(_)
1045 ));
1046 assert_eq!(
1047 got.unwrap_err().to_string(),
1048 "'my chain' shouldn't contains whitespace"
1049 );
1050 }
1051
1052 #[test]
1053 fn converting_an_empty_str_into_a_chain_should_fails() {
1054 let got: Result<Chain, ConversionError> = "".try_into();
1055
1056 assert!(matches!(
1057 got.clone().unwrap_err(),
1058 ConversionError::CantBeEmpty
1059 ));
1060 assert_eq!(got.unwrap_err().to_string(), "can't be empty");
1061 }
1062
1063 #[test]
1064 fn converting_a_str_containing_only_ip_into_an_image_should_fails() {
1065 let got: Result<Image, ConversionError> = "127.0.0.1".try_into();
1066
1067 assert!(matches!(
1068 got.clone().unwrap_err(),
1069 ConversionError::DoesntMatchRegex { value: _, regex: _ }
1070 ));
1071 assert_eq!(
1072 got.unwrap_err().to_string(),
1073 "'127.0.0.1' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1074 );
1075 }
1076
1077 #[test]
1078 fn converting_a_str_containing_only_ip_and_tag_version_into_an_image_should_fails() {
1079 let got: Result<Image, ConversionError> = "127.0.0.1:version".try_into();
1080
1081 assert!(matches!(
1082 got.clone().unwrap_err(),
1083 ConversionError::DoesntMatchRegex { value: _, regex: _ }
1084 ));
1085 assert_eq!(got.unwrap_err().to_string(), "'127.0.0.1:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
1086 }
1087
1088 #[test]
1089 fn converting_a_str_containing_only_hostname_into_an_image_should_fails() {
1090 let got: Result<Image, ConversionError> = "myrepository.com".try_into();
1091
1092 assert!(matches!(
1093 got.clone().unwrap_err(),
1094 ConversionError::DoesntMatchRegex { value: _, regex: _ }
1095 ));
1096 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
1097 }
1098
1099 #[test]
1100 fn converting_a_str_containing_only_hostname_and_tag_version_into_an_image_should_fails() {
1101 let got: Result<Image, ConversionError> = "myrepository.com:version".try_into();
1102
1103 assert!(matches!(
1104 got.clone().unwrap_err(),
1105 ConversionError::DoesntMatchRegex { value: _, regex: _ }
1106 ));
1107 assert_eq!(got.unwrap_err().to_string(), "'myrepository.com:version' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'");
1108 }
1109
1110 #[test]
1111 fn converting_a_str_with_whitespaces_into_a_command_should_fails() {
1112 let got: Result<Command, ConversionError> = "my command".try_into();
1113
1114 assert!(matches!(
1115 got.clone().unwrap_err(),
1116 ConversionError::ContainsWhitespaces(_)
1117 ));
1118 assert_eq!(
1119 got.unwrap_err().to_string(),
1120 "'my command' shouldn't contains whitespace"
1121 );
1122 }
1123
1124 #[test]
1125 fn test_convert_to_json_overrides() {
1126 let url: AssetLocation = "https://example.com/overrides.json".into();
1127 assert!(matches!(
1128 url.into(),
1129 JsonOverrides::Location(AssetLocation::Url(_))
1130 ));
1131
1132 let path: AssetLocation = "/path/to/overrides.json".into();
1133 assert!(matches!(
1134 path.into(),
1135 JsonOverrides::Location(AssetLocation::FilePath(_))
1136 ));
1137
1138 let inline = serde_json::json!({ "para_id": 2000});
1139 assert!(matches!(
1140 inline.into(),
1141 JsonOverrides::Json(serde_json::Value::Object(_))
1142 ));
1143 }
1144}