Skip to main content

chroma_types/
topology.rs

1//! This module defines the topology-related configuration for multi-cloud, multi-region
2//! configuration.
3//!
4//! There are three core structs:
5//! - A [`ProviderRegion`] names a given cloud provider and identifies one geographic region that
6//!   they offer.  For example, AWS has us-east-1 and GCP has europe-west1.  By convention the name
7//!   is `format!("{provider}-{region}")`.
8//! - A [`Topology`] defines a virtual replication overlay that spans multiple provider regions.
9//! - A [`MultiCloudMultiRegionConfiguration`] is a self-contained specification of a set of
10//!   regions, a set of topologies that refer to those regions, and the name of the preferred
11//!   region for operations that have region-affinity.
12//!
13//! This means the following invariants must be upheld within a
14//! `MultiCloudMultiRegionConfiguration`:
15//! - `ProviderRegion.name` must be unique within a MultiCloudMultiRegionConfiguration.
16//! - `Topology.name` must be unique within a MultiCloudMultiRegionConfiguration.
17//! - `Topology.regions` must be refer to a `ProviderRegion.name` within the
18//!   MultiCloudMultiRegionConfiguration.
19//! - `MultiCloudMultiRegionConfiguration.preferred` must refer to a `ProviderRegion.name` within
20//!   the `MultiCloudMultiRegionConfiguration`.
21//!
22//! # Example
23//!
24//! ```
25//! use chroma_types::{
26//!     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology, RegionName, TopologyName,
27//! };
28//!
29//! let config = MultiCloudMultiRegionConfiguration::new(
30//!     RegionName::new("aws-us-east-1").unwrap(),
31//!     vec![
32//!         ProviderRegion::new(
33//!             RegionName::new("aws-us-east-1").unwrap(),
34//!             "aws",
35//!             "us-east-1",
36//!             (),
37//!         ),
38//!         ProviderRegion::new(
39//!             RegionName::new("gcp-europe-west1").unwrap(),
40//!             "gcp",
41//!             "europe-west1",
42//!             (),
43//!         ),
44//!     ],
45//!     vec![Topology::new(
46//!         TopologyName::new("global").unwrap(),
47//!         vec![
48//!             RegionName::new("aws-us-east-1").unwrap(),
49//!             RegionName::new("gcp-europe-west1").unwrap(),
50//!         ],
51//!         (),
52//!     )],
53//! );
54//!
55//! assert!(config.is_ok());
56//! ```
57//!
58//! # Serde
59//!
60//! All types in this module support serialization and deserialization via serde.
61//! [`MultiCloudMultiRegionConfiguration`] validates its invariants during deserialization,
62//! so invalid configurations will fail to deserialize.
63//!
64//! ```
65//! use chroma_types::MultiCloudMultiRegionConfiguration;
66//!
67//! let json = r#"{
68//!     "preferred": "aws-us-east-1",
69//!     "regions": [
70//!         {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
71//!     ],
72//!     "topologies": []
73//! }"#;
74//!
75//! let config: MultiCloudMultiRegionConfiguration<(), ()> = serde_json::from_str(json).unwrap();
76//! assert_eq!(config.preferred().as_str(), "aws-us-east-1");
77//! ```
78
79use 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
90/// Maximum length for region and topology names.
91const MAX_NAME_LENGTH: usize = 32;
92
93/// Errors that can occur when creating a [`RegionName`] or [`TopologyName`].
94#[derive(Clone, Debug, Eq, Error, PartialEq)]
95pub enum NameError {
96    /// The name is empty.
97    #[error("name cannot be empty")]
98    Empty,
99    /// The name exceeds the maximum allowed length.
100    #[error("name exceeds maximum length of {MAX_NAME_LENGTH} characters: {0} characters")]
101    TooLong(usize),
102    /// The name contains non-ASCII characters.
103    #[error("name contains non-ASCII characters")]
104    NonAscii,
105}
106
107/// Validates that a name is a non-empty string of at most 32 ASCII characters.
108fn 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
121/// Finds duplicate values in a slice by extracting a key from each item.
122///
123/// Returns a sorted, deduplicated list of keys that appear more than once.
124fn 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/// A strongly-typed region name.
147///
148/// This newtype wrapper ensures region names cannot be confused with other string types
149/// like topology names.
150///
151/// # Example
152///
153/// ```
154/// use chroma_types::RegionName;
155///
156/// let name = RegionName::new("aws-us-east-1").unwrap();
157/// assert_eq!(name.as_str(), "aws-us-east-1");
158/// ```
159#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
160#[serde(transparent)]
161pub struct RegionName(String);
162
163impl RegionName {
164    /// Creates a new region name.
165    ///
166    /// # Errors
167    ///
168    /// Returns a [`NameError`] if the name:
169    /// - Is empty
170    /// - Exceeds 32 characters
171    /// - Contains non-ASCII characters
172    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    /// Returns the region name as a string slice.
179    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/// A strongly-typed topology name.
201///
202/// This newtype wrapper ensures topology names cannot be confused with other string types
203/// like region names.
204///
205/// # Example
206///
207/// ```
208/// use chroma_types::TopologyName;
209///
210/// let name = TopologyName::new("global").unwrap();
211/// assert_eq!(name.as_str(), "global");
212/// ```
213#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
214#[serde(transparent)]
215pub struct TopologyName(String);
216
217impl TopologyName {
218    /// Creates a new topology name.
219    ///
220    /// # Errors
221    ///
222    /// Returns a [`NameError`] if the name:
223    /// - Is empty
224    /// - Exceeds 32 characters
225    /// - Contains non-ASCII characters
226    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    /// Returns the topology name as a string slice.
233    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/// A cloud provider and geographic region.
255///
256/// # Example
257///
258/// ```
259/// use chroma_types::{ProviderRegion, RegionName};
260///
261/// let region = ProviderRegion::new(
262///     RegionName::new("aws-us-east-1").unwrap(),
263///     "aws",
264///     "us-east-1",
265///     (),
266/// );
267/// assert_eq!(region.name().as_str(), "aws-us-east-1");
268/// assert_eq!(region.provider(), "aws");
269/// assert_eq!(region.region(), "us-east-1");
270/// ```
271#[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    /// The unique name for this provider-region combination.
278    pub name: RegionName,
279    /// The cloud provider (e.g., "aws", "gcp").
280    pub provider: String,
281    /// The region within the provider (e.g., "us-east-1", "europe-west1").
282    pub region: String,
283    /// Additional per-region data.
284    pub config: T,
285}
286
287impl<T: Clone + Debug> ProviderRegion<T> {
288    /// Creates a new provider region.
289    ///
290    /// # Example
291    ///
292    /// ```
293    /// use chroma_types::{ProviderRegion, RegionName};
294    ///
295    /// let region = ProviderRegion::new(
296    ///     RegionName::new("gcp-europe-west1").unwrap(),
297    ///     "gcp",
298    ///     "europe-west1",
299    ///     (),
300    /// );
301    /// ```
302    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    /// Returns the unique name for this provider-region combination.
317    pub fn name(&self) -> &RegionName {
318        &self.name
319    }
320
321    /// Returns the cloud provider (e.g., "aws", "gcp").
322    pub fn provider(&self) -> &str {
323        &self.provider
324    }
325
326    /// Returns the region within the provider (e.g., "us-east-1", "europe-west1").
327    pub fn region(&self) -> &str {
328        &self.region
329    }
330
331    /// Returns the additional per-region configuration data.
332    pub fn config(&self) -> &T {
333        &self.config
334    }
335
336    /// Transforms this provider region into a new type by applying a function to the config.
337    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    /// Transforms this provider region into a new type by applying a fallible function to the
351    /// config.
352    ///
353    /// # Errors
354    ///
355    /// Returns an error if the transformation function returns an error.
356    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    /// Transforms this provider region into a new type by applying an async fallible function
370    /// to the config.
371    ///
372    /// # Errors
373    ///
374    /// Returns an error if the transformation function returns an error.
375    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/// A named replication topology spanning multiple provider regions.
391///
392/// # Example
393///
394/// ```
395/// use chroma_types::{Topology, TopologyName, RegionName};
396///
397/// let topology = Topology::new(
398///     TopologyName::new("us-multi-az").unwrap(),
399///     vec![
400///         RegionName::new("aws-us-east-1").unwrap(),
401///         RegionName::new("aws-us-west-2").unwrap(),
402///     ],
403///     (),
404/// );
405/// assert_eq!(topology.name().as_str(), "us-multi-az");
406/// assert_eq!(topology.regions().len(), 2);
407/// ```
408#[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    /// The unique name for this topology.
415    pub name: TopologyName,
416    /// The names of provider regions included in this topology.
417    regions: Vec<RegionName>,
418    /// The configuration of this topology.
419    pub config: T,
420}
421
422impl<T: Clone + Debug> Topology<T> {
423    /// Creates a new topology.
424    ///
425    /// # Example
426    ///
427    /// ```
428    /// use chroma_types::{Topology, TopologyName, RegionName};
429    ///
430    /// let topology = Topology::new(
431    ///     TopologyName::new("global").unwrap(),
432    ///     vec![RegionName::new("aws-us-east-1").unwrap()],
433    ///     (),
434    /// );
435    /// ```
436    pub fn new(name: TopologyName, regions: Vec<RegionName>, config: T) -> Self {
437        Self {
438            name,
439            regions,
440            config,
441        }
442    }
443
444    /// Returns the unique name for this topology.
445    pub fn name(&self) -> &TopologyName {
446        &self.name
447    }
448
449    /// Returns the names of provider regions included in this topology.
450    pub fn regions(&self) -> &[RegionName] {
451        &self.regions
452    }
453
454    /// Returns the additional per-topology configuration data.
455    pub fn config(&self) -> &T {
456        &self.config
457    }
458
459    /// Transforms this topology into a new type by applying a function to the config.
460    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    /// Transforms this topology into a new type by applying a fallible function to the config.
473    ///
474    /// # Errors
475    ///
476    /// Returns an error if the transformation function returns an error.
477    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    /// Transforms this topology into a new type by applying an async fallible function to the
490    /// config.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if the transformation function returns an error.
495    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/// Configuration for multi-cloud, multi-region deployments.
510///
511/// This type validates its invariants both during construction via [`new`](Self::new)
512/// and during deserialization. Invalid configurations will fail with a [`ValidationError`].
513///
514/// The type has two generic parameters:
515/// - `R`: The type of per-region configuration data stored in [`ProviderRegion`].
516/// - `T`: The type of per-topology configuration data stored in [`Topology`].
517///
518/// # Example
519///
520/// ```
521/// use chroma_types::{
522///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
523///     RegionName, TopologyName,
524/// };
525///
526/// let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
527///     RegionName::new("aws-us-east-1").unwrap(),
528///     vec![ProviderRegion::new(
529///         RegionName::new("aws-us-east-1").unwrap(),
530///         "aws",
531///         "us-east-1",
532///         (),
533///     )],
534///     vec![],
535/// ).expect("valid configuration");
536///
537/// assert_eq!(config.preferred().as_str(), "aws-us-east-1");
538/// ```
539#[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    /// The name of the preferred region for operations with region affinity.
546    pub preferred: RegionName,
547    /// The set of provider regions available in this configuration.
548    pub regions: Vec<ProviderRegion<R>>,
549    /// The set of topologies defined over the provider regions.
550    pub topologies: Vec<Topology<T>>,
551}
552
553impl<R: Clone + Debug, T: Clone + Debug> MultiCloudMultiRegionConfiguration<R, T> {
554    /// Returns the preferred region, if found.
555    pub fn preferred_region(&self) -> Option<&ProviderRegion<R>> {
556        self.regions.iter().find(|pr| pr.name() == &self.preferred)
557    }
558
559    /// Returns the named region or None if nothing.
560    pub fn lookup_region(&self, name: &RegionName) -> Option<ProviderRegion<R>> {
561        self.regions.iter().find(|pr| pr.name() == name).cloned()
562    }
563
564    /// Returns the regions for a configured topology or None if it doesn't exist or references a
565    /// non-existent region.
566    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/// Raw representation for serde deserialization before validation.
580#[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/// Errors that can occur when validating a [`MultiCloudMultiRegionConfiguration`].
629#[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    /// Returns true if validation errors were found.
661    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    /// Returns the provider region names that appear more than once.
669    pub fn duplicate_region_names(&self) -> &[RegionName] {
670        &self.duplicate_region_names
671    }
672
673    /// Returns the topology names that appear more than once.
674    pub fn duplicate_topology_names(&self) -> &[TopologyName] {
675        &self.duplicate_topology_names
676    }
677
678    /// Returns the region names referenced by topologies that do not exist in the configuration.
679    pub fn unknown_topology_regions(&self) -> &[RegionName] {
680        &self.unknown_topology_regions
681    }
682
683    /// Returns the preferred region name if it does not exist in the configuration.
684    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
724/// Formats a slice of displayable items as a comma-separated string.
725fn 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    /// Creates and validates a new multi-cloud, multi-region configuration.
735    ///
736    /// Returns an error if validation fails.
737    ///
738    /// # Example
739    ///
740    /// ```
741    /// use chroma_types::{
742    ///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
743    ///     RegionName, TopologyName,
744    /// };
745    ///
746    /// // Valid configuration
747    /// let config = MultiCloudMultiRegionConfiguration::new(
748    ///     RegionName::new("aws-us-east-1").unwrap(),
749    ///     vec![ProviderRegion::new(
750    ///         RegionName::new("aws-us-east-1").unwrap(),
751    ///         "aws",
752    ///         "us-east-1",
753    ///         (),
754    ///     )],
755    ///     vec![Topology::new(
756    ///         TopologyName::new("default").unwrap(),
757    ///         vec![RegionName::new("aws-us-east-1").unwrap()],
758    ///         (),
759    ///     )],
760    /// );
761    /// assert!(config.is_ok());
762    ///
763    /// // Invalid configuration - preferred region doesn't exist
764    /// let config = MultiCloudMultiRegionConfiguration::<(), ()>::new(
765    ///     RegionName::new("nonexistent").unwrap(),
766    ///     vec![ProviderRegion::new(
767    ///         RegionName::new("aws-us-east-1").unwrap(),
768    ///         "aws",
769    ///         "us-east-1",
770    ///         (),
771    ///     )],
772    ///     vec![],
773    /// );
774    /// assert!(config.is_err());
775    /// ```
776    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    /// Returns the preferred region for operations with region affinity.
791    pub fn preferred(&self) -> &RegionName {
792        &self.preferred
793    }
794
795    /// Returns the set of provider regions available in this configuration.
796    pub fn regions(&self) -> &[ProviderRegion<R>] {
797        &self.regions
798    }
799
800    /// Returns the set of topologies defined over the provider regions.
801    pub fn topologies(&self) -> &[Topology<T>] {
802        &self.topologies
803    }
804
805    /// Validates the configuration against the invariants.
806    ///
807    /// Returns `Ok(())` if validation passes, or a [`ValidationError`] describing any violations.
808    /// Each unique error is reported only once, even if it occurs multiple times.
809    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        // Find all unique unknown regions across all topologies.
817        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        // Check if the preferred region is one of the defined regions.
829        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    /// Returns the configuration for the preferred region, if found.
841    ///
842    /// Since the configuration validates that the preferred region exists during construction,
843    /// this method returns `Some` for valid configurations. It returns `None` only if the
844    /// internal state is inconsistent, which should not occur with properly constructed instances.
845    ///
846    /// # Example
847    ///
848    /// ```
849    /// use chroma_types::{
850    ///     MultiCloudMultiRegionConfiguration, ProviderRegion, RegionName,
851    /// };
852    ///
853    /// let config = MultiCloudMultiRegionConfiguration::<String, ()>::new(
854    ///     RegionName::new("aws-us-east-1").unwrap(),
855    ///     vec![ProviderRegion::new(
856    ///         RegionName::new("aws-us-east-1").unwrap(),
857    ///         "aws",
858    ///         "us-east-1",
859    ///         "custom-config".to_string(),
860    ///     )],
861    ///     vec![],
862    /// ).expect("valid configuration");
863    ///
864    /// assert_eq!(config.preferred_region_config(), Some(&"custom-config".to_string()));
865    /// ```
866    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    /// Transforms this configuration into a new type by applying functions to the region and
874    /// topology configs.
875    ///
876    /// This method consumes the configuration and produces a new one with different generic
877    /// type parameters. The transformation functions are applied to each region config and
878    /// topology config respectively.
879    ///
880    /// # Example
881    ///
882    /// ```
883    /// use chroma_types::{
884    ///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
885    ///     RegionName, TopologyName,
886    /// };
887    ///
888    /// let config = MultiCloudMultiRegionConfiguration::<i32, &str>::new(
889    ///     RegionName::new("aws-us-east-1").unwrap(),
890    ///     vec![ProviderRegion::new(
891    ///         RegionName::new("aws-us-east-1").unwrap(),
892    ///         "aws",
893    ///         "us-east-1",
894    ///         42,
895    ///     )],
896    ///     vec![Topology::new(
897    ///         TopologyName::new("default").unwrap(),
898    ///         vec![RegionName::new("aws-us-east-1").unwrap()],
899    ///         "topology-config",
900    ///     )],
901    /// ).expect("valid configuration");
902    ///
903    /// let transformed = config.cast(
904    ///     |r| r.to_string(),
905    ///     |t| t.len(),
906    /// );
907    ///
908    /// assert_eq!(transformed.preferred_region_config(), Some(&"42".to_string()));
909    /// assert_eq!(transformed.topologies()[0].config(), &15);
910    /// ```
911    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(&region_fn))
928                .collect(),
929            topologies: self
930                .topologies
931                .into_iter()
932                .map(|t| t.cast(&topology_fn))
933                .collect(),
934        }
935    }
936
937    /// Transforms this configuration into a new type by applying fallible functions to the
938    /// region and topology configs.
939    ///
940    /// This method consumes the configuration and produces a new one with different generic
941    /// type parameters. The transformation functions are applied to each region config and
942    /// topology config respectively. If any transformation fails, the error is returned
943    /// immediately.
944    ///
945    /// # Errors
946    ///
947    /// Returns an error if any region or topology transformation function returns an error.
948    ///
949    /// # Example
950    ///
951    /// ```
952    /// use chroma_types::{
953    ///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
954    ///     RegionName, TopologyName,
955    /// };
956    ///
957    /// let config = MultiCloudMultiRegionConfiguration::<&str, i32>::new(
958    ///     RegionName::new("aws-us-east-1").unwrap(),
959    ///     vec![ProviderRegion::new(
960    ///         RegionName::new("aws-us-east-1").unwrap(),
961    ///         "aws",
962    ///         "us-east-1",
963    ///         "42",
964    ///     )],
965    ///     vec![Topology::new(
966    ///         TopologyName::new("default").unwrap(),
967    ///         vec![RegionName::new("aws-us-east-1").unwrap()],
968    ///         100,
969    ///     )],
970    /// ).expect("valid configuration");
971    ///
972    /// let result: Result<_, std::num::ParseIntError> = config.try_cast(
973    ///     |r| r.parse::<i32>(),
974    ///     |t| Ok(t.to_string()),
975    /// );
976    ///
977    /// let transformed = result.expect("transformation should succeed");
978    /// assert_eq!(transformed.preferred_region_config(), Some(&42));
979    /// assert_eq!(transformed.topologies()[0].config(), "100");
980    /// ```
981    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(&region_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    /// Transforms this configuration into a new type by applying async fallible functions to the
1010    /// region and topology configs.
1011    ///
1012    /// # Errors
1013    ///
1014    /// Returns an error if any region or topology transformation function returns an error.
1015    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(&region_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(), &region_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(&region).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(), &region_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        /// A concrete configuration struct to verify serde bounds work for complex types.
1278        #[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        // Verify serialization produces valid JSON with config payloads.
1330        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        // Verify deserialization roundtrip.
1341        let deserialized: MultiCloudMultiRegionConfiguration<RegionConfig, TopologyConfig> =
1342            serde_json::from_str(&json).unwrap();
1343        assert_eq!(config, deserialized);
1344
1345        // Verify the config values are accessible and correct.
1346        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(&region_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        // When the same unknown region is referenced in multiple topologies, it should only
1541        // appear once in the error output for cleaner, more deterministic error messages.
1542        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(&region_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(&region_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        // 32 chars exactly
1752        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        // 33 chars
1769        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        // 32 chars exactly
1794        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        // 33 chars
1811        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(), &region_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(), &region_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(), &region_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}