1use std::collections::HashSet;
80use std::fmt::Debug;
81use std::future::Future;
82use std::hash::Hash;
83
84use chroma_error::ChromaError;
85use chroma_error::ErrorCodes;
86use serde::Deserialize;
87use serde::Serialize;
88use thiserror::Error;
89
90const MAX_NAME_LENGTH: usize = 32;
92
93#[derive(Clone, Debug, Eq, Error, PartialEq)]
95pub enum NameError {
96 #[error("name cannot be empty")]
98 Empty,
99 #[error("name exceeds maximum length of {MAX_NAME_LENGTH} characters: {0} characters")]
101 TooLong(usize),
102 #[error("name contains non-ASCII characters")]
104 NonAscii,
105}
106
107fn validate_name(name: &str) -> Result<(), NameError> {
109 if name.is_empty() {
110 return Err(NameError::Empty);
111 }
112 if !name.is_ascii() {
113 return Err(NameError::NonAscii);
114 }
115 if name.len() > MAX_NAME_LENGTH {
116 return Err(NameError::TooLong(name.len()));
117 }
118 Ok(())
119}
120
121fn find_duplicates<'a, T, K, F>(items: &'a [T], key_fn: F) -> Vec<K>
125where
126 K: 'a + Clone + Eq + Hash + Ord,
127 F: Fn(&'a T) -> &'a K,
128{
129 let mut seen = HashSet::new();
130 let mut duplicates: Vec<_> = items
131 .iter()
132 .filter_map(|item| {
133 let key = key_fn(item);
134 if !seen.insert(key) {
135 Some(key.clone())
136 } else {
137 None
138 }
139 })
140 .collect();
141 duplicates.sort();
142 duplicates.dedup();
143 duplicates
144}
145
146#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
160#[serde(transparent)]
161pub struct RegionName(String);
162
163impl RegionName {
164 pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
173 let name = name.into();
174 validate_name(&name)?;
175 Ok(Self(name))
176 }
177
178 pub fn as_str(&self) -> &str {
180 &self.0
181 }
182}
183
184impl<'de> Deserialize<'de> for RegionName {
185 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
186 where
187 D: serde::Deserializer<'de>,
188 {
189 let s = String::deserialize(deserializer)?;
190 RegionName::new(s).map_err(serde::de::Error::custom)
191 }
192}
193
194impl std::fmt::Display for RegionName {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 write!(f, "{}", self.0)
197 }
198}
199
200#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
214#[serde(transparent)]
215pub struct TopologyName(String);
216
217impl TopologyName {
218 pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
227 let name = name.into();
228 validate_name(&name)?;
229 Ok(Self(name))
230 }
231
232 pub fn as_str(&self) -> &str {
234 &self.0
235 }
236}
237
238impl<'de> Deserialize<'de> for TopologyName {
239 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240 where
241 D: serde::Deserializer<'de>,
242 {
243 let s = String::deserialize(deserializer)?;
244 TopologyName::new(s).map_err(serde::de::Error::custom)
245 }
246}
247
248impl std::fmt::Display for TopologyName {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 write!(f, "{}", self.0)
251 }
252}
253
254#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
272#[serde(bound(
273 serialize = "T: Clone + Debug + Serialize",
274 deserialize = "T: Clone + Debug + serde::de::DeserializeOwned"
275))]
276pub struct ProviderRegion<T: Clone + Debug> {
277 pub name: RegionName,
279 pub provider: String,
281 pub region: String,
283 pub config: T,
285}
286
287impl<T: Clone + Debug> ProviderRegion<T> {
288 pub fn new(
303 name: RegionName,
304 provider: impl Into<String>,
305 region: impl Into<String>,
306 config: T,
307 ) -> Self {
308 Self {
309 name,
310 provider: provider.into(),
311 region: region.into(),
312 config,
313 }
314 }
315
316 pub fn name(&self) -> &RegionName {
318 &self.name
319 }
320
321 pub fn provider(&self) -> &str {
323 &self.provider
324 }
325
326 pub fn region(&self) -> &str {
328 &self.region
329 }
330
331 pub fn config(&self) -> &T {
333 &self.config
334 }
335
336 pub fn cast<U, F>(self, f: F) -> ProviderRegion<U>
338 where
339 U: Clone + Debug,
340 F: FnOnce(T) -> U,
341 {
342 ProviderRegion {
343 name: self.name,
344 provider: self.provider,
345 region: self.region,
346 config: f(self.config),
347 }
348 }
349
350 pub fn try_cast<U, E, F>(self, f: F) -> Result<ProviderRegion<U>, E>
357 where
358 U: Clone + Debug,
359 F: FnOnce(T) -> Result<U, E>,
360 {
361 Ok(ProviderRegion {
362 name: self.name,
363 provider: self.provider,
364 region: self.region,
365 config: f(self.config)?,
366 })
367 }
368
369 pub async fn try_cast_async<U, E, F, R>(self, f: F) -> Result<ProviderRegion<U>, E>
376 where
377 U: Clone + Debug,
378 F: FnOnce(T) -> R,
379 R: Future<Output = Result<U, E>> + Send,
380 {
381 Ok(ProviderRegion {
382 name: self.name,
383 provider: self.provider,
384 region: self.region,
385 config: f(self.config).await?,
386 })
387 }
388}
389
390#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
409#[serde(bound(
410 serialize = "T: Clone + Debug + Serialize",
411 deserialize = "T: Clone + Debug + serde::de::DeserializeOwned"
412))]
413pub struct Topology<T: Clone + Debug> {
414 pub name: TopologyName,
416 regions: Vec<RegionName>,
418 pub config: T,
420}
421
422impl<T: Clone + Debug> Topology<T> {
423 pub fn new(name: TopologyName, regions: Vec<RegionName>, config: T) -> Self {
437 Self {
438 name,
439 regions,
440 config,
441 }
442 }
443
444 pub fn name(&self) -> &TopologyName {
446 &self.name
447 }
448
449 pub fn regions(&self) -> &[RegionName] {
451 &self.regions
452 }
453
454 pub fn config(&self) -> &T {
456 &self.config
457 }
458
459 pub fn cast<U, F>(self, f: F) -> Topology<U>
461 where
462 U: Clone + Debug,
463 F: FnOnce(T) -> U,
464 {
465 Topology {
466 name: self.name,
467 regions: self.regions,
468 config: f(self.config),
469 }
470 }
471
472 pub fn try_cast<U, E, F>(self, f: F) -> Result<Topology<U>, E>
478 where
479 U: Clone + Debug,
480 F: FnOnce(T) -> Result<U, E>,
481 {
482 Ok(Topology {
483 name: self.name,
484 regions: self.regions,
485 config: f(self.config)?,
486 })
487 }
488
489 pub async fn try_cast_async<U, E, F, R>(self, f: F) -> Result<Topology<U>, E>
496 where
497 U: Clone + Debug,
498 F: FnOnce(T) -> R,
499 R: Future<Output = Result<U, E>> + Send,
500 {
501 Ok(Topology {
502 name: self.name,
503 regions: self.regions,
504 config: f(self.config).await?,
505 })
506 }
507}
508
509#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
540#[serde(
541 into = "RawMultiCloudMultiRegionConfiguration<R, T>",
542 bound(serialize = "R: Clone + Debug + Serialize, T: Clone + Debug + Serialize")
543)]
544pub struct MultiCloudMultiRegionConfiguration<R: Clone + Debug, T: Clone + Debug> {
545 pub preferred: RegionName,
547 pub regions: Vec<ProviderRegion<R>>,
549 pub topologies: Vec<Topology<T>>,
551}
552
553impl<R: Clone + Debug, T: Clone + Debug> MultiCloudMultiRegionConfiguration<R, T> {
554 pub fn preferred_region(&self) -> Option<&ProviderRegion<R>> {
556 self.regions.iter().find(|pr| pr.name() == &self.preferred)
557 }
558
559 pub fn lookup_region(&self, name: &RegionName) -> Option<ProviderRegion<R>> {
561 self.regions.iter().find(|pr| pr.name() == name).cloned()
562 }
563
564 pub fn lookup_topology(
567 &self,
568 name: &TopologyName,
569 ) -> Option<(Vec<ProviderRegion<R>>, Topology<T>)> {
570 let t = self.topologies.iter().find(|t| &t.name == name).cloned()?;
571 let mut regions = vec![];
572 for r in t.regions.iter() {
573 regions.push(self.lookup_region(r)?)
574 }
575 Some((regions, t))
576 }
577}
578
579#[derive(Clone, Debug, Serialize, Deserialize)]
581#[serde(bound(
582 serialize = "R: Clone + Debug + Serialize, T: Clone + Debug + Serialize",
583 deserialize = "R: Clone + Debug + serde::de::DeserializeOwned, T: Clone + Debug + serde::de::DeserializeOwned",
584))]
585struct RawMultiCloudMultiRegionConfiguration<R: Clone + Debug, T: Clone + Debug> {
586 preferred: RegionName,
587 regions: Vec<ProviderRegion<R>>,
588 topologies: Vec<Topology<T>>,
589}
590
591impl<R: Clone + Debug, T: Clone + Debug> From<MultiCloudMultiRegionConfiguration<R, T>>
592 for RawMultiCloudMultiRegionConfiguration<R, T>
593{
594 fn from(config: MultiCloudMultiRegionConfiguration<R, T>) -> Self {
595 Self {
596 preferred: config.preferred,
597 regions: config.regions,
598 topologies: config.topologies,
599 }
600 }
601}
602
603impl<R: Clone + Debug, T: Clone + Debug> TryFrom<RawMultiCloudMultiRegionConfiguration<R, T>>
604 for MultiCloudMultiRegionConfiguration<R, T>
605{
606 type Error = ValidationError;
607
608 fn try_from(raw: RawMultiCloudMultiRegionConfiguration<R, T>) -> Result<Self, Self::Error> {
609 MultiCloudMultiRegionConfiguration::new(raw.preferred, raw.regions, raw.topologies)
610 }
611}
612
613impl<
614 'de,
615 R: Clone + Debug + Serialize + serde::de::DeserializeOwned,
616 T: Clone + Debug + Serialize + serde::de::DeserializeOwned,
617 > Deserialize<'de> for MultiCloudMultiRegionConfiguration<R, T>
618{
619 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
620 where
621 D: serde::Deserializer<'de>,
622 {
623 let raw = RawMultiCloudMultiRegionConfiguration::<R, T>::deserialize(deserializer)?;
624 MultiCloudMultiRegionConfiguration::try_from(raw).map_err(serde::de::Error::custom)
625 }
626}
627
628#[derive(Clone, Debug, Default, Eq, Error, PartialEq)]
630#[error("{}", self.format_message())]
631pub struct ValidationError {
632 duplicate_region_names: Vec<RegionName>,
633 duplicate_topology_names: Vec<TopologyName>,
634 unknown_topology_regions: Vec<RegionName>,
635 unknown_preferred_region: Option<RegionName>,
636}
637
638impl ChromaError for ValidationError {
639 fn code(&self) -> ErrorCodes {
640 ErrorCodes::InvalidArgument
641 }
642}
643
644impl ValidationError {
645 #[cfg(test)]
646 fn new(
647 duplicate_region_names: Vec<RegionName>,
648 duplicate_topology_names: Vec<TopologyName>,
649 unknown_topology_regions: Vec<RegionName>,
650 unknown_preferred_region: Option<RegionName>,
651 ) -> Self {
652 Self {
653 duplicate_region_names,
654 duplicate_topology_names,
655 unknown_topology_regions,
656 unknown_preferred_region,
657 }
658 }
659
660 pub fn has_errors(&self) -> bool {
662 !self.duplicate_region_names.is_empty()
663 || !self.duplicate_topology_names.is_empty()
664 || !self.unknown_topology_regions.is_empty()
665 || self.unknown_preferred_region.is_some()
666 }
667
668 pub fn duplicate_region_names(&self) -> &[RegionName] {
670 &self.duplicate_region_names
671 }
672
673 pub fn duplicate_topology_names(&self) -> &[TopologyName] {
675 &self.duplicate_topology_names
676 }
677
678 pub fn unknown_topology_regions(&self) -> &[RegionName] {
680 &self.unknown_topology_regions
681 }
682
683 pub fn unknown_preferred_region(&self) -> Option<&RegionName> {
685 self.unknown_preferred_region.as_ref()
686 }
687
688 fn format_message(&self) -> String {
689 if !self.has_errors() {
690 return "no validation errors".to_string();
691 }
692
693 let mut parts = Vec::new();
694
695 if !self.duplicate_region_names.is_empty() {
696 parts.push(format!(
697 "duplicate region names: {}",
698 format_name_list(&self.duplicate_region_names)
699 ));
700 }
701
702 if !self.duplicate_topology_names.is_empty() {
703 parts.push(format!(
704 "duplicate topology names: {}",
705 format_name_list(&self.duplicate_topology_names)
706 ));
707 }
708
709 if !self.unknown_topology_regions.is_empty() {
710 parts.push(format!(
711 "unknown topology regions: {}",
712 format_name_list(&self.unknown_topology_regions)
713 ));
714 }
715
716 if let Some(ref name) = self.unknown_preferred_region {
717 parts.push(format!("unknown preferred region: {}", name));
718 }
719
720 parts.join("; ")
721 }
722}
723
724fn format_name_list<T: std::fmt::Display>(names: &[T]) -> String {
726 names
727 .iter()
728 .map(|n| n.to_string())
729 .collect::<Vec<_>>()
730 .join(", ")
731}
732
733impl<R: Clone + Debug, T: Clone + Debug> MultiCloudMultiRegionConfiguration<R, T> {
734 pub fn new(
777 preferred: RegionName,
778 regions: Vec<ProviderRegion<R>>,
779 topologies: Vec<Topology<T>>,
780 ) -> Result<Self, ValidationError> {
781 let config = Self {
782 preferred,
783 regions,
784 topologies,
785 };
786 config.validate()?;
787 Ok(config)
788 }
789
790 pub fn preferred(&self) -> &RegionName {
792 &self.preferred
793 }
794
795 pub fn regions(&self) -> &[ProviderRegion<R>] {
797 &self.regions
798 }
799
800 pub fn topologies(&self) -> &[Topology<T>] {
802 &self.topologies
803 }
804
805 pub fn validate(&self) -> Result<(), ValidationError> {
810 let mut error = ValidationError::default();
811 let all_region_names: HashSet<_> = self.regions.iter().map(|r| &r.name).collect();
812
813 error.duplicate_region_names = find_duplicates(&self.regions, |r| &r.name);
814 error.duplicate_topology_names = find_duplicates(&self.topologies, |t| &t.name);
815
816 let mut unknown_regions: Vec<_> = self
818 .topologies
819 .iter()
820 .flat_map(|t| &t.regions)
821 .filter(|r| !all_region_names.contains(r))
822 .cloned()
823 .collect();
824 unknown_regions.sort();
825 unknown_regions.dedup();
826 error.unknown_topology_regions = unknown_regions;
827
828 if !all_region_names.contains(&self.preferred) {
830 error.unknown_preferred_region = Some(self.preferred.clone());
831 }
832
833 if error.has_errors() {
834 Err(error)
835 } else {
836 Ok(())
837 }
838 }
839
840 pub fn preferred_region_config(&self) -> Option<&R> {
867 self.regions
868 .iter()
869 .find(|r| r.name == self.preferred)
870 .map(|r| r.config())
871 }
872
873 pub fn cast<R2, T2, FR, FT>(
912 self,
913 region_fn: FR,
914 topology_fn: FT,
915 ) -> MultiCloudMultiRegionConfiguration<R2, T2>
916 where
917 R2: Clone + Debug,
918 T2: Clone + Debug,
919 FR: Fn(R) -> R2,
920 FT: Fn(T) -> T2,
921 {
922 MultiCloudMultiRegionConfiguration {
923 preferred: self.preferred,
924 regions: self
925 .regions
926 .into_iter()
927 .map(|r| r.cast(®ion_fn))
928 .collect(),
929 topologies: self
930 .topologies
931 .into_iter()
932 .map(|t| t.cast(&topology_fn))
933 .collect(),
934 }
935 }
936
937 pub fn try_cast<R2, T2, E, FR, FT>(
982 self,
983 region_fn: FR,
984 topology_fn: FT,
985 ) -> Result<MultiCloudMultiRegionConfiguration<R2, T2>, E>
986 where
987 R2: Clone + Debug,
988 T2: Clone + Debug,
989 FR: Fn(R) -> Result<R2, E>,
990 FT: Fn(T) -> Result<T2, E>,
991 {
992 let regions: Result<Vec<_>, E> = self
993 .regions
994 .into_iter()
995 .map(|r| r.try_cast(®ion_fn))
996 .collect();
997 let topologies: Result<Vec<_>, E> = self
998 .topologies
999 .into_iter()
1000 .map(|t| t.try_cast(&topology_fn))
1001 .collect();
1002 Ok(MultiCloudMultiRegionConfiguration {
1003 preferred: self.preferred,
1004 regions: regions?,
1005 topologies: topologies?,
1006 })
1007 }
1008
1009 pub async fn try_cast_async<R2, T2, E, FR, FT, FUT1, FUT2>(
1016 self,
1017 region_fn: FR,
1018 topology_fn: FT,
1019 ) -> Result<MultiCloudMultiRegionConfiguration<R2, T2>, E>
1020 where
1021 R2: Clone + Debug,
1022 T2: Clone + Debug,
1023 FR: Fn(R) -> FUT1,
1024 FT: Fn(T) -> FUT2,
1025 FUT1: Future<Output = Result<R2, E>> + Send,
1026 FUT2: Future<Output = Result<T2, E>> + Send,
1027 {
1028 let mut regions = Vec::with_capacity(self.regions.len());
1029 for region in self.regions.into_iter() {
1030 regions.push(region.try_cast_async(®ion_fn).await?);
1031 }
1032 let mut topologies = Vec::with_capacity(self.topologies.len());
1033 for topo in self.topologies.into_iter() {
1034 topologies.push(topo.try_cast_async(&topology_fn).await?);
1035 }
1036 Ok(MultiCloudMultiRegionConfiguration {
1037 preferred: self.preferred,
1038 regions,
1039 topologies,
1040 })
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047
1048 fn region_name(s: impl Into<String>) -> RegionName {
1049 RegionName::new(s).expect("test region name should be valid")
1050 }
1051
1052 fn topology_name(s: impl Into<String>) -> TopologyName {
1053 TopologyName::new(s).expect("test topology name should be valid")
1054 }
1055
1056 fn provider_region(
1057 name: impl Into<String>,
1058 provider: impl Into<String>,
1059 region: impl Into<String>,
1060 ) -> ProviderRegion<()> {
1061 ProviderRegion::new(
1062 RegionName::new(name).expect("test region name should be valid"),
1063 provider,
1064 region,
1065 (),
1066 )
1067 }
1068
1069 fn topology(name: impl Into<String>, regions: Vec<&str>) -> Topology<()> {
1070 Topology::new(
1071 TopologyName::new(name).expect("test topology name should be valid"),
1072 regions
1073 .into_iter()
1074 .map(|s| RegionName::new(s).expect("test region name should be valid"))
1075 .collect(),
1076 (),
1077 )
1078 }
1079
1080 #[test]
1081 fn region_name_as_str() {
1082 let name = RegionName::new("aws-us-east-1").expect("valid name");
1083 assert_eq!(name.as_str(), "aws-us-east-1");
1084 }
1085
1086 #[test]
1087 fn region_name_display() {
1088 let name = RegionName::new("aws-us-east-1").expect("valid name");
1089 assert_eq!(format!("{}", name), "aws-us-east-1");
1090 }
1091
1092 #[test]
1093 fn region_name_equality() {
1094 let a = RegionName::new("aws-us-east-1");
1095 let b = RegionName::new("aws-us-east-1");
1096 let c = RegionName::new("gcp-europe-west1");
1097 assert_eq!(a, b);
1098 assert_ne!(a, c);
1099 }
1100
1101 #[test]
1102 fn region_name_clone() {
1103 let a = RegionName::new("aws-us-east-1");
1104 let b = a.clone();
1105 assert_eq!(a, b);
1106 }
1107
1108 #[test]
1109 fn region_name_serde_roundtrip() {
1110 let name = RegionName::new("aws-us-east-1").expect("valid name");
1111 let json = serde_json::to_string(&name).unwrap();
1112 assert_eq!(json, "\"aws-us-east-1\"");
1113 let deserialized: RegionName = serde_json::from_str(&json).unwrap();
1114 assert_eq!(name, deserialized);
1115 }
1116
1117 #[test]
1118 fn topology_name_as_str() {
1119 let name = TopologyName::new("global").expect("valid name");
1120 assert_eq!(name.as_str(), "global");
1121 }
1122
1123 #[test]
1124 fn topology_name_display() {
1125 let name = TopologyName::new("global").expect("valid name");
1126 assert_eq!(format!("{}", name), "global");
1127 }
1128
1129 #[test]
1130 fn topology_name_equality() {
1131 let a = TopologyName::new("global");
1132 let b = TopologyName::new("global");
1133 let c = TopologyName::new("regional");
1134 assert_eq!(a, b);
1135 assert_ne!(a, c);
1136 }
1137
1138 #[test]
1139 fn topology_name_clone() {
1140 let a = TopologyName::new("global");
1141 let b = a.clone();
1142 assert_eq!(a, b);
1143 }
1144
1145 #[test]
1146 fn topology_name_serde_roundtrip() {
1147 let name = TopologyName::new("global").expect("valid name");
1148 let json = serde_json::to_string(&name).unwrap();
1149 assert_eq!(json, "\"global\"");
1150 let deserialized: TopologyName = serde_json::from_str(&json).unwrap();
1151 assert_eq!(name, deserialized);
1152 }
1153
1154 #[test]
1155 fn provider_region_accessors() {
1156 let region = ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", ());
1157 assert_eq!(region.name(), ®ion_name("aws-us-east-1"));
1158 assert_eq!(region.provider(), "aws");
1159 assert_eq!(region.region(), "us-east-1");
1160 }
1161
1162 #[test]
1163 fn provider_region_equality() {
1164 let a = provider_region("aws-us-east-1", "aws", "us-east-1");
1165 let b = provider_region("aws-us-east-1", "aws", "us-east-1");
1166 let c = provider_region("gcp-europe-west1", "gcp", "europe-west1");
1167 assert_eq!(a, b);
1168 assert_ne!(a, c);
1169 }
1170
1171 #[test]
1172 fn provider_region_clone() {
1173 let a = provider_region("aws-us-east-1", "aws", "us-east-1");
1174 let b = a.clone();
1175 assert_eq!(a, b);
1176 }
1177
1178 #[test]
1179 fn provider_region_serde_roundtrip() {
1180 let region = provider_region("aws-us-east-1", "aws", "us-east-1");
1181 let json = serde_json::to_string(®ion).unwrap();
1182 let deserialized: ProviderRegion<()> = serde_json::from_str(&json).unwrap();
1183 assert_eq!(region, deserialized);
1184 }
1185
1186 #[test]
1187 fn topology_accessors() {
1188 let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
1189 assert_eq!(t.name(), &topology_name("global"));
1190 assert_eq!(
1191 t.regions(),
1192 &[
1193 region_name("aws-us-east-1"),
1194 region_name("gcp-europe-west1")
1195 ]
1196 );
1197 }
1198
1199 #[test]
1200 fn topology_equality() {
1201 let a = topology("global", vec!["aws-us-east-1"]);
1202 let b = topology("global", vec!["aws-us-east-1"]);
1203 let c = topology("regional", vec!["aws-us-east-1"]);
1204 assert_eq!(a, b);
1205 assert_ne!(a, c);
1206 }
1207
1208 #[test]
1209 fn topology_clone() {
1210 let a = topology("global", vec!["aws-us-east-1"]);
1211 let b = a.clone();
1212 assert_eq!(a, b);
1213 }
1214
1215 #[test]
1216 fn topology_serde_roundtrip() {
1217 let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
1218 let json = serde_json::to_string(&t).unwrap();
1219 let deserialized: Topology<()> = serde_json::from_str(&json).unwrap();
1220 assert_eq!(t, deserialized);
1221 }
1222
1223 #[test]
1224 fn valid_configuration() {
1225 let config = MultiCloudMultiRegionConfiguration::new(
1226 region_name("aws-us-east-1"),
1227 vec![
1228 provider_region("aws-us-east-1", "aws", "us-east-1"),
1229 provider_region("gcp-europe-west1", "gcp", "europe-west1"),
1230 ],
1231 vec![topology(
1232 "global",
1233 vec!["aws-us-east-1", "gcp-europe-west1"],
1234 )],
1235 );
1236
1237 assert!(config.is_ok(), "Expected valid configuration: {:?}", config);
1238 }
1239
1240 #[test]
1241 fn valid_configuration_accessors() {
1242 let config = MultiCloudMultiRegionConfiguration::new(
1243 region_name("aws-us-east-1"),
1244 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1245 vec![topology("global", vec!["aws-us-east-1"])],
1246 )
1247 .expect("valid configuration");
1248
1249 assert_eq!(config.preferred(), ®ion_name("aws-us-east-1"));
1250 assert_eq!(config.regions().len(), 1);
1251 assert_eq!(config.topologies().len(), 1);
1252 }
1253
1254 #[test]
1255 fn configuration_serde_roundtrip() {
1256 let config = MultiCloudMultiRegionConfiguration::new(
1257 region_name("aws-us-east-1"),
1258 vec![
1259 provider_region("aws-us-east-1", "aws", "us-east-1"),
1260 provider_region("gcp-europe-west1", "gcp", "europe-west1"),
1261 ],
1262 vec![topology(
1263 "global",
1264 vec!["aws-us-east-1", "gcp-europe-west1"],
1265 )],
1266 )
1267 .expect("valid configuration");
1268
1269 let json = serde_json::to_string(&config).unwrap();
1270 let deserialized: MultiCloudMultiRegionConfiguration<(), ()> =
1271 serde_json::from_str(&json).unwrap();
1272 assert_eq!(config, deserialized);
1273 }
1274
1275 #[test]
1276 fn configuration_serde_roundtrip_with_complex_config() {
1277 #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1279 struct RegionConfig {
1280 endpoint: String,
1281 max_connections: u32,
1282 }
1283
1284 #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1285 struct TopologyConfig {
1286 replication_factor: u8,
1287 consistency_level: String,
1288 }
1289
1290 let region1 = ProviderRegion::new(
1291 RegionName::new("aws-us-east-1").unwrap(),
1292 "aws",
1293 "us-east-1",
1294 RegionConfig {
1295 endpoint: "https://us-east-1.example.com".to_string(),
1296 max_connections: 100,
1297 },
1298 );
1299 let region2 = ProviderRegion::new(
1300 RegionName::new("gcp-europe-west1").unwrap(),
1301 "gcp",
1302 "europe-west1",
1303 RegionConfig {
1304 endpoint: "https://europe-west1.example.com".to_string(),
1305 max_connections: 50,
1306 },
1307 );
1308
1309 let topology = Topology::new(
1310 TopologyName::new("global").unwrap(),
1311 vec![
1312 RegionName::new("aws-us-east-1").unwrap(),
1313 RegionName::new("gcp-europe-west1").unwrap(),
1314 ],
1315 TopologyConfig {
1316 replication_factor: 3,
1317 consistency_level: "quorum".to_string(),
1318 },
1319 );
1320
1321 let config: MultiCloudMultiRegionConfiguration<RegionConfig, TopologyConfig> =
1322 MultiCloudMultiRegionConfiguration::new(
1323 RegionName::new("aws-us-east-1").unwrap(),
1324 vec![region1, region2],
1325 vec![topology],
1326 )
1327 .expect("valid configuration");
1328
1329 let json = serde_json::to_string_pretty(&config).unwrap();
1331 assert!(
1332 json.contains("endpoint"),
1333 "JSON should contain region config fields: {json}"
1334 );
1335 assert!(
1336 json.contains("replication_factor"),
1337 "JSON should contain topology config fields: {json}"
1338 );
1339
1340 let deserialized: MultiCloudMultiRegionConfiguration<RegionConfig, TopologyConfig> =
1342 serde_json::from_str(&json).unwrap();
1343 assert_eq!(config, deserialized);
1344
1345 assert_eq!(
1347 deserialized.regions()[0].config().endpoint,
1348 "https://us-east-1.example.com"
1349 );
1350 assert_eq!(deserialized.regions()[0].config().max_connections, 100);
1351 assert_eq!(deserialized.topologies()[0].config().replication_factor, 3);
1352 assert_eq!(
1353 deserialized.topologies()[0].config().consistency_level,
1354 "quorum"
1355 );
1356 }
1357
1358 #[test]
1359 fn configuration_deserialize_valid() {
1360 let json = r#"{
1361 "preferred": "aws-us-east-1",
1362 "regions": [
1363 {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null},
1364 {"name": "gcp-europe-west1", "provider": "gcp", "region": "europe-west1", "config": null}
1365 ],
1366 "topologies": [
1367 {"name": "global", "regions": ["aws-us-east-1", "gcp-europe-west1"], "config": null}
1368 ]
1369 }"#;
1370
1371 let config: MultiCloudMultiRegionConfiguration<(), ()> =
1372 serde_json::from_str(json).unwrap();
1373 assert_eq!(config.preferred().as_str(), "aws-us-east-1");
1374 assert_eq!(config.topologies().len(), 1);
1375 assert_eq!(config.topologies()[0].name().as_str(), "global");
1376 assert_eq!(config.topologies()[0].regions().len(), 2);
1377 }
1378
1379 #[test]
1380 fn configuration_deserialize_invalid_preferred() {
1381 let json = r#"{
1382 "preferred": "nonexistent",
1383 "regions": [
1384 {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
1385 ],
1386 "topologies": []
1387 }"#;
1388
1389 let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
1390 serde_json::from_str(json);
1391 assert!(result.is_err());
1392 let err_msg = result.unwrap_err().to_string();
1393 assert!(
1394 err_msg.contains("unknown preferred region"),
1395 "Expected error message to contain 'unknown preferred region', got: {}",
1396 err_msg
1397 );
1398 }
1399
1400 #[test]
1401 fn configuration_deserialize_duplicate_regions() {
1402 let json = r#"{
1403 "preferred": "aws-us-east-1",
1404 "regions": [
1405 {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null},
1406 {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
1407 ],
1408 "topologies": []
1409 }"#;
1410
1411 let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
1412 serde_json::from_str(json);
1413 assert!(result.is_err());
1414 let err_msg = result.unwrap_err().to_string();
1415 assert!(
1416 err_msg.contains("duplicate region names"),
1417 "Expected error message to contain 'duplicate region names', got: {}",
1418 err_msg
1419 );
1420 }
1421
1422 #[test]
1423 fn configuration_deserialize_unknown_topology_region() {
1424 let json = r#"{
1425 "preferred": "aws-us-east-1",
1426 "regions": [
1427 {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
1428 ],
1429 "topologies": [
1430 {"name": "global", "regions": ["aws-us-east-1", "nonexistent"], "config": null}
1431 ]
1432 }"#;
1433
1434 let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
1435 serde_json::from_str(json);
1436 assert!(result.is_err());
1437 let err_msg = result.unwrap_err().to_string();
1438 assert!(
1439 err_msg.contains("unknown topology regions"),
1440 "Expected error message to contain 'unknown topology regions', got: {}",
1441 err_msg
1442 );
1443 }
1444
1445 #[test]
1446 fn empty_configuration() {
1447 let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
1448 region_name("nonexistent"),
1449 vec![],
1450 vec![],
1451 );
1452
1453 let err = config.unwrap_err();
1454 assert!(err.duplicate_region_names().is_empty());
1455 assert!(err.duplicate_topology_names().is_empty());
1456 assert!(err.unknown_topology_regions().is_empty());
1457 assert_eq!(
1458 err.unknown_preferred_region(),
1459 Some(®ion_name("nonexistent"))
1460 );
1461 }
1462
1463 #[test]
1464 fn empty_topology_regions() {
1465 let config = MultiCloudMultiRegionConfiguration::new(
1466 region_name("aws-us-east-1"),
1467 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1468 vec![topology("empty", vec![])],
1469 );
1470
1471 assert!(
1472 config.is_ok(),
1473 "Topology with no regions should be valid: {:?}",
1474 config
1475 );
1476 }
1477
1478 #[test]
1479 fn duplicate_region_names() {
1480 let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
1481 region_name("aws-us-east-1"),
1482 vec![
1483 provider_region("aws-us-east-1", "aws", "us-east-1"),
1484 provider_region("aws-us-east-1", "aws", "us-east-1"),
1485 ],
1486 vec![],
1487 );
1488
1489 let err = config.unwrap_err();
1490 assert_eq!(
1491 err.duplicate_region_names(),
1492 &[region_name("aws-us-east-1")]
1493 );
1494 assert!(err.duplicate_topology_names().is_empty());
1495 assert!(err.unknown_topology_regions().is_empty());
1496 assert_eq!(err.unknown_preferred_region(), None);
1497 }
1498
1499 #[test]
1500 fn duplicate_topology_names() {
1501 let config = MultiCloudMultiRegionConfiguration::new(
1502 region_name("aws-us-east-1"),
1503 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1504 vec![
1505 topology("global", vec!["aws-us-east-1"]),
1506 topology("global", vec!["aws-us-east-1"]),
1507 ],
1508 );
1509
1510 let err = config.unwrap_err();
1511 assert!(err.duplicate_region_names().is_empty());
1512 assert_eq!(err.duplicate_topology_names(), &[topology_name("global")]);
1513 assert!(err.unknown_topology_regions().is_empty());
1514 assert_eq!(err.unknown_preferred_region(), None);
1515 }
1516
1517 #[test]
1518 fn unknown_topology_region() {
1519 let config = MultiCloudMultiRegionConfiguration::new(
1520 region_name("aws-us-east-1"),
1521 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1522 vec![topology(
1523 "global",
1524 vec!["aws-us-east-1", "nonexistent-region"],
1525 )],
1526 );
1527
1528 let err = config.unwrap_err();
1529 assert!(err.duplicate_region_names().is_empty());
1530 assert!(err.duplicate_topology_names().is_empty());
1531 assert_eq!(
1532 err.unknown_topology_regions(),
1533 &[region_name("nonexistent-region")]
1534 );
1535 assert_eq!(err.unknown_preferred_region(), None);
1536 }
1537
1538 #[test]
1539 fn unknown_topology_region_duplicated_in_multiple_topologies() {
1540 let config = MultiCloudMultiRegionConfiguration::new(
1543 region_name("aws-us-east-1"),
1544 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1545 vec![
1546 topology("topo1", vec!["aws-us-east-1", "nonexistent"]),
1547 topology("topo2", vec!["nonexistent"]),
1548 ],
1549 );
1550
1551 let err = config.unwrap_err();
1552 assert!(err.duplicate_region_names().is_empty());
1553 assert!(err.duplicate_topology_names().is_empty());
1554 assert_eq!(
1555 err.unknown_topology_regions(),
1556 &[region_name("nonexistent")]
1557 );
1558 assert_eq!(err.unknown_preferred_region(), None);
1559 }
1560
1561 #[test]
1562 fn unknown_preferred_region() {
1563 let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
1564 region_name("nonexistent-region"),
1565 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1566 vec![],
1567 );
1568
1569 let err = config.unwrap_err();
1570 assert!(err.duplicate_region_names().is_empty());
1571 assert!(err.duplicate_topology_names().is_empty());
1572 assert!(err.unknown_topology_regions().is_empty());
1573 assert_eq!(
1574 err.unknown_preferred_region(),
1575 Some(®ion_name("nonexistent-region"))
1576 );
1577 }
1578
1579 #[test]
1580 fn multiple_validation_errors() {
1581 let config = MultiCloudMultiRegionConfiguration::new(
1582 region_name("nonexistent-preferred"),
1583 vec![
1584 provider_region("aws-us-east-1", "aws", "us-east-1"),
1585 provider_region("aws-us-east-1", "aws", "us-east-1"),
1586 ],
1587 vec![
1588 topology("topo1", vec!["unknown-region"]),
1589 topology("topo1", vec!["aws-us-east-1"]),
1590 ],
1591 );
1592
1593 let err = config.unwrap_err();
1594 assert_eq!(
1595 err.duplicate_region_names(),
1596 &[region_name("aws-us-east-1")]
1597 );
1598 assert_eq!(err.duplicate_topology_names(), &[topology_name("topo1")]);
1599 assert_eq!(
1600 err.unknown_topology_regions(),
1601 &[region_name("unknown-region")]
1602 );
1603 assert_eq!(
1604 err.unknown_preferred_region(),
1605 Some(®ion_name("nonexistent-preferred"))
1606 );
1607 }
1608
1609 #[test]
1610 fn display_no_errors() {
1611 let error = ValidationError::default();
1612 assert_eq!(error.to_string(), "no validation errors");
1613 }
1614
1615 #[test]
1616 fn display_duplicate_region_names_only() {
1617 let error = ValidationError::new(
1618 vec![region_name("region-a"), region_name("region-b")],
1619 vec![],
1620 vec![],
1621 None,
1622 );
1623 assert_eq!(
1624 error.to_string(),
1625 "duplicate region names: region-a, region-b"
1626 );
1627 }
1628
1629 #[test]
1630 fn display_duplicate_topology_names_only() {
1631 let error = ValidationError::new(vec![], vec![topology_name("topo-x")], vec![], None);
1632 assert_eq!(error.to_string(), "duplicate topology names: topo-x");
1633 }
1634
1635 #[test]
1636 fn display_unknown_topology_regions_only() {
1637 let error = ValidationError::new(
1638 vec![],
1639 vec![],
1640 vec![region_name("missing-1"), region_name("missing-2")],
1641 None,
1642 );
1643 assert_eq!(
1644 error.to_string(),
1645 "unknown topology regions: missing-1, missing-2"
1646 );
1647 }
1648
1649 #[test]
1650 fn display_unknown_preferred_region_only() {
1651 let error =
1652 ValidationError::new(vec![], vec![], vec![], Some(region_name("missing-region")));
1653 assert_eq!(
1654 error.to_string(),
1655 "unknown preferred region: missing-region"
1656 );
1657 }
1658
1659 #[test]
1660 fn display_all_errors() {
1661 let error = ValidationError::new(
1662 vec![region_name("dup-region")],
1663 vec![topology_name("dup-topo")],
1664 vec![region_name("unknown-reg")],
1665 Some(region_name("bad-preferred")),
1666 );
1667 assert_eq!(
1668 error.to_string(),
1669 "duplicate region names: dup-region; duplicate topology names: dup-topo; unknown topology regions: unknown-reg; unknown preferred region: bad-preferred"
1670 );
1671 }
1672
1673 #[test]
1674 fn display_special_characters() {
1675 let error = ValidationError::new(
1676 vec![
1677 region_name("region-with-dash_and_underscore"),
1678 region_name("region with spaces"),
1679 ],
1680 vec![topology_name("topo.dot")],
1681 vec![region_name("region\nwith\nnewlines")],
1682 None,
1683 );
1684 assert_eq!(
1685 error.to_string(),
1686 "duplicate region names: region-with-dash_and_underscore, region with spaces; duplicate topology names: topo.dot; unknown topology regions: region\nwith\nnewlines"
1687 );
1688 }
1689
1690 #[test]
1691 fn validation_error_has_errors_default() {
1692 let error = ValidationError::default();
1693 assert!(!error.has_errors());
1694 }
1695
1696 #[test]
1697 fn validation_error_has_errors_with_duplicate_regions() {
1698 let error = ValidationError::new(vec![region_name("dup")], vec![], vec![], None);
1699 assert!(error.has_errors());
1700 }
1701
1702 #[test]
1703 fn validation_error_has_errors_with_duplicate_topologies() {
1704 let error = ValidationError::new(vec![], vec![topology_name("dup")], vec![], None);
1705 assert!(error.has_errors());
1706 }
1707
1708 #[test]
1709 fn validation_error_has_errors_with_unknown_topology_regions() {
1710 let error = ValidationError::new(vec![], vec![], vec![region_name("unknown")], None);
1711 assert!(error.has_errors());
1712 }
1713
1714 #[test]
1715 fn validation_error_has_errors_with_unknown_preferred() {
1716 let error = ValidationError::new(vec![], vec![], vec![], Some(region_name("unknown")));
1717 assert!(error.has_errors());
1718 }
1719
1720 #[test]
1721 fn configuration_clone() {
1722 let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
1723 region_name("aws-us-east-1"),
1724 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1725 vec![],
1726 )
1727 .expect("valid configuration");
1728
1729 let cloned = config.clone();
1730 assert_eq!(config, cloned);
1731 }
1732
1733 #[test]
1734 fn configuration_debug() {
1735 let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
1736 region_name("aws-us-east-1"),
1737 vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1738 vec![],
1739 )
1740 .expect("valid configuration");
1741
1742 let debug_str = format!("{:?}", config);
1743 assert!(debug_str.contains("MultiCloudMultiRegionConfiguration"));
1744 assert!(debug_str.contains("aws-us-east-1"));
1745 }
1746
1747 #[test]
1748 fn region_name_valid() {
1749 assert!(RegionName::new("aws-us-east-1").is_ok());
1750 assert!(RegionName::new("a").is_ok());
1751 assert!(RegionName::new("12345678901234567890123456789012").is_ok());
1753 }
1754
1755 #[test]
1756 fn region_name_empty() {
1757 let result = RegionName::new("");
1758 assert!(result.is_err());
1759 println!(
1760 "region_name_empty error: {:?}",
1761 result.as_ref().unwrap_err()
1762 );
1763 assert!(matches!(result, Err(NameError::Empty)));
1764 }
1765
1766 #[test]
1767 fn region_name_too_long() {
1768 let result = RegionName::new("123456789012345678901234567890123");
1770 assert!(result.is_err());
1771 println!(
1772 "region_name_too_long error: {:?}",
1773 result.as_ref().unwrap_err()
1774 );
1775 assert!(matches!(result, Err(NameError::TooLong(33))));
1776 }
1777
1778 #[test]
1779 fn region_name_non_ascii() {
1780 let result = RegionName::new("region-🌍");
1781 assert!(result.is_err());
1782 println!(
1783 "region_name_non_ascii error: {:?}",
1784 result.as_ref().unwrap_err()
1785 );
1786 assert!(matches!(result, Err(NameError::NonAscii)));
1787 }
1788
1789 #[test]
1790 fn topology_name_valid() {
1791 assert!(TopologyName::new("global").is_ok());
1792 assert!(TopologyName::new("a").is_ok());
1793 assert!(TopologyName::new("12345678901234567890123456789012").is_ok());
1795 }
1796
1797 #[test]
1798 fn topology_name_empty() {
1799 let result = TopologyName::new("");
1800 assert!(result.is_err());
1801 println!(
1802 "topology_name_empty error: {:?}",
1803 result.as_ref().unwrap_err()
1804 );
1805 assert!(matches!(result, Err(NameError::Empty)));
1806 }
1807
1808 #[test]
1809 fn topology_name_too_long() {
1810 let result = TopologyName::new("123456789012345678901234567890123");
1812 assert!(result.is_err());
1813 println!(
1814 "topology_name_too_long error: {:?}",
1815 result.as_ref().unwrap_err()
1816 );
1817 assert!(matches!(result, Err(NameError::TooLong(33))));
1818 }
1819
1820 #[test]
1821 fn topology_name_non_ascii() {
1822 let result = TopologyName::new("拓扑名");
1823 assert!(result.is_err());
1824 println!(
1825 "topology_name_non_ascii error: {:?}",
1826 result.as_ref().unwrap_err()
1827 );
1828 assert!(matches!(result, Err(NameError::NonAscii)));
1829 }
1830
1831 #[test]
1832 fn name_error_display_empty() {
1833 let err = NameError::Empty;
1834 assert_eq!(err.to_string(), "name cannot be empty");
1835 }
1836
1837 #[test]
1838 fn name_error_display_too_long() {
1839 let err = NameError::TooLong(50);
1840 assert_eq!(
1841 err.to_string(),
1842 "name exceeds maximum length of 32 characters: 50 characters"
1843 );
1844 }
1845
1846 #[test]
1847 fn name_error_display_non_ascii() {
1848 let err = NameError::NonAscii;
1849 assert_eq!(err.to_string(), "name contains non-ASCII characters");
1850 }
1851
1852 #[test]
1853 fn region_name_deserialize_valid() {
1854 let json = "\"aws-us-east-1\"";
1855 let name: RegionName = serde_json::from_str(json).unwrap();
1856 assert_eq!(name.as_str(), "aws-us-east-1");
1857 }
1858
1859 #[test]
1860 fn region_name_deserialize_empty() {
1861 let json = "\"\"";
1862 let result: Result<RegionName, _> = serde_json::from_str(json);
1863 assert!(result.is_err());
1864 let err_msg = result.unwrap_err().to_string();
1865 println!("region_name_deserialize_empty error: {}", err_msg);
1866 assert!(
1867 err_msg.contains("name cannot be empty"),
1868 "Expected error message to contain 'name cannot be empty', got: {}",
1869 err_msg
1870 );
1871 }
1872
1873 #[test]
1874 fn region_name_deserialize_too_long() {
1875 let json = "\"123456789012345678901234567890123\"";
1876 let result: Result<RegionName, _> = serde_json::from_str(json);
1877 assert!(result.is_err());
1878 let err_msg = result.unwrap_err().to_string();
1879 println!("region_name_deserialize_too_long error: {}", err_msg);
1880 assert!(
1881 err_msg.contains("name exceeds maximum length"),
1882 "Expected error message to contain 'name exceeds maximum length', got: {}",
1883 err_msg
1884 );
1885 }
1886
1887 #[test]
1888 fn region_name_deserialize_non_ascii() {
1889 let json = "\"region-🌍\"";
1890 let result: Result<RegionName, _> = serde_json::from_str(json);
1891 assert!(result.is_err());
1892 let err_msg = result.unwrap_err().to_string();
1893 println!("region_name_deserialize_non_ascii error: {}", err_msg);
1894 assert!(
1895 err_msg.contains("non-ASCII"),
1896 "Expected error message to contain 'non-ASCII', got: {}",
1897 err_msg
1898 );
1899 }
1900
1901 #[test]
1902 fn topology_name_deserialize_valid() {
1903 let json = "\"global\"";
1904 let name: TopologyName = serde_json::from_str(json).unwrap();
1905 assert_eq!(name.as_str(), "global");
1906 }
1907
1908 #[test]
1909 fn topology_name_deserialize_empty() {
1910 let json = "\"\"";
1911 let result: Result<TopologyName, _> = serde_json::from_str(json);
1912 assert!(result.is_err());
1913 let err_msg = result.unwrap_err().to_string();
1914 println!("topology_name_deserialize_empty error: {}", err_msg);
1915 assert!(
1916 err_msg.contains("name cannot be empty"),
1917 "Expected error message to contain 'name cannot be empty', got: {}",
1918 err_msg
1919 );
1920 }
1921
1922 #[test]
1923 fn topology_name_deserialize_too_long() {
1924 let json = "\"123456789012345678901234567890123\"";
1925 let result: Result<TopologyName, _> = serde_json::from_str(json);
1926 assert!(result.is_err());
1927 let err_msg = result.unwrap_err().to_string();
1928 println!("topology_name_deserialize_too_long error: {}", err_msg);
1929 assert!(
1930 err_msg.contains("name exceeds maximum length"),
1931 "Expected error message to contain 'name exceeds maximum length', got: {}",
1932 err_msg
1933 );
1934 }
1935
1936 #[test]
1937 fn topology_name_deserialize_non_ascii() {
1938 let json = "\"拓扑名\"";
1939 let result: Result<TopologyName, _> = serde_json::from_str(json);
1940 assert!(result.is_err());
1941 let err_msg = result.unwrap_err().to_string();
1942 println!("topology_name_deserialize_non_ascii error: {}", err_msg);
1943 assert!(
1944 err_msg.contains("non-ASCII"),
1945 "Expected error message to contain 'non-ASCII', got: {}",
1946 err_msg
1947 );
1948 }
1949
1950 #[test]
1951 fn preferred_region_config_returns_config() {
1952 let config = MultiCloudMultiRegionConfiguration::<String, ()>::new(
1953 region_name("aws-us-east-1"),
1954 vec![ProviderRegion::new(
1955 region_name("aws-us-east-1"),
1956 "aws",
1957 "us-east-1",
1958 "custom-config".to_string(),
1959 )],
1960 vec![],
1961 )
1962 .expect("valid configuration");
1963
1964 assert_eq!(
1965 config.preferred_region_config(),
1966 Some(&"custom-config".to_string())
1967 );
1968 }
1969
1970 #[test]
1971 fn preferred_region_config_selects_correct_region() {
1972 let config = MultiCloudMultiRegionConfiguration::<String, ()>::new(
1973 region_name("gcp-europe-west1"),
1974 vec![
1975 ProviderRegion::new(
1976 region_name("aws-us-east-1"),
1977 "aws",
1978 "us-east-1",
1979 "aws-config".to_string(),
1980 ),
1981 ProviderRegion::new(
1982 region_name("gcp-europe-west1"),
1983 "gcp",
1984 "europe-west1",
1985 "gcp-config".to_string(),
1986 ),
1987 ],
1988 vec![],
1989 )
1990 .expect("valid configuration");
1991
1992 assert_eq!(
1993 config.preferred_region_config(),
1994 Some(&"gcp-config".to_string())
1995 );
1996 }
1997
1998 #[test]
1999 fn provider_region_cast() {
2000 let region = ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", 42i32);
2001 let casted = region.cast(|n| n.to_string());
2002 assert_eq!(casted.name(), ®ion_name("aws-us-east-1"));
2003 assert_eq!(casted.provider(), "aws");
2004 assert_eq!(casted.region(), "us-east-1");
2005 assert_eq!(casted.config(), "42");
2006 }
2007
2008 #[test]
2009 fn provider_region_try_cast_success() {
2010 let region = ProviderRegion::new(
2011 region_name("aws-us-east-1"),
2012 "aws",
2013 "us-east-1",
2014 "42".to_string(),
2015 );
2016 let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
2017 region.try_cast(|s| s.parse());
2018 let casted = result.expect("parsing should succeed");
2019 assert_eq!(casted.config(), &42);
2020 }
2021
2022 #[test]
2023 fn provider_region_try_cast_failure() {
2024 let region = ProviderRegion::new(
2025 region_name("aws-us-east-1"),
2026 "aws",
2027 "us-east-1",
2028 "not-a-number".to_string(),
2029 );
2030 let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
2031 region.try_cast(|s| s.parse());
2032 assert!(result.is_err());
2033 println!(
2034 "provider_region_try_cast_failure error: {:?}",
2035 result.unwrap_err()
2036 );
2037 }
2038
2039 #[test]
2040 fn topology_cast() {
2041 let t = Topology::new(
2042 topology_name("global"),
2043 vec![region_name("aws-us-east-1")],
2044 100i32,
2045 );
2046 let casted = t.cast(|n| n * 2);
2047 assert_eq!(casted.name(), &topology_name("global"));
2048 assert_eq!(casted.regions(), &[region_name("aws-us-east-1")]);
2049 assert_eq!(casted.config(), &200);
2050 }
2051
2052 #[test]
2053 fn topology_try_cast_success() {
2054 let t = Topology::new(
2055 topology_name("global"),
2056 vec![region_name("aws-us-east-1")],
2057 "123".to_string(),
2058 );
2059 let result: Result<Topology<i32>, std::num::ParseIntError> = t.try_cast(|s| s.parse());
2060 let casted = result.expect("parsing should succeed");
2061 assert_eq!(casted.config(), &123);
2062 }
2063
2064 #[test]
2065 fn topology_try_cast_failure() {
2066 let t = Topology::new(
2067 topology_name("global"),
2068 vec![region_name("aws-us-east-1")],
2069 "invalid".to_string(),
2070 );
2071 let result: Result<Topology<i32>, std::num::ParseIntError> = t.try_cast(|s| s.parse());
2072 assert!(result.is_err());
2073 println!("topology_try_cast_failure error: {:?}", result.unwrap_err());
2074 }
2075
2076 #[test]
2077 fn configuration_cast() {
2078 let config = MultiCloudMultiRegionConfiguration::<i32, i32>::new(
2079 region_name("aws-us-east-1"),
2080 vec![
2081 ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", 10),
2082 ProviderRegion::new(region_name("gcp-europe-west1"), "gcp", "europe-west1", 20),
2083 ],
2084 vec![Topology::new(
2085 topology_name("global"),
2086 vec![
2087 region_name("aws-us-east-1"),
2088 region_name("gcp-europe-west1"),
2089 ],
2090 100,
2091 )],
2092 )
2093 .expect("valid configuration");
2094
2095 let casted = config.cast(|r| r.to_string(), |t| t * 2);
2096
2097 assert_eq!(casted.preferred(), ®ion_name("aws-us-east-1"));
2098 assert_eq!(casted.regions().len(), 2);
2099 assert_eq!(casted.regions()[0].config(), "10");
2100 assert_eq!(casted.regions()[1].config(), "20");
2101 assert_eq!(casted.topologies().len(), 1);
2102 assert_eq!(casted.topologies()[0].config(), &200);
2103 }
2104
2105 #[test]
2106 fn configuration_try_cast_success() {
2107 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2108 region_name("aws-us-east-1"),
2109 vec![ProviderRegion::new(
2110 region_name("aws-us-east-1"),
2111 "aws",
2112 "us-east-1",
2113 "42".to_string(),
2114 )],
2115 vec![Topology::new(
2116 topology_name("global"),
2117 vec![region_name("aws-us-east-1")],
2118 "100".to_string(),
2119 )],
2120 )
2121 .expect("valid configuration");
2122
2123 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2124 config.try_cast(|r| r.parse(), |t| t.parse());
2125
2126 let casted = result.expect("parsing should succeed");
2127 assert_eq!(casted.preferred_region_config(), Some(&42));
2128 assert_eq!(casted.topologies()[0].config(), &100);
2129 }
2130
2131 #[test]
2132 fn configuration_try_cast_region_failure() {
2133 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2134 region_name("aws-us-east-1"),
2135 vec![ProviderRegion::new(
2136 region_name("aws-us-east-1"),
2137 "aws",
2138 "us-east-1",
2139 "not-a-number".to_string(),
2140 )],
2141 vec![Topology::new(
2142 topology_name("global"),
2143 vec![region_name("aws-us-east-1")],
2144 "100".to_string(),
2145 )],
2146 )
2147 .expect("valid configuration");
2148
2149 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2150 config.try_cast(|r| r.parse(), |t| t.parse());
2151
2152 assert!(result.is_err());
2153 println!(
2154 "configuration_try_cast_region_failure error: {:?}",
2155 result.unwrap_err()
2156 );
2157 }
2158
2159 #[test]
2160 fn configuration_try_cast_topology_failure() {
2161 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2162 region_name("aws-us-east-1"),
2163 vec![ProviderRegion::new(
2164 region_name("aws-us-east-1"),
2165 "aws",
2166 "us-east-1",
2167 "42".to_string(),
2168 )],
2169 vec![Topology::new(
2170 topology_name("global"),
2171 vec![region_name("aws-us-east-1")],
2172 "not-a-number".to_string(),
2173 )],
2174 )
2175 .expect("valid configuration");
2176
2177 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2178 config.try_cast(|r| r.parse(), |t| t.parse());
2179
2180 assert!(result.is_err());
2181 println!(
2182 "configuration_try_cast_topology_failure error: {:?}",
2183 result.unwrap_err()
2184 );
2185 }
2186
2187 #[tokio::test]
2188 async fn provider_region_try_cast_async_success() {
2189 let region = ProviderRegion::new(
2190 region_name("aws-us-east-1"),
2191 "aws",
2192 "us-east-1",
2193 "42".to_string(),
2194 );
2195 let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
2196 region.try_cast_async(|s| async move { s.parse() }).await;
2197 let casted = result.expect("parsing should succeed");
2198 assert_eq!(casted.name(), ®ion_name("aws-us-east-1"));
2199 assert_eq!(casted.provider(), "aws");
2200 assert_eq!(casted.region(), "us-east-1");
2201 assert_eq!(casted.config(), &42);
2202 }
2203
2204 #[tokio::test]
2205 async fn provider_region_try_cast_async_failure() {
2206 let region = ProviderRegion::new(
2207 region_name("aws-us-east-1"),
2208 "aws",
2209 "us-east-1",
2210 "not-a-number".to_string(),
2211 );
2212 let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
2213 region.try_cast_async(|s| async move { s.parse() }).await;
2214 assert!(result.is_err());
2215 println!(
2216 "provider_region_try_cast_async_failure error: {:?}",
2217 result.unwrap_err()
2218 );
2219 }
2220
2221 #[tokio::test]
2222 async fn topology_try_cast_async_success() {
2223 let t = Topology::new(
2224 topology_name("global"),
2225 vec![region_name("aws-us-east-1")],
2226 "123".to_string(),
2227 );
2228 let result: Result<Topology<i32>, std::num::ParseIntError> =
2229 t.try_cast_async(|s| async move { s.parse() }).await;
2230 let casted = result.expect("parsing should succeed");
2231 assert_eq!(casted.name(), &topology_name("global"));
2232 assert_eq!(casted.regions(), &[region_name("aws-us-east-1")]);
2233 assert_eq!(casted.config(), &123);
2234 }
2235
2236 #[tokio::test]
2237 async fn topology_try_cast_async_failure() {
2238 let t = Topology::new(
2239 topology_name("global"),
2240 vec![region_name("aws-us-east-1")],
2241 "invalid".to_string(),
2242 );
2243 let result: Result<Topology<i32>, std::num::ParseIntError> =
2244 t.try_cast_async(|s| async move { s.parse() }).await;
2245 assert!(result.is_err());
2246 println!(
2247 "topology_try_cast_async_failure error: {:?}",
2248 result.unwrap_err()
2249 );
2250 }
2251
2252 #[tokio::test]
2253 async fn configuration_try_cast_async_success() {
2254 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2255 region_name("aws-us-east-1"),
2256 vec![ProviderRegion::new(
2257 region_name("aws-us-east-1"),
2258 "aws",
2259 "us-east-1",
2260 "42".to_string(),
2261 )],
2262 vec![Topology::new(
2263 topology_name("global"),
2264 vec![region_name("aws-us-east-1")],
2265 "100".to_string(),
2266 )],
2267 )
2268 .expect("valid configuration");
2269
2270 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2271 config
2272 .try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
2273 .await;
2274
2275 let casted = result.expect("parsing should succeed");
2276 assert_eq!(casted.preferred_region_config(), Some(&42));
2277 assert_eq!(casted.topologies()[0].config(), &100);
2278 }
2279
2280 #[tokio::test]
2281 async fn configuration_try_cast_async_region_failure() {
2282 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2283 region_name("aws-us-east-1"),
2284 vec![ProviderRegion::new(
2285 region_name("aws-us-east-1"),
2286 "aws",
2287 "us-east-1",
2288 "not-a-number".to_string(),
2289 )],
2290 vec![Topology::new(
2291 topology_name("global"),
2292 vec![region_name("aws-us-east-1")],
2293 "100".to_string(),
2294 )],
2295 )
2296 .expect("valid configuration");
2297
2298 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2299 config
2300 .try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
2301 .await;
2302
2303 assert!(result.is_err());
2304 println!(
2305 "configuration_try_cast_async_region_failure error: {:?}",
2306 result.unwrap_err()
2307 );
2308 }
2309
2310 #[tokio::test]
2311 async fn configuration_try_cast_async_topology_failure() {
2312 let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
2313 region_name("aws-us-east-1"),
2314 vec![ProviderRegion::new(
2315 region_name("aws-us-east-1"),
2316 "aws",
2317 "us-east-1",
2318 "42".to_string(),
2319 )],
2320 vec![Topology::new(
2321 topology_name("global"),
2322 vec![region_name("aws-us-east-1")],
2323 "not-a-number".to_string(),
2324 )],
2325 )
2326 .expect("valid configuration");
2327
2328 let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
2329 config
2330 .try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
2331 .await;
2332
2333 assert!(result.is_err());
2334 println!(
2335 "configuration_try_cast_async_topology_failure error: {:?}",
2336 result.unwrap_err()
2337 );
2338 }
2339}