use std::collections::HashSet;
use std::fmt::Debug;
use std::future::Future;
use std::hash::Hash;
use chroma_error::ChromaError;
use chroma_error::ErrorCodes;
use serde::Deserialize;
use serde::Serialize;
use thiserror::Error;
const MAX_NAME_LENGTH: usize = 32;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
pub enum NameError {
#[error("name cannot be empty")]
Empty,
#[error("name exceeds maximum length of {MAX_NAME_LENGTH} characters: {0} characters")]
TooLong(usize),
#[error("name contains non-ASCII characters")]
NonAscii,
}
fn validate_name(name: &str) -> Result<(), NameError> {
if name.is_empty() {
return Err(NameError::Empty);
}
if !name.is_ascii() {
return Err(NameError::NonAscii);
}
if name.len() > MAX_NAME_LENGTH {
return Err(NameError::TooLong(name.len()));
}
Ok(())
}
fn find_duplicates<'a, T, K, F>(items: &'a [T], key_fn: F) -> Vec<K>
where
K: 'a + Clone + Eq + Hash + Ord,
F: Fn(&'a T) -> &'a K,
{
let mut seen = HashSet::new();
let mut duplicates: Vec<_> = items
.iter()
.filter_map(|item| {
let key = key_fn(item);
if !seen.insert(key) {
Some(key.clone())
} else {
None
}
})
.collect();
duplicates.sort();
duplicates.dedup();
duplicates
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
#[serde(transparent)]
pub struct RegionName(String);
impl RegionName {
pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
let name = name.into();
validate_name(&name)?;
Ok(Self(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for RegionName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
RegionName::new(s).map_err(serde::de::Error::custom)
}
}
impl std::fmt::Display for RegionName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
#[serde(transparent)]
pub struct TopologyName(String);
impl TopologyName {
pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
let name = name.into();
validate_name(&name)?;
Ok(Self(name))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for TopologyName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
TopologyName::new(s).map_err(serde::de::Error::custom)
}
}
impl std::fmt::Display for TopologyName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(bound(
serialize = "T: Clone + Debug + Serialize",
deserialize = "T: Clone + Debug + serde::de::DeserializeOwned"
))]
pub struct ProviderRegion<T: Clone + Debug> {
pub name: RegionName,
pub provider: String,
pub region: String,
pub config: T,
}
impl<T: Clone + Debug> ProviderRegion<T> {
pub fn new(
name: RegionName,
provider: impl Into<String>,
region: impl Into<String>,
config: T,
) -> Self {
Self {
name,
provider: provider.into(),
region: region.into(),
config,
}
}
pub fn name(&self) -> &RegionName {
&self.name
}
pub fn provider(&self) -> &str {
&self.provider
}
pub fn region(&self) -> &str {
&self.region
}
pub fn config(&self) -> &T {
&self.config
}
pub fn cast<U, F>(self, f: F) -> ProviderRegion<U>
where
U: Clone + Debug,
F: FnOnce(T) -> U,
{
ProviderRegion {
name: self.name,
provider: self.provider,
region: self.region,
config: f(self.config),
}
}
pub fn try_cast<U, E, F>(self, f: F) -> Result<ProviderRegion<U>, E>
where
U: Clone + Debug,
F: FnOnce(T) -> Result<U, E>,
{
Ok(ProviderRegion {
name: self.name,
provider: self.provider,
region: self.region,
config: f(self.config)?,
})
}
pub async fn try_cast_async<U, E, F, R>(self, f: F) -> Result<ProviderRegion<U>, E>
where
U: Clone + Debug,
F: FnOnce(T) -> R,
R: Future<Output = Result<U, E>> + Send,
{
Ok(ProviderRegion {
name: self.name,
provider: self.provider,
region: self.region,
config: f(self.config).await?,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(bound(
serialize = "T: Clone + Debug + Serialize",
deserialize = "T: Clone + Debug + serde::de::DeserializeOwned"
))]
pub struct Topology<T: Clone + Debug> {
pub name: TopologyName,
regions: Vec<RegionName>,
pub config: T,
}
impl<T: Clone + Debug> Topology<T> {
pub fn new(name: TopologyName, regions: Vec<RegionName>, config: T) -> Self {
Self {
name,
regions,
config,
}
}
pub fn name(&self) -> &TopologyName {
&self.name
}
pub fn regions(&self) -> &[RegionName] {
&self.regions
}
pub fn config(&self) -> &T {
&self.config
}
pub fn cast<U, F>(self, f: F) -> Topology<U>
where
U: Clone + Debug,
F: FnOnce(T) -> U,
{
Topology {
name: self.name,
regions: self.regions,
config: f(self.config),
}
}
pub fn try_cast<U, E, F>(self, f: F) -> Result<Topology<U>, E>
where
U: Clone + Debug,
F: FnOnce(T) -> Result<U, E>,
{
Ok(Topology {
name: self.name,
regions: self.regions,
config: f(self.config)?,
})
}
pub async fn try_cast_async<U, E, F, R>(self, f: F) -> Result<Topology<U>, E>
where
U: Clone + Debug,
F: FnOnce(T) -> R,
R: Future<Output = Result<U, E>> + Send,
{
Ok(Topology {
name: self.name,
regions: self.regions,
config: f(self.config).await?,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(
into = "RawMultiCloudMultiRegionConfiguration<R, T>",
bound(serialize = "R: Clone + Debug + Serialize, T: Clone + Debug + Serialize")
)]
pub struct MultiCloudMultiRegionConfiguration<R: Clone + Debug, T: Clone + Debug> {
pub preferred: RegionName,
pub regions: Vec<ProviderRegion<R>>,
pub topologies: Vec<Topology<T>>,
}
impl<R: Clone + Debug, T: Clone + Debug> MultiCloudMultiRegionConfiguration<R, T> {
pub fn preferred_region(&self) -> Option<&ProviderRegion<R>> {
self.regions.iter().find(|pr| pr.name() == &self.preferred)
}
pub fn lookup_region(&self, name: &RegionName) -> Option<ProviderRegion<R>> {
self.regions.iter().find(|pr| pr.name() == name).cloned()
}
pub fn lookup_topology(
&self,
name: &TopologyName,
) -> Option<(Vec<ProviderRegion<R>>, Topology<T>)> {
let t = self.topologies.iter().find(|t| &t.name == name).cloned()?;
let mut regions = vec![];
for r in t.regions.iter() {
regions.push(self.lookup_region(r)?)
}
Some((regions, t))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(bound(
serialize = "R: Clone + Debug + Serialize, T: Clone + Debug + Serialize",
deserialize = "R: Clone + Debug + serde::de::DeserializeOwned, T: Clone + Debug + serde::de::DeserializeOwned",
))]
struct RawMultiCloudMultiRegionConfiguration<R: Clone + Debug, T: Clone + Debug> {
preferred: RegionName,
regions: Vec<ProviderRegion<R>>,
topologies: Vec<Topology<T>>,
}
impl<R: Clone + Debug, T: Clone + Debug> From<MultiCloudMultiRegionConfiguration<R, T>>
for RawMultiCloudMultiRegionConfiguration<R, T>
{
fn from(config: MultiCloudMultiRegionConfiguration<R, T>) -> Self {
Self {
preferred: config.preferred,
regions: config.regions,
topologies: config.topologies,
}
}
}
impl<R: Clone + Debug, T: Clone + Debug> TryFrom<RawMultiCloudMultiRegionConfiguration<R, T>>
for MultiCloudMultiRegionConfiguration<R, T>
{
type Error = ValidationError;
fn try_from(raw: RawMultiCloudMultiRegionConfiguration<R, T>) -> Result<Self, Self::Error> {
MultiCloudMultiRegionConfiguration::new(raw.preferred, raw.regions, raw.topologies)
}
}
impl<
'de,
R: Clone + Debug + Serialize + serde::de::DeserializeOwned,
T: Clone + Debug + Serialize + serde::de::DeserializeOwned,
> Deserialize<'de> for MultiCloudMultiRegionConfiguration<R, T>
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawMultiCloudMultiRegionConfiguration::<R, T>::deserialize(deserializer)?;
MultiCloudMultiRegionConfiguration::try_from(raw).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, Default, Eq, Error, PartialEq)]
#[error("{}", self.format_message())]
pub struct ValidationError {
duplicate_region_names: Vec<RegionName>,
duplicate_topology_names: Vec<TopologyName>,
unknown_topology_regions: Vec<RegionName>,
unknown_preferred_region: Option<RegionName>,
}
impl ChromaError for ValidationError {
fn code(&self) -> ErrorCodes {
ErrorCodes::InvalidArgument
}
}
impl ValidationError {
#[cfg(test)]
fn new(
duplicate_region_names: Vec<RegionName>,
duplicate_topology_names: Vec<TopologyName>,
unknown_topology_regions: Vec<RegionName>,
unknown_preferred_region: Option<RegionName>,
) -> Self {
Self {
duplicate_region_names,
duplicate_topology_names,
unknown_topology_regions,
unknown_preferred_region,
}
}
pub fn has_errors(&self) -> bool {
!self.duplicate_region_names.is_empty()
|| !self.duplicate_topology_names.is_empty()
|| !self.unknown_topology_regions.is_empty()
|| self.unknown_preferred_region.is_some()
}
pub fn duplicate_region_names(&self) -> &[RegionName] {
&self.duplicate_region_names
}
pub fn duplicate_topology_names(&self) -> &[TopologyName] {
&self.duplicate_topology_names
}
pub fn unknown_topology_regions(&self) -> &[RegionName] {
&self.unknown_topology_regions
}
pub fn unknown_preferred_region(&self) -> Option<&RegionName> {
self.unknown_preferred_region.as_ref()
}
fn format_message(&self) -> String {
if !self.has_errors() {
return "no validation errors".to_string();
}
let mut parts = Vec::new();
if !self.duplicate_region_names.is_empty() {
parts.push(format!(
"duplicate region names: {}",
format_name_list(&self.duplicate_region_names)
));
}
if !self.duplicate_topology_names.is_empty() {
parts.push(format!(
"duplicate topology names: {}",
format_name_list(&self.duplicate_topology_names)
));
}
if !self.unknown_topology_regions.is_empty() {
parts.push(format!(
"unknown topology regions: {}",
format_name_list(&self.unknown_topology_regions)
));
}
if let Some(ref name) = self.unknown_preferred_region {
parts.push(format!("unknown preferred region: {}", name));
}
parts.join("; ")
}
}
fn format_name_list<T: std::fmt::Display>(names: &[T]) -> String {
names
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ")
}
impl<R: Clone + Debug, T: Clone + Debug> MultiCloudMultiRegionConfiguration<R, T> {
pub fn new(
preferred: RegionName,
regions: Vec<ProviderRegion<R>>,
topologies: Vec<Topology<T>>,
) -> Result<Self, ValidationError> {
let config = Self {
preferred,
regions,
topologies,
};
config.validate()?;
Ok(config)
}
pub fn preferred(&self) -> &RegionName {
&self.preferred
}
pub fn regions(&self) -> &[ProviderRegion<R>] {
&self.regions
}
pub fn topologies(&self) -> &[Topology<T>] {
&self.topologies
}
pub fn validate(&self) -> Result<(), ValidationError> {
let mut error = ValidationError::default();
let all_region_names: HashSet<_> = self.regions.iter().map(|r| &r.name).collect();
error.duplicate_region_names = find_duplicates(&self.regions, |r| &r.name);
error.duplicate_topology_names = find_duplicates(&self.topologies, |t| &t.name);
let mut unknown_regions: Vec<_> = self
.topologies
.iter()
.flat_map(|t| &t.regions)
.filter(|r| !all_region_names.contains(r))
.cloned()
.collect();
unknown_regions.sort();
unknown_regions.dedup();
error.unknown_topology_regions = unknown_regions;
if !all_region_names.contains(&self.preferred) {
error.unknown_preferred_region = Some(self.preferred.clone());
}
if error.has_errors() {
Err(error)
} else {
Ok(())
}
}
pub fn preferred_region_config(&self) -> Option<&R> {
self.regions
.iter()
.find(|r| r.name == self.preferred)
.map(|r| r.config())
}
pub fn cast<R2, T2, FR, FT>(
self,
region_fn: FR,
topology_fn: FT,
) -> MultiCloudMultiRegionConfiguration<R2, T2>
where
R2: Clone + Debug,
T2: Clone + Debug,
FR: Fn(R) -> R2,
FT: Fn(T) -> T2,
{
MultiCloudMultiRegionConfiguration {
preferred: self.preferred,
regions: self
.regions
.into_iter()
.map(|r| r.cast(®ion_fn))
.collect(),
topologies: self
.topologies
.into_iter()
.map(|t| t.cast(&topology_fn))
.collect(),
}
}
pub fn try_cast<R2, T2, E, FR, FT>(
self,
region_fn: FR,
topology_fn: FT,
) -> Result<MultiCloudMultiRegionConfiguration<R2, T2>, E>
where
R2: Clone + Debug,
T2: Clone + Debug,
FR: Fn(R) -> Result<R2, E>,
FT: Fn(T) -> Result<T2, E>,
{
let regions: Result<Vec<_>, E> = self
.regions
.into_iter()
.map(|r| r.try_cast(®ion_fn))
.collect();
let topologies: Result<Vec<_>, E> = self
.topologies
.into_iter()
.map(|t| t.try_cast(&topology_fn))
.collect();
Ok(MultiCloudMultiRegionConfiguration {
preferred: self.preferred,
regions: regions?,
topologies: topologies?,
})
}
pub async fn try_cast_async<R2, T2, E, FR, FT, FUT1, FUT2>(
self,
region_fn: FR,
topology_fn: FT,
) -> Result<MultiCloudMultiRegionConfiguration<R2, T2>, E>
where
R2: Clone + Debug,
T2: Clone + Debug,
FR: Fn(R) -> FUT1,
FT: Fn(T) -> FUT2,
FUT1: Future<Output = Result<R2, E>> + Send,
FUT2: Future<Output = Result<T2, E>> + Send,
{
let mut regions = Vec::with_capacity(self.regions.len());
for region in self.regions.into_iter() {
regions.push(region.try_cast_async(®ion_fn).await?);
}
let mut topologies = Vec::with_capacity(self.topologies.len());
for topo in self.topologies.into_iter() {
topologies.push(topo.try_cast_async(&topology_fn).await?);
}
Ok(MultiCloudMultiRegionConfiguration {
preferred: self.preferred,
regions,
topologies,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn region_name(s: impl Into<String>) -> RegionName {
RegionName::new(s).expect("test region name should be valid")
}
fn topology_name(s: impl Into<String>) -> TopologyName {
TopologyName::new(s).expect("test topology name should be valid")
}
fn provider_region(
name: impl Into<String>,
provider: impl Into<String>,
region: impl Into<String>,
) -> ProviderRegion<()> {
ProviderRegion::new(
RegionName::new(name).expect("test region name should be valid"),
provider,
region,
(),
)
}
fn topology(name: impl Into<String>, regions: Vec<&str>) -> Topology<()> {
Topology::new(
TopologyName::new(name).expect("test topology name should be valid"),
regions
.into_iter()
.map(|s| RegionName::new(s).expect("test region name should be valid"))
.collect(),
(),
)
}
#[test]
fn region_name_as_str() {
let name = RegionName::new("aws-us-east-1").expect("valid name");
assert_eq!(name.as_str(), "aws-us-east-1");
}
#[test]
fn region_name_display() {
let name = RegionName::new("aws-us-east-1").expect("valid name");
assert_eq!(format!("{}", name), "aws-us-east-1");
}
#[test]
fn region_name_equality() {
let a = RegionName::new("aws-us-east-1");
let b = RegionName::new("aws-us-east-1");
let c = RegionName::new("gcp-europe-west1");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn region_name_clone() {
let a = RegionName::new("aws-us-east-1");
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn region_name_serde_roundtrip() {
let name = RegionName::new("aws-us-east-1").expect("valid name");
let json = serde_json::to_string(&name).unwrap();
assert_eq!(json, "\"aws-us-east-1\"");
let deserialized: RegionName = serde_json::from_str(&json).unwrap();
assert_eq!(name, deserialized);
}
#[test]
fn topology_name_as_str() {
let name = TopologyName::new("global").expect("valid name");
assert_eq!(name.as_str(), "global");
}
#[test]
fn topology_name_display() {
let name = TopologyName::new("global").expect("valid name");
assert_eq!(format!("{}", name), "global");
}
#[test]
fn topology_name_equality() {
let a = TopologyName::new("global");
let b = TopologyName::new("global");
let c = TopologyName::new("regional");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn topology_name_clone() {
let a = TopologyName::new("global");
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn topology_name_serde_roundtrip() {
let name = TopologyName::new("global").expect("valid name");
let json = serde_json::to_string(&name).unwrap();
assert_eq!(json, "\"global\"");
let deserialized: TopologyName = serde_json::from_str(&json).unwrap();
assert_eq!(name, deserialized);
}
#[test]
fn provider_region_accessors() {
let region = ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", ());
assert_eq!(region.name(), ®ion_name("aws-us-east-1"));
assert_eq!(region.provider(), "aws");
assert_eq!(region.region(), "us-east-1");
}
#[test]
fn provider_region_equality() {
let a = provider_region("aws-us-east-1", "aws", "us-east-1");
let b = provider_region("aws-us-east-1", "aws", "us-east-1");
let c = provider_region("gcp-europe-west1", "gcp", "europe-west1");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn provider_region_clone() {
let a = provider_region("aws-us-east-1", "aws", "us-east-1");
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn provider_region_serde_roundtrip() {
let region = provider_region("aws-us-east-1", "aws", "us-east-1");
let json = serde_json::to_string(®ion).unwrap();
let deserialized: ProviderRegion<()> = serde_json::from_str(&json).unwrap();
assert_eq!(region, deserialized);
}
#[test]
fn topology_accessors() {
let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
assert_eq!(t.name(), &topology_name("global"));
assert_eq!(
t.regions(),
&[
region_name("aws-us-east-1"),
region_name("gcp-europe-west1")
]
);
}
#[test]
fn topology_equality() {
let a = topology("global", vec!["aws-us-east-1"]);
let b = topology("global", vec!["aws-us-east-1"]);
let c = topology("regional", vec!["aws-us-east-1"]);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn topology_clone() {
let a = topology("global", vec!["aws-us-east-1"]);
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn topology_serde_roundtrip() {
let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
let json = serde_json::to_string(&t).unwrap();
let deserialized: Topology<()> = serde_json::from_str(&json).unwrap();
assert_eq!(t, deserialized);
}
#[test]
fn valid_configuration() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![
provider_region("aws-us-east-1", "aws", "us-east-1"),
provider_region("gcp-europe-west1", "gcp", "europe-west1"),
],
vec![topology(
"global",
vec!["aws-us-east-1", "gcp-europe-west1"],
)],
);
assert!(config.is_ok(), "Expected valid configuration: {:?}", config);
}
#[test]
fn valid_configuration_accessors() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![topology("global", vec!["aws-us-east-1"])],
)
.expect("valid configuration");
assert_eq!(config.preferred(), ®ion_name("aws-us-east-1"));
assert_eq!(config.regions().len(), 1);
assert_eq!(config.topologies().len(), 1);
}
#[test]
fn configuration_serde_roundtrip() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![
provider_region("aws-us-east-1", "aws", "us-east-1"),
provider_region("gcp-europe-west1", "gcp", "europe-west1"),
],
vec![topology(
"global",
vec!["aws-us-east-1", "gcp-europe-west1"],
)],
)
.expect("valid configuration");
let json = serde_json::to_string(&config).unwrap();
let deserialized: MultiCloudMultiRegionConfiguration<(), ()> =
serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
}
#[test]
fn configuration_serde_roundtrip_with_complex_config() {
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
struct RegionConfig {
endpoint: String,
max_connections: u32,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
struct TopologyConfig {
replication_factor: u8,
consistency_level: String,
}
let region1 = ProviderRegion::new(
RegionName::new("aws-us-east-1").unwrap(),
"aws",
"us-east-1",
RegionConfig {
endpoint: "https://us-east-1.example.com".to_string(),
max_connections: 100,
},
);
let region2 = ProviderRegion::new(
RegionName::new("gcp-europe-west1").unwrap(),
"gcp",
"europe-west1",
RegionConfig {
endpoint: "https://europe-west1.example.com".to_string(),
max_connections: 50,
},
);
let topology = Topology::new(
TopologyName::new("global").unwrap(),
vec![
RegionName::new("aws-us-east-1").unwrap(),
RegionName::new("gcp-europe-west1").unwrap(),
],
TopologyConfig {
replication_factor: 3,
consistency_level: "quorum".to_string(),
},
);
let config: MultiCloudMultiRegionConfiguration<RegionConfig, TopologyConfig> =
MultiCloudMultiRegionConfiguration::new(
RegionName::new("aws-us-east-1").unwrap(),
vec![region1, region2],
vec![topology],
)
.expect("valid configuration");
let json = serde_json::to_string_pretty(&config).unwrap();
assert!(
json.contains("endpoint"),
"JSON should contain region config fields: {json}"
);
assert!(
json.contains("replication_factor"),
"JSON should contain topology config fields: {json}"
);
let deserialized: MultiCloudMultiRegionConfiguration<RegionConfig, TopologyConfig> =
serde_json::from_str(&json).unwrap();
assert_eq!(config, deserialized);
assert_eq!(
deserialized.regions()[0].config().endpoint,
"https://us-east-1.example.com"
);
assert_eq!(deserialized.regions()[0].config().max_connections, 100);
assert_eq!(deserialized.topologies()[0].config().replication_factor, 3);
assert_eq!(
deserialized.topologies()[0].config().consistency_level,
"quorum"
);
}
#[test]
fn configuration_deserialize_valid() {
let json = r#"{
"preferred": "aws-us-east-1",
"regions": [
{"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null},
{"name": "gcp-europe-west1", "provider": "gcp", "region": "europe-west1", "config": null}
],
"topologies": [
{"name": "global", "regions": ["aws-us-east-1", "gcp-europe-west1"], "config": null}
]
}"#;
let config: MultiCloudMultiRegionConfiguration<(), ()> =
serde_json::from_str(json).unwrap();
assert_eq!(config.preferred().as_str(), "aws-us-east-1");
assert_eq!(config.topologies().len(), 1);
assert_eq!(config.topologies()[0].name().as_str(), "global");
assert_eq!(config.topologies()[0].regions().len(), 2);
}
#[test]
fn configuration_deserialize_invalid_preferred() {
let json = r#"{
"preferred": "nonexistent",
"regions": [
{"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
],
"topologies": []
}"#;
let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("unknown preferred region"),
"Expected error message to contain 'unknown preferred region', got: {}",
err_msg
);
}
#[test]
fn configuration_deserialize_duplicate_regions() {
let json = r#"{
"preferred": "aws-us-east-1",
"regions": [
{"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null},
{"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
],
"topologies": []
}"#;
let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("duplicate region names"),
"Expected error message to contain 'duplicate region names', got: {}",
err_msg
);
}
#[test]
fn configuration_deserialize_unknown_topology_region() {
let json = r#"{
"preferred": "aws-us-east-1",
"regions": [
{"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
],
"topologies": [
{"name": "global", "regions": ["aws-us-east-1", "nonexistent"], "config": null}
]
}"#;
let result: Result<MultiCloudMultiRegionConfiguration<(), ()>, _> =
serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("unknown topology regions"),
"Expected error message to contain 'unknown topology regions', got: {}",
err_msg
);
}
#[test]
fn empty_configuration() {
let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
region_name("nonexistent"),
vec![],
vec![],
);
let err = config.unwrap_err();
assert!(err.duplicate_region_names().is_empty());
assert!(err.duplicate_topology_names().is_empty());
assert!(err.unknown_topology_regions().is_empty());
assert_eq!(
err.unknown_preferred_region(),
Some(®ion_name("nonexistent"))
);
}
#[test]
fn empty_topology_regions() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![topology("empty", vec![])],
);
assert!(
config.is_ok(),
"Topology with no regions should be valid: {:?}",
config
);
}
#[test]
fn duplicate_region_names() {
let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
region_name("aws-us-east-1"),
vec![
provider_region("aws-us-east-1", "aws", "us-east-1"),
provider_region("aws-us-east-1", "aws", "us-east-1"),
],
vec![],
);
let err = config.unwrap_err();
assert_eq!(
err.duplicate_region_names(),
&[region_name("aws-us-east-1")]
);
assert!(err.duplicate_topology_names().is_empty());
assert!(err.unknown_topology_regions().is_empty());
assert_eq!(err.unknown_preferred_region(), None);
}
#[test]
fn duplicate_topology_names() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![
topology("global", vec!["aws-us-east-1"]),
topology("global", vec!["aws-us-east-1"]),
],
);
let err = config.unwrap_err();
assert!(err.duplicate_region_names().is_empty());
assert_eq!(err.duplicate_topology_names(), &[topology_name("global")]);
assert!(err.unknown_topology_regions().is_empty());
assert_eq!(err.unknown_preferred_region(), None);
}
#[test]
fn unknown_topology_region() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![topology(
"global",
vec!["aws-us-east-1", "nonexistent-region"],
)],
);
let err = config.unwrap_err();
assert!(err.duplicate_region_names().is_empty());
assert!(err.duplicate_topology_names().is_empty());
assert_eq!(
err.unknown_topology_regions(),
&[region_name("nonexistent-region")]
);
assert_eq!(err.unknown_preferred_region(), None);
}
#[test]
fn unknown_topology_region_duplicated_in_multiple_topologies() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![
topology("topo1", vec!["aws-us-east-1", "nonexistent"]),
topology("topo2", vec!["nonexistent"]),
],
);
let err = config.unwrap_err();
assert!(err.duplicate_region_names().is_empty());
assert!(err.duplicate_topology_names().is_empty());
assert_eq!(
err.unknown_topology_regions(),
&[region_name("nonexistent")]
);
assert_eq!(err.unknown_preferred_region(), None);
}
#[test]
fn unknown_preferred_region() {
let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
region_name("nonexistent-region"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![],
);
let err = config.unwrap_err();
assert!(err.duplicate_region_names().is_empty());
assert!(err.duplicate_topology_names().is_empty());
assert!(err.unknown_topology_regions().is_empty());
assert_eq!(
err.unknown_preferred_region(),
Some(®ion_name("nonexistent-region"))
);
}
#[test]
fn multiple_validation_errors() {
let config = MultiCloudMultiRegionConfiguration::new(
region_name("nonexistent-preferred"),
vec![
provider_region("aws-us-east-1", "aws", "us-east-1"),
provider_region("aws-us-east-1", "aws", "us-east-1"),
],
vec![
topology("topo1", vec!["unknown-region"]),
topology("topo1", vec!["aws-us-east-1"]),
],
);
let err = config.unwrap_err();
assert_eq!(
err.duplicate_region_names(),
&[region_name("aws-us-east-1")]
);
assert_eq!(err.duplicate_topology_names(), &[topology_name("topo1")]);
assert_eq!(
err.unknown_topology_regions(),
&[region_name("unknown-region")]
);
assert_eq!(
err.unknown_preferred_region(),
Some(®ion_name("nonexistent-preferred"))
);
}
#[test]
fn display_no_errors() {
let error = ValidationError::default();
assert_eq!(error.to_string(), "no validation errors");
}
#[test]
fn display_duplicate_region_names_only() {
let error = ValidationError::new(
vec![region_name("region-a"), region_name("region-b")],
vec![],
vec![],
None,
);
assert_eq!(
error.to_string(),
"duplicate region names: region-a, region-b"
);
}
#[test]
fn display_duplicate_topology_names_only() {
let error = ValidationError::new(vec![], vec![topology_name("topo-x")], vec![], None);
assert_eq!(error.to_string(), "duplicate topology names: topo-x");
}
#[test]
fn display_unknown_topology_regions_only() {
let error = ValidationError::new(
vec![],
vec![],
vec![region_name("missing-1"), region_name("missing-2")],
None,
);
assert_eq!(
error.to_string(),
"unknown topology regions: missing-1, missing-2"
);
}
#[test]
fn display_unknown_preferred_region_only() {
let error =
ValidationError::new(vec![], vec![], vec![], Some(region_name("missing-region")));
assert_eq!(
error.to_string(),
"unknown preferred region: missing-region"
);
}
#[test]
fn display_all_errors() {
let error = ValidationError::new(
vec![region_name("dup-region")],
vec![topology_name("dup-topo")],
vec![region_name("unknown-reg")],
Some(region_name("bad-preferred")),
);
assert_eq!(
error.to_string(),
"duplicate region names: dup-region; duplicate topology names: dup-topo; unknown topology regions: unknown-reg; unknown preferred region: bad-preferred"
);
}
#[test]
fn display_special_characters() {
let error = ValidationError::new(
vec![
region_name("region-with-dash_and_underscore"),
region_name("region with spaces"),
],
vec![topology_name("topo.dot")],
vec![region_name("region\nwith\nnewlines")],
None,
);
assert_eq!(
error.to_string(),
"duplicate region names: region-with-dash_and_underscore, region with spaces; duplicate topology names: topo.dot; unknown topology regions: region\nwith\nnewlines"
);
}
#[test]
fn validation_error_has_errors_default() {
let error = ValidationError::default();
assert!(!error.has_errors());
}
#[test]
fn validation_error_has_errors_with_duplicate_regions() {
let error = ValidationError::new(vec![region_name("dup")], vec![], vec![], None);
assert!(error.has_errors());
}
#[test]
fn validation_error_has_errors_with_duplicate_topologies() {
let error = ValidationError::new(vec![], vec![topology_name("dup")], vec![], None);
assert!(error.has_errors());
}
#[test]
fn validation_error_has_errors_with_unknown_topology_regions() {
let error = ValidationError::new(vec![], vec![], vec![region_name("unknown")], None);
assert!(error.has_errors());
}
#[test]
fn validation_error_has_errors_with_unknown_preferred() {
let error = ValidationError::new(vec![], vec![], vec![], Some(region_name("unknown")));
assert!(error.has_errors());
}
#[test]
fn configuration_clone() {
let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![],
)
.expect("valid configuration");
let cloned = config.clone();
assert_eq!(config, cloned);
}
#[test]
fn configuration_debug() {
let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
region_name("aws-us-east-1"),
vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
vec![],
)
.expect("valid configuration");
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("MultiCloudMultiRegionConfiguration"));
assert!(debug_str.contains("aws-us-east-1"));
}
#[test]
fn region_name_valid() {
assert!(RegionName::new("aws-us-east-1").is_ok());
assert!(RegionName::new("a").is_ok());
assert!(RegionName::new("12345678901234567890123456789012").is_ok());
}
#[test]
fn region_name_empty() {
let result = RegionName::new("");
assert!(result.is_err());
println!(
"region_name_empty error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::Empty)));
}
#[test]
fn region_name_too_long() {
let result = RegionName::new("123456789012345678901234567890123");
assert!(result.is_err());
println!(
"region_name_too_long error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::TooLong(33))));
}
#[test]
fn region_name_non_ascii() {
let result = RegionName::new("region-🌍");
assert!(result.is_err());
println!(
"region_name_non_ascii error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::NonAscii)));
}
#[test]
fn topology_name_valid() {
assert!(TopologyName::new("global").is_ok());
assert!(TopologyName::new("a").is_ok());
assert!(TopologyName::new("12345678901234567890123456789012").is_ok());
}
#[test]
fn topology_name_empty() {
let result = TopologyName::new("");
assert!(result.is_err());
println!(
"topology_name_empty error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::Empty)));
}
#[test]
fn topology_name_too_long() {
let result = TopologyName::new("123456789012345678901234567890123");
assert!(result.is_err());
println!(
"topology_name_too_long error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::TooLong(33))));
}
#[test]
fn topology_name_non_ascii() {
let result = TopologyName::new("拓扑名");
assert!(result.is_err());
println!(
"topology_name_non_ascii error: {:?}",
result.as_ref().unwrap_err()
);
assert!(matches!(result, Err(NameError::NonAscii)));
}
#[test]
fn name_error_display_empty() {
let err = NameError::Empty;
assert_eq!(err.to_string(), "name cannot be empty");
}
#[test]
fn name_error_display_too_long() {
let err = NameError::TooLong(50);
assert_eq!(
err.to_string(),
"name exceeds maximum length of 32 characters: 50 characters"
);
}
#[test]
fn name_error_display_non_ascii() {
let err = NameError::NonAscii;
assert_eq!(err.to_string(), "name contains non-ASCII characters");
}
#[test]
fn region_name_deserialize_valid() {
let json = "\"aws-us-east-1\"";
let name: RegionName = serde_json::from_str(json).unwrap();
assert_eq!(name.as_str(), "aws-us-east-1");
}
#[test]
fn region_name_deserialize_empty() {
let json = "\"\"";
let result: Result<RegionName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("region_name_deserialize_empty error: {}", err_msg);
assert!(
err_msg.contains("name cannot be empty"),
"Expected error message to contain 'name cannot be empty', got: {}",
err_msg
);
}
#[test]
fn region_name_deserialize_too_long() {
let json = "\"123456789012345678901234567890123\"";
let result: Result<RegionName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("region_name_deserialize_too_long error: {}", err_msg);
assert!(
err_msg.contains("name exceeds maximum length"),
"Expected error message to contain 'name exceeds maximum length', got: {}",
err_msg
);
}
#[test]
fn region_name_deserialize_non_ascii() {
let json = "\"region-🌍\"";
let result: Result<RegionName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("region_name_deserialize_non_ascii error: {}", err_msg);
assert!(
err_msg.contains("non-ASCII"),
"Expected error message to contain 'non-ASCII', got: {}",
err_msg
);
}
#[test]
fn topology_name_deserialize_valid() {
let json = "\"global\"";
let name: TopologyName = serde_json::from_str(json).unwrap();
assert_eq!(name.as_str(), "global");
}
#[test]
fn topology_name_deserialize_empty() {
let json = "\"\"";
let result: Result<TopologyName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("topology_name_deserialize_empty error: {}", err_msg);
assert!(
err_msg.contains("name cannot be empty"),
"Expected error message to contain 'name cannot be empty', got: {}",
err_msg
);
}
#[test]
fn topology_name_deserialize_too_long() {
let json = "\"123456789012345678901234567890123\"";
let result: Result<TopologyName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("topology_name_deserialize_too_long error: {}", err_msg);
assert!(
err_msg.contains("name exceeds maximum length"),
"Expected error message to contain 'name exceeds maximum length', got: {}",
err_msg
);
}
#[test]
fn topology_name_deserialize_non_ascii() {
let json = "\"拓扑名\"";
let result: Result<TopologyName, _> = serde_json::from_str(json);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
println!("topology_name_deserialize_non_ascii error: {}", err_msg);
assert!(
err_msg.contains("non-ASCII"),
"Expected error message to contain 'non-ASCII', got: {}",
err_msg
);
}
#[test]
fn preferred_region_config_returns_config() {
let config = MultiCloudMultiRegionConfiguration::<String, ()>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"custom-config".to_string(),
)],
vec![],
)
.expect("valid configuration");
assert_eq!(
config.preferred_region_config(),
Some(&"custom-config".to_string())
);
}
#[test]
fn preferred_region_config_selects_correct_region() {
let config = MultiCloudMultiRegionConfiguration::<String, ()>::new(
region_name("gcp-europe-west1"),
vec![
ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"aws-config".to_string(),
),
ProviderRegion::new(
region_name("gcp-europe-west1"),
"gcp",
"europe-west1",
"gcp-config".to_string(),
),
],
vec![],
)
.expect("valid configuration");
assert_eq!(
config.preferred_region_config(),
Some(&"gcp-config".to_string())
);
}
#[test]
fn provider_region_cast() {
let region = ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", 42i32);
let casted = region.cast(|n| n.to_string());
assert_eq!(casted.name(), ®ion_name("aws-us-east-1"));
assert_eq!(casted.provider(), "aws");
assert_eq!(casted.region(), "us-east-1");
assert_eq!(casted.config(), "42");
}
#[test]
fn provider_region_try_cast_success() {
let region = ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
);
let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
region.try_cast(|s| s.parse());
let casted = result.expect("parsing should succeed");
assert_eq!(casted.config(), &42);
}
#[test]
fn provider_region_try_cast_failure() {
let region = ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"not-a-number".to_string(),
);
let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
region.try_cast(|s| s.parse());
assert!(result.is_err());
println!(
"provider_region_try_cast_failure error: {:?}",
result.unwrap_err()
);
}
#[test]
fn topology_cast() {
let t = Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
100i32,
);
let casted = t.cast(|n| n * 2);
assert_eq!(casted.name(), &topology_name("global"));
assert_eq!(casted.regions(), &[region_name("aws-us-east-1")]);
assert_eq!(casted.config(), &200);
}
#[test]
fn topology_try_cast_success() {
let t = Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"123".to_string(),
);
let result: Result<Topology<i32>, std::num::ParseIntError> = t.try_cast(|s| s.parse());
let casted = result.expect("parsing should succeed");
assert_eq!(casted.config(), &123);
}
#[test]
fn topology_try_cast_failure() {
let t = Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"invalid".to_string(),
);
let result: Result<Topology<i32>, std::num::ParseIntError> = t.try_cast(|s| s.parse());
assert!(result.is_err());
println!("topology_try_cast_failure error: {:?}", result.unwrap_err());
}
#[test]
fn configuration_cast() {
let config = MultiCloudMultiRegionConfiguration::<i32, i32>::new(
region_name("aws-us-east-1"),
vec![
ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", 10),
ProviderRegion::new(region_name("gcp-europe-west1"), "gcp", "europe-west1", 20),
],
vec![Topology::new(
topology_name("global"),
vec![
region_name("aws-us-east-1"),
region_name("gcp-europe-west1"),
],
100,
)],
)
.expect("valid configuration");
let casted = config.cast(|r| r.to_string(), |t| t * 2);
assert_eq!(casted.preferred(), ®ion_name("aws-us-east-1"));
assert_eq!(casted.regions().len(), 2);
assert_eq!(casted.regions()[0].config(), "10");
assert_eq!(casted.regions()[1].config(), "20");
assert_eq!(casted.topologies().len(), 1);
assert_eq!(casted.topologies()[0].config(), &200);
}
#[test]
fn configuration_try_cast_success() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"100".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config.try_cast(|r| r.parse(), |t| t.parse());
let casted = result.expect("parsing should succeed");
assert_eq!(casted.preferred_region_config(), Some(&42));
assert_eq!(casted.topologies()[0].config(), &100);
}
#[test]
fn configuration_try_cast_region_failure() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"not-a-number".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"100".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config.try_cast(|r| r.parse(), |t| t.parse());
assert!(result.is_err());
println!(
"configuration_try_cast_region_failure error: {:?}",
result.unwrap_err()
);
}
#[test]
fn configuration_try_cast_topology_failure() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"not-a-number".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config.try_cast(|r| r.parse(), |t| t.parse());
assert!(result.is_err());
println!(
"configuration_try_cast_topology_failure error: {:?}",
result.unwrap_err()
);
}
#[tokio::test]
async fn provider_region_try_cast_async_success() {
let region = ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
);
let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
region.try_cast_async(|s| async move { s.parse() }).await;
let casted = result.expect("parsing should succeed");
assert_eq!(casted.name(), ®ion_name("aws-us-east-1"));
assert_eq!(casted.provider(), "aws");
assert_eq!(casted.region(), "us-east-1");
assert_eq!(casted.config(), &42);
}
#[tokio::test]
async fn provider_region_try_cast_async_failure() {
let region = ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"not-a-number".to_string(),
);
let result: Result<ProviderRegion<i32>, std::num::ParseIntError> =
region.try_cast_async(|s| async move { s.parse() }).await;
assert!(result.is_err());
println!(
"provider_region_try_cast_async_failure error: {:?}",
result.unwrap_err()
);
}
#[tokio::test]
async fn topology_try_cast_async_success() {
let t = Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"123".to_string(),
);
let result: Result<Topology<i32>, std::num::ParseIntError> =
t.try_cast_async(|s| async move { s.parse() }).await;
let casted = result.expect("parsing should succeed");
assert_eq!(casted.name(), &topology_name("global"));
assert_eq!(casted.regions(), &[region_name("aws-us-east-1")]);
assert_eq!(casted.config(), &123);
}
#[tokio::test]
async fn topology_try_cast_async_failure() {
let t = Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"invalid".to_string(),
);
let result: Result<Topology<i32>, std::num::ParseIntError> =
t.try_cast_async(|s| async move { s.parse() }).await;
assert!(result.is_err());
println!(
"topology_try_cast_async_failure error: {:?}",
result.unwrap_err()
);
}
#[tokio::test]
async fn configuration_try_cast_async_success() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"100".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config
.try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
.await;
let casted = result.expect("parsing should succeed");
assert_eq!(casted.preferred_region_config(), Some(&42));
assert_eq!(casted.topologies()[0].config(), &100);
}
#[tokio::test]
async fn configuration_try_cast_async_region_failure() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"not-a-number".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"100".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config
.try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
.await;
assert!(result.is_err());
println!(
"configuration_try_cast_async_region_failure error: {:?}",
result.unwrap_err()
);
}
#[tokio::test]
async fn configuration_try_cast_async_topology_failure() {
let config = MultiCloudMultiRegionConfiguration::<String, String>::new(
region_name("aws-us-east-1"),
vec![ProviderRegion::new(
region_name("aws-us-east-1"),
"aws",
"us-east-1",
"42".to_string(),
)],
vec![Topology::new(
topology_name("global"),
vec![region_name("aws-us-east-1")],
"not-a-number".to_string(),
)],
)
.expect("valid configuration");
let result: Result<MultiCloudMultiRegionConfiguration<i32, i32>, std::num::ParseIntError> =
config
.try_cast_async(|r| async move { r.parse() }, |t| async move { t.parse() })
.await;
assert!(result.is_err());
println!(
"configuration_try_cast_async_topology_failure error: {:?}",
result.unwrap_err()
);
}
}