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