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//! assert!(config.is_ok());
55//! ```
56//!
57//! # Serde
58//!
59//! All types in this module support serialization and deserialization via serde.
60//! [`MultiCloudMultiRegionConfiguration`] validates its invariants during deserialization,
61//! so invalid configurations will fail to deserialize.
62//!
63//! ```
64//! use chroma_types::MultiCloudMultiRegionConfiguration;
65//!
66//! let json = r#"{
67//!     "preferred": "aws-us-east-1",
68//!     "regions": [
69//!         {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
70//!     ],
71//!     "topologies": []
72//! }"#;
73//!
74//! let config: MultiCloudMultiRegionConfiguration<()> = serde_json::from_str(json).unwrap();
75//! assert_eq!(config.preferred().as_str(), "aws-us-east-1");
76//! ```
77
78use std::collections::HashSet;
79use std::fmt::Debug;
80use std::hash::Hash;
81
82use serde::Deserialize;
83use serde::Serialize;
84use thiserror::Error;
85
86/// Maximum length for region and topology names.
87const MAX_NAME_LENGTH: usize = 32;
88
89/// Errors that can occur when creating a [`RegionName`] or [`TopologyName`].
90#[derive(Clone, Debug, Eq, Error, PartialEq)]
91pub enum NameError {
92    /// The name is empty.
93    #[error("name cannot be empty")]
94    Empty,
95    /// The name exceeds the maximum allowed length.
96    #[error("name exceeds maximum length of {MAX_NAME_LENGTH} characters: {0} characters")]
97    TooLong(usize),
98    /// The name contains non-ASCII characters.
99    #[error("name contains non-ASCII characters")]
100    NonAscii,
101}
102
103/// Validates that a name is a non-empty string of at most 32 ASCII characters.
104fn validate_name(name: &str) -> Result<(), NameError> {
105    if name.is_empty() {
106        return Err(NameError::Empty);
107    }
108    if !name.is_ascii() {
109        return Err(NameError::NonAscii);
110    }
111    if name.len() > MAX_NAME_LENGTH {
112        return Err(NameError::TooLong(name.len()));
113    }
114    Ok(())
115}
116
117/// Finds duplicate values in a slice by extracting a key from each item.
118///
119/// Returns a sorted, deduplicated list of keys that appear more than once.
120fn find_duplicates<'a, T, K, F>(items: &'a [T], key_fn: F) -> Vec<K>
121where
122    K: 'a + Clone + Eq + Hash + Ord,
123    F: Fn(&'a T) -> &'a K,
124{
125    let mut seen = HashSet::new();
126    let mut duplicates: Vec<_> = items
127        .iter()
128        .filter_map(|item| {
129            let key = key_fn(item);
130            if !seen.insert(key) {
131                Some(key.clone())
132            } else {
133                None
134            }
135        })
136        .collect();
137    duplicates.sort();
138    duplicates.dedup();
139    duplicates
140}
141
142/// A strongly-typed region name.
143///
144/// This newtype wrapper ensures region names cannot be confused with other string types
145/// like topology names.
146///
147/// # Example
148///
149/// ```
150/// use chroma_types::RegionName;
151///
152/// let name = RegionName::new("aws-us-east-1").unwrap();
153/// assert_eq!(name.as_str(), "aws-us-east-1");
154/// ```
155#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
156#[serde(transparent)]
157pub struct RegionName(String);
158
159impl RegionName {
160    /// Creates a new region name.
161    ///
162    /// # Errors
163    ///
164    /// Returns a [`NameError`] if the name:
165    /// - Is empty
166    /// - Exceeds 32 characters
167    /// - Contains non-ASCII characters
168    pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
169        let name = name.into();
170        validate_name(&name)?;
171        Ok(Self(name))
172    }
173
174    /// Returns the region name as a string slice.
175    pub fn as_str(&self) -> &str {
176        &self.0
177    }
178}
179
180impl<'de> Deserialize<'de> for RegionName {
181    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182    where
183        D: serde::Deserializer<'de>,
184    {
185        let s = String::deserialize(deserializer)?;
186        RegionName::new(s).map_err(serde::de::Error::custom)
187    }
188}
189
190impl std::fmt::Display for RegionName {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "{}", self.0)
193    }
194}
195
196/// A strongly-typed topology name.
197///
198/// This newtype wrapper ensures topology names cannot be confused with other string types
199/// like region names.
200///
201/// # Example
202///
203/// ```
204/// use chroma_types::TopologyName;
205///
206/// let name = TopologyName::new("global").unwrap();
207/// assert_eq!(name.as_str(), "global");
208/// ```
209#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
210#[serde(transparent)]
211pub struct TopologyName(String);
212
213impl TopologyName {
214    /// Creates a new topology name.
215    ///
216    /// # Errors
217    ///
218    /// Returns a [`NameError`] if the name:
219    /// - Is empty
220    /// - Exceeds 32 characters
221    /// - Contains non-ASCII characters
222    pub fn new(name: impl Into<String>) -> Result<Self, NameError> {
223        let name = name.into();
224        validate_name(&name)?;
225        Ok(Self(name))
226    }
227
228    /// Returns the topology name as a string slice.
229    pub fn as_str(&self) -> &str {
230        &self.0
231    }
232}
233
234impl<'de> Deserialize<'de> for TopologyName {
235    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
236    where
237        D: serde::Deserializer<'de>,
238    {
239        let s = String::deserialize(deserializer)?;
240        TopologyName::new(s).map_err(serde::de::Error::custom)
241    }
242}
243
244impl std::fmt::Display for TopologyName {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        write!(f, "{}", self.0)
247    }
248}
249
250/// A cloud provider and geographic region.
251///
252/// # Example
253///
254/// ```
255/// use chroma_types::{ProviderRegion, RegionName};
256///
257/// let region = ProviderRegion::new(
258///     RegionName::new("aws-us-east-1").unwrap(),
259///     "aws",
260///     "us-east-1",
261///     (),
262/// );
263/// assert_eq!(region.name().as_str(), "aws-us-east-1");
264/// assert_eq!(region.provider(), "aws");
265/// assert_eq!(region.region(), "us-east-1");
266/// ```
267#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
268#[serde(bound(
269    serialize = "T: Clone + Debug + Eq + PartialEq + Serialize",
270    deserialize = "T: Clone + Debug + Eq + PartialEq + serde::de::DeserializeOwned"
271))]
272pub struct ProviderRegion<T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>> {
273    /// The unique name for this provider-region combination.
274    name: RegionName,
275    /// The cloud provider (e.g., "aws", "gcp").
276    provider: String,
277    /// The region within the provider (e.g., "us-east-1", "europe-west1").
278    region: String,
279    /// Additional per-region data.
280    config: T,
281}
282
283impl<T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>> ProviderRegion<T> {
284    /// Creates a new provider region.
285    ///
286    /// # Example
287    ///
288    /// ```
289    /// use chroma_types::{ProviderRegion, RegionName};
290    ///
291    /// let region = ProviderRegion::new(
292    ///     RegionName::new("gcp-europe-west1").unwrap(),
293    ///     "gcp",
294    ///     "europe-west1",
295    ///     (),
296    /// );
297    /// ```
298    pub fn new(
299        name: RegionName,
300        provider: impl Into<String>,
301        region: impl Into<String>,
302        config: T,
303    ) -> Self {
304        Self {
305            name,
306            provider: provider.into(),
307            region: region.into(),
308            config,
309        }
310    }
311
312    /// Returns the unique name for this provider-region combination.
313    pub fn name(&self) -> &RegionName {
314        &self.name
315    }
316
317    /// Returns the cloud provider (e.g., "aws", "gcp").
318    pub fn provider(&self) -> &str {
319        &self.provider
320    }
321
322    /// Returns the region within the provider (e.g., "us-east-1", "europe-west1").
323    pub fn region(&self) -> &str {
324        &self.region
325    }
326
327    /// Returns the additional per-region configuration data.
328    pub fn config(&self) -> &T {
329        &self.config
330    }
331}
332
333/// A named replication topology spanning multiple provider regions.
334///
335/// # Example
336///
337/// ```
338/// use chroma_types::{Topology, TopologyName, RegionName};
339///
340/// let topology = Topology::new(
341///     TopologyName::new("us-multi-az").unwrap(),
342///     vec![
343///         RegionName::new("aws-us-east-1").unwrap(),
344///         RegionName::new("aws-us-west-2").unwrap(),
345///     ],
346/// );
347/// assert_eq!(topology.name().as_str(), "us-multi-az");
348/// assert_eq!(topology.regions().len(), 2);
349/// ```
350#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
351pub struct Topology {
352    /// The unique name for this topology.
353    name: TopologyName,
354    /// The names of provider regions included in this topology.
355    regions: Vec<RegionName>,
356}
357
358impl Topology {
359    /// Creates a new topology.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use chroma_types::{Topology, TopologyName, RegionName};
365    ///
366    /// let topology = Topology::new(
367    ///     TopologyName::new("global").unwrap(),
368    ///     vec![RegionName::new("aws-us-east-1").unwrap()],
369    /// );
370    /// ```
371    pub fn new(name: TopologyName, regions: Vec<RegionName>) -> Self {
372        Self { name, regions }
373    }
374
375    /// Returns the unique name for this topology.
376    pub fn name(&self) -> &TopologyName {
377        &self.name
378    }
379
380    /// Returns the names of provider regions included in this topology.
381    pub fn regions(&self) -> &[RegionName] {
382        &self.regions
383    }
384}
385
386/// Configuration for multi-cloud, multi-region deployments.
387///
388/// This type validates its invariants both during construction via [`new`](Self::new)
389/// and during deserialization. Invalid configurations will fail with a [`ValidationError`].
390///
391/// # Example
392///
393/// ```
394/// use chroma_types::{
395///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
396///     RegionName, TopologyName,
397/// };
398///
399/// let config = MultiCloudMultiRegionConfiguration::new(
400///     RegionName::new("aws-us-east-1").unwrap(),
401///     vec![ProviderRegion::new(
402///         RegionName::new("aws-us-east-1").unwrap(),
403///         "aws",
404///         "us-east-1",
405///         (),
406///     )],
407///     vec![],
408/// ).expect("valid configuration");
409///
410/// assert_eq!(config.preferred().as_str(), "aws-us-east-1");
411/// ```
412#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
413#[serde(into = "RawMultiCloudMultiRegionConfiguration<T>")]
414pub struct MultiCloudMultiRegionConfiguration<
415    T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>,
416> {
417    /// The name of the preferred region for operations with region affinity.
418    preferred: RegionName,
419    /// The set of provider regions available in this configuration.
420    regions: Vec<ProviderRegion<T>>,
421    /// The set of topologies defined over the provider regions.
422    topologies: Vec<Topology>,
423}
424
425/// Raw representation for serde deserialization before validation.
426#[derive(Clone, Debug, Serialize, Deserialize)]
427#[serde(bound(
428    serialize = "T: Clone + Debug + Eq + PartialEq + Serialize",
429    deserialize = "T: Clone + Debug + Eq + PartialEq + serde::de::DeserializeOwned"
430))]
431struct RawMultiCloudMultiRegionConfiguration<
432    T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>,
433> {
434    preferred: RegionName,
435    regions: Vec<ProviderRegion<T>>,
436    topologies: Vec<Topology>,
437}
438
439impl<T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>>
440    From<MultiCloudMultiRegionConfiguration<T>> for RawMultiCloudMultiRegionConfiguration<T>
441{
442    fn from(config: MultiCloudMultiRegionConfiguration<T>) -> Self {
443        Self {
444            preferred: config.preferred,
445            regions: config.regions,
446            topologies: config.topologies,
447        }
448    }
449}
450
451impl<T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>>
452    TryFrom<RawMultiCloudMultiRegionConfiguration<T>> for MultiCloudMultiRegionConfiguration<T>
453{
454    type Error = ValidationError;
455
456    fn try_from(raw: RawMultiCloudMultiRegionConfiguration<T>) -> Result<Self, Self::Error> {
457        MultiCloudMultiRegionConfiguration::new(raw.preferred, raw.regions, raw.topologies)
458    }
459}
460
461impl<'de, T: Clone + Debug + Eq + PartialEq + Serialize + serde::de::DeserializeOwned>
462    Deserialize<'de> for MultiCloudMultiRegionConfiguration<T>
463{
464    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
465    where
466        D: serde::Deserializer<'de>,
467    {
468        let raw = RawMultiCloudMultiRegionConfiguration::<T>::deserialize(deserializer)?;
469        MultiCloudMultiRegionConfiguration::try_from(raw).map_err(serde::de::Error::custom)
470    }
471}
472
473/// Errors that can occur when validating a [`MultiCloudMultiRegionConfiguration`].
474#[derive(Clone, Debug, Default, Eq, Error, PartialEq)]
475#[error("{}", self.format_message())]
476pub struct ValidationError {
477    duplicate_region_names: Vec<RegionName>,
478    duplicate_topology_names: Vec<TopologyName>,
479    unknown_topology_regions: Vec<RegionName>,
480    unknown_preferred_region: Option<RegionName>,
481}
482
483impl ValidationError {
484    #[cfg(test)]
485    fn new(
486        duplicate_region_names: Vec<RegionName>,
487        duplicate_topology_names: Vec<TopologyName>,
488        unknown_topology_regions: Vec<RegionName>,
489        unknown_preferred_region: Option<RegionName>,
490    ) -> Self {
491        Self {
492            duplicate_region_names,
493            duplicate_topology_names,
494            unknown_topology_regions,
495            unknown_preferred_region,
496        }
497    }
498
499    /// Returns true if validation errors were found.
500    pub fn has_errors(&self) -> bool {
501        !self.duplicate_region_names.is_empty()
502            || !self.duplicate_topology_names.is_empty()
503            || !self.unknown_topology_regions.is_empty()
504            || self.unknown_preferred_region.is_some()
505    }
506
507    /// Returns the provider region names that appear more than once.
508    pub fn duplicate_region_names(&self) -> &[RegionName] {
509        &self.duplicate_region_names
510    }
511
512    /// Returns the topology names that appear more than once.
513    pub fn duplicate_topology_names(&self) -> &[TopologyName] {
514        &self.duplicate_topology_names
515    }
516
517    /// Returns the region names referenced by topologies that do not exist in the configuration.
518    pub fn unknown_topology_regions(&self) -> &[RegionName] {
519        &self.unknown_topology_regions
520    }
521
522    /// Returns the preferred region name if it does not exist in the configuration.
523    pub fn unknown_preferred_region(&self) -> Option<&RegionName> {
524        self.unknown_preferred_region.as_ref()
525    }
526
527    fn format_message(&self) -> String {
528        if !self.has_errors() {
529            return "no validation errors".to_string();
530        }
531
532        let mut parts = Vec::new();
533
534        if !self.duplicate_region_names.is_empty() {
535            parts.push(format!(
536                "duplicate region names: {}",
537                format_name_list(&self.duplicate_region_names)
538            ));
539        }
540
541        if !self.duplicate_topology_names.is_empty() {
542            parts.push(format!(
543                "duplicate topology names: {}",
544                format_name_list(&self.duplicate_topology_names)
545            ));
546        }
547
548        if !self.unknown_topology_regions.is_empty() {
549            parts.push(format!(
550                "unknown topology regions: {}",
551                format_name_list(&self.unknown_topology_regions)
552            ));
553        }
554
555        if let Some(ref name) = self.unknown_preferred_region {
556            parts.push(format!("unknown preferred region: {}", name));
557        }
558
559        parts.join("; ")
560    }
561}
562
563/// Formats a slice of displayable items as a comma-separated string.
564fn format_name_list<T: std::fmt::Display>(names: &[T]) -> String {
565    names
566        .iter()
567        .map(|n| n.to_string())
568        .collect::<Vec<_>>()
569        .join(", ")
570}
571
572impl<T: Clone + Debug + Eq + PartialEq + Serialize + for<'a> Deserialize<'a>>
573    MultiCloudMultiRegionConfiguration<T>
574{
575    /// Creates and validates a new multi-cloud, multi-region configuration.
576    ///
577    /// Returns an error if validation fails.
578    ///
579    /// # Example
580    ///
581    /// ```
582    /// use chroma_types::{
583    ///     MultiCloudMultiRegionConfiguration, ProviderRegion, Topology,
584    ///     RegionName, TopologyName,
585    /// };
586    ///
587    /// // Valid configuration
588    /// let config = MultiCloudMultiRegionConfiguration::new(
589    ///     RegionName::new("aws-us-east-1").unwrap(),
590    ///     vec![ProviderRegion::new(
591    ///         RegionName::new("aws-us-east-1").unwrap(),
592    ///         "aws",
593    ///         "us-east-1",
594    ///         (),
595    ///     )],
596    ///     vec![Topology::new(
597    ///         TopologyName::new("default").unwrap(),
598    ///         vec![RegionName::new("aws-us-east-1").unwrap()],
599    ///     )],
600    /// );
601    /// assert!(config.is_ok());
602    ///
603    /// // Invalid configuration - preferred region doesn't exist
604    /// let config = MultiCloudMultiRegionConfiguration::<()>::new(
605    ///     RegionName::new("nonexistent").unwrap(),
606    ///     vec![ProviderRegion::new(
607    ///         RegionName::new("aws-us-east-1").unwrap(),
608    ///         "aws",
609    ///         "us-east-1",
610    ///         (),
611    ///     )],
612    ///     vec![],
613    /// );
614    /// assert!(config.is_err());
615    /// ```
616    pub fn new(
617        preferred: RegionName,
618        regions: Vec<ProviderRegion<T>>,
619        topologies: Vec<Topology>,
620    ) -> Result<Self, ValidationError> {
621        let config = Self {
622            preferred,
623            regions,
624            topologies,
625        };
626        config.validate()?;
627        Ok(config)
628    }
629
630    /// Returns the preferred region for operations with region affinity.
631    pub fn preferred(&self) -> &RegionName {
632        &self.preferred
633    }
634
635    /// Returns the set of provider regions available in this configuration.
636    pub fn regions(&self) -> &[ProviderRegion<T>] {
637        &self.regions
638    }
639
640    /// Returns the set of topologies defined over the provider regions.
641    pub fn topologies(&self) -> &[Topology] {
642        &self.topologies
643    }
644
645    /// Validates the configuration against the invariants.
646    ///
647    /// Returns `Ok(())` if validation passes, or a [`ValidationError`] describing any violations.
648    /// Each unique error is reported only once, even if it occurs multiple times.
649    fn validate(&self) -> Result<(), ValidationError> {
650        let mut error = ValidationError::default();
651        let all_region_names: HashSet<_> = self.regions.iter().map(|r| &r.name).collect();
652
653        error.duplicate_region_names = find_duplicates(&self.regions, |r| &r.name);
654        error.duplicate_topology_names = find_duplicates(&self.topologies, |t| &t.name);
655
656        // Find all unique unknown regions across all topologies.
657        let mut unknown_regions: Vec<_> = self
658            .topologies
659            .iter()
660            .flat_map(|t| &t.regions)
661            .filter(|r| !all_region_names.contains(r))
662            .cloned()
663            .collect();
664        unknown_regions.sort();
665        unknown_regions.dedup();
666        error.unknown_topology_regions = unknown_regions;
667
668        // Check if the preferred region is one of the defined regions.
669        if !all_region_names.contains(&self.preferred) {
670            error.unknown_preferred_region = Some(self.preferred.clone());
671        }
672
673        if error.has_errors() {
674            Err(error)
675        } else {
676            Ok(())
677        }
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    fn region_name(s: impl Into<String>) -> RegionName {
686        RegionName::new(s).expect("test region name should be valid")
687    }
688
689    fn topology_name(s: impl Into<String>) -> TopologyName {
690        TopologyName::new(s).expect("test topology name should be valid")
691    }
692
693    fn provider_region(
694        name: impl Into<String>,
695        provider: impl Into<String>,
696        region: impl Into<String>,
697    ) -> ProviderRegion<()> {
698        ProviderRegion::new(
699            RegionName::new(name).expect("test region name should be valid"),
700            provider,
701            region,
702            (),
703        )
704    }
705
706    fn topology(name: impl Into<String>, regions: Vec<&str>) -> Topology {
707        Topology::new(
708            TopologyName::new(name).expect("test topology name should be valid"),
709            regions
710                .into_iter()
711                .map(|s| RegionName::new(s).expect("test region name should be valid"))
712                .collect(),
713        )
714    }
715
716    #[test]
717    fn region_name_as_str() {
718        let name = RegionName::new("aws-us-east-1").expect("valid name");
719        assert_eq!(name.as_str(), "aws-us-east-1");
720    }
721
722    #[test]
723    fn region_name_display() {
724        let name = RegionName::new("aws-us-east-1").expect("valid name");
725        assert_eq!(format!("{}", name), "aws-us-east-1");
726    }
727
728    #[test]
729    fn region_name_equality() {
730        let a = RegionName::new("aws-us-east-1");
731        let b = RegionName::new("aws-us-east-1");
732        let c = RegionName::new("gcp-europe-west1");
733        assert_eq!(a, b);
734        assert_ne!(a, c);
735    }
736
737    #[test]
738    fn region_name_clone() {
739        let a = RegionName::new("aws-us-east-1");
740        let b = a.clone();
741        assert_eq!(a, b);
742    }
743
744    #[test]
745    fn region_name_serde_roundtrip() {
746        let name = RegionName::new("aws-us-east-1").expect("valid name");
747        let json = serde_json::to_string(&name).unwrap();
748        assert_eq!(json, "\"aws-us-east-1\"");
749        let deserialized: RegionName = serde_json::from_str(&json).unwrap();
750        assert_eq!(name, deserialized);
751    }
752
753    #[test]
754    fn topology_name_as_str() {
755        let name = TopologyName::new("global").expect("valid name");
756        assert_eq!(name.as_str(), "global");
757    }
758
759    #[test]
760    fn topology_name_display() {
761        let name = TopologyName::new("global").expect("valid name");
762        assert_eq!(format!("{}", name), "global");
763    }
764
765    #[test]
766    fn topology_name_equality() {
767        let a = TopologyName::new("global");
768        let b = TopologyName::new("global");
769        let c = TopologyName::new("regional");
770        assert_eq!(a, b);
771        assert_ne!(a, c);
772    }
773
774    #[test]
775    fn topology_name_clone() {
776        let a = TopologyName::new("global");
777        let b = a.clone();
778        assert_eq!(a, b);
779    }
780
781    #[test]
782    fn topology_name_serde_roundtrip() {
783        let name = TopologyName::new("global").expect("valid name");
784        let json = serde_json::to_string(&name).unwrap();
785        assert_eq!(json, "\"global\"");
786        let deserialized: TopologyName = serde_json::from_str(&json).unwrap();
787        assert_eq!(name, deserialized);
788    }
789
790    #[test]
791    fn provider_region_accessors() {
792        let region = ProviderRegion::new(region_name("aws-us-east-1"), "aws", "us-east-1", ());
793        assert_eq!(region.name(), &region_name("aws-us-east-1"));
794        assert_eq!(region.provider(), "aws");
795        assert_eq!(region.region(), "us-east-1");
796    }
797
798    #[test]
799    fn provider_region_equality() {
800        let a = provider_region("aws-us-east-1", "aws", "us-east-1");
801        let b = provider_region("aws-us-east-1", "aws", "us-east-1");
802        let c = provider_region("gcp-europe-west1", "gcp", "europe-west1");
803        assert_eq!(a, b);
804        assert_ne!(a, c);
805    }
806
807    #[test]
808    fn provider_region_clone() {
809        let a = provider_region("aws-us-east-1", "aws", "us-east-1");
810        let b = a.clone();
811        assert_eq!(a, b);
812    }
813
814    #[test]
815    fn provider_region_serde_roundtrip() {
816        let region = provider_region("aws-us-east-1", "aws", "us-east-1");
817        let json = serde_json::to_string(&region).unwrap();
818        let deserialized: ProviderRegion<()> = serde_json::from_str(&json).unwrap();
819        assert_eq!(region, deserialized);
820    }
821
822    #[test]
823    fn topology_accessors() {
824        let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
825        assert_eq!(t.name(), &topology_name("global"));
826        assert_eq!(
827            t.regions(),
828            &[
829                region_name("aws-us-east-1"),
830                region_name("gcp-europe-west1")
831            ]
832        );
833    }
834
835    #[test]
836    fn topology_equality() {
837        let a = topology("global", vec!["aws-us-east-1"]);
838        let b = topology("global", vec!["aws-us-east-1"]);
839        let c = topology("regional", vec!["aws-us-east-1"]);
840        assert_eq!(a, b);
841        assert_ne!(a, c);
842    }
843
844    #[test]
845    fn topology_clone() {
846        let a = topology("global", vec!["aws-us-east-1"]);
847        let b = a.clone();
848        assert_eq!(a, b);
849    }
850
851    #[test]
852    fn topology_serde_roundtrip() {
853        let t = topology("global", vec!["aws-us-east-1", "gcp-europe-west1"]);
854        let json = serde_json::to_string(&t).unwrap();
855        let deserialized: Topology = serde_json::from_str(&json).unwrap();
856        assert_eq!(t, deserialized);
857    }
858
859    #[test]
860    fn valid_configuration() {
861        let config = MultiCloudMultiRegionConfiguration::new(
862            region_name("aws-us-east-1"),
863            vec![
864                provider_region("aws-us-east-1", "aws", "us-east-1"),
865                provider_region("gcp-europe-west1", "gcp", "europe-west1"),
866            ],
867            vec![topology(
868                "global",
869                vec!["aws-us-east-1", "gcp-europe-west1"],
870            )],
871        );
872
873        assert!(config.is_ok(), "Expected valid configuration: {:?}", config);
874    }
875
876    #[test]
877    fn valid_configuration_accessors() {
878        let config = MultiCloudMultiRegionConfiguration::new(
879            region_name("aws-us-east-1"),
880            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
881            vec![topology("global", vec!["aws-us-east-1"])],
882        )
883        .expect("valid configuration");
884
885        assert_eq!(config.preferred(), &region_name("aws-us-east-1"));
886        assert_eq!(config.regions().len(), 1);
887        assert_eq!(config.topologies().len(), 1);
888    }
889
890    #[test]
891    fn configuration_serde_roundtrip() {
892        let config = MultiCloudMultiRegionConfiguration::new(
893            region_name("aws-us-east-1"),
894            vec![
895                provider_region("aws-us-east-1", "aws", "us-east-1"),
896                provider_region("gcp-europe-west1", "gcp", "europe-west1"),
897            ],
898            vec![topology(
899                "global",
900                vec!["aws-us-east-1", "gcp-europe-west1"],
901            )],
902        )
903        .expect("valid configuration");
904
905        let json = serde_json::to_string(&config).unwrap();
906        let deserialized: MultiCloudMultiRegionConfiguration<()> =
907            serde_json::from_str(&json).unwrap();
908        assert_eq!(config, deserialized);
909    }
910
911    #[test]
912    fn configuration_deserialize_valid() {
913        let json = r#"{
914            "preferred": "aws-us-east-1",
915            "regions": [
916                {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
917            ],
918            "topologies": []
919        }"#;
920
921        let config: MultiCloudMultiRegionConfiguration<()> = serde_json::from_str(json).unwrap();
922        assert_eq!(config.preferred().as_str(), "aws-us-east-1");
923    }
924
925    #[test]
926    fn configuration_deserialize_invalid_preferred() {
927        let json = r#"{
928            "preferred": "nonexistent",
929            "regions": [
930                {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
931            ],
932            "topologies": []
933        }"#;
934
935        let result: Result<MultiCloudMultiRegionConfiguration<()>, _> = serde_json::from_str(json);
936        assert!(result.is_err());
937        let err_msg = result.unwrap_err().to_string();
938        assert!(
939            err_msg.contains("unknown preferred region"),
940            "Expected error message to contain 'unknown preferred region', got: {}",
941            err_msg
942        );
943    }
944
945    #[test]
946    fn configuration_deserialize_duplicate_regions() {
947        let json = r#"{
948            "preferred": "aws-us-east-1",
949            "regions": [
950                {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null},
951                {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
952            ],
953            "topologies": []
954        }"#;
955
956        let result: Result<MultiCloudMultiRegionConfiguration<()>, _> = serde_json::from_str(json);
957        assert!(result.is_err());
958        let err_msg = result.unwrap_err().to_string();
959        assert!(
960            err_msg.contains("duplicate region names"),
961            "Expected error message to contain 'duplicate region names', got: {}",
962            err_msg
963        );
964    }
965
966    #[test]
967    fn configuration_deserialize_unknown_topology_region() {
968        let json = r#"{
969            "preferred": "aws-us-east-1",
970            "regions": [
971                {"name": "aws-us-east-1", "provider": "aws", "region": "us-east-1", "config": null}
972            ],
973            "topologies": [
974                {"name": "global", "regions": ["aws-us-east-1", "nonexistent"]}
975            ]
976        }"#;
977
978        let result: Result<MultiCloudMultiRegionConfiguration<()>, _> = serde_json::from_str(json);
979        assert!(result.is_err());
980        let err_msg = result.unwrap_err().to_string();
981        assert!(
982            err_msg.contains("unknown topology regions"),
983            "Expected error message to contain 'unknown topology regions', got: {}",
984            err_msg
985        );
986    }
987
988    #[test]
989    fn empty_configuration() {
990        let config = MultiCloudMultiRegionConfiguration::<()>::new(
991            region_name("nonexistent"),
992            vec![],
993            vec![],
994        );
995
996        let err = config.unwrap_err();
997        assert!(err.duplicate_region_names().is_empty());
998        assert!(err.duplicate_topology_names().is_empty());
999        assert!(err.unknown_topology_regions().is_empty());
1000        assert_eq!(
1001            err.unknown_preferred_region(),
1002            Some(&region_name("nonexistent"))
1003        );
1004    }
1005
1006    #[test]
1007    fn empty_topology_regions() {
1008        let config = MultiCloudMultiRegionConfiguration::new(
1009            region_name("aws-us-east-1"),
1010            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1011            vec![topology("empty", vec![])],
1012        );
1013
1014        assert!(
1015            config.is_ok(),
1016            "Topology with no regions should be valid: {:?}",
1017            config
1018        );
1019    }
1020
1021    #[test]
1022    fn duplicate_region_names() {
1023        let config = MultiCloudMultiRegionConfiguration::new(
1024            region_name("aws-us-east-1"),
1025            vec![
1026                provider_region("aws-us-east-1", "aws", "us-east-1"),
1027                provider_region("aws-us-east-1", "aws", "us-east-1"),
1028            ],
1029            vec![],
1030        );
1031
1032        let err = config.unwrap_err();
1033        assert_eq!(
1034            err.duplicate_region_names(),
1035            &[region_name("aws-us-east-1")]
1036        );
1037        assert!(err.duplicate_topology_names().is_empty());
1038        assert!(err.unknown_topology_regions().is_empty());
1039        assert_eq!(err.unknown_preferred_region(), None);
1040    }
1041
1042    #[test]
1043    fn duplicate_topology_names() {
1044        let config = MultiCloudMultiRegionConfiguration::new(
1045            region_name("aws-us-east-1"),
1046            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1047            vec![
1048                topology("global", vec!["aws-us-east-1"]),
1049                topology("global", vec!["aws-us-east-1"]),
1050            ],
1051        );
1052
1053        let err = config.unwrap_err();
1054        assert!(err.duplicate_region_names().is_empty());
1055        assert_eq!(err.duplicate_topology_names(), &[topology_name("global")]);
1056        assert!(err.unknown_topology_regions().is_empty());
1057        assert_eq!(err.unknown_preferred_region(), None);
1058    }
1059
1060    #[test]
1061    fn unknown_topology_region() {
1062        let config = MultiCloudMultiRegionConfiguration::new(
1063            region_name("aws-us-east-1"),
1064            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1065            vec![topology(
1066                "global",
1067                vec!["aws-us-east-1", "nonexistent-region"],
1068            )],
1069        );
1070
1071        let err = config.unwrap_err();
1072        assert!(err.duplicate_region_names().is_empty());
1073        assert!(err.duplicate_topology_names().is_empty());
1074        assert_eq!(
1075            err.unknown_topology_regions(),
1076            &[region_name("nonexistent-region")]
1077        );
1078        assert_eq!(err.unknown_preferred_region(), None);
1079    }
1080
1081    #[test]
1082    fn unknown_topology_region_duplicated_in_multiple_topologies() {
1083        // When the same unknown region is referenced in multiple topologies, it should only
1084        // appear once in the error output for cleaner, more deterministic error messages.
1085        let config = MultiCloudMultiRegionConfiguration::new(
1086            region_name("aws-us-east-1"),
1087            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1088            vec![
1089                topology("topo1", vec!["aws-us-east-1", "nonexistent"]),
1090                topology("topo2", vec!["nonexistent"]),
1091            ],
1092        );
1093
1094        let err = config.unwrap_err();
1095        assert!(err.duplicate_region_names().is_empty());
1096        assert!(err.duplicate_topology_names().is_empty());
1097        assert_eq!(
1098            err.unknown_topology_regions(),
1099            &[region_name("nonexistent")]
1100        );
1101        assert_eq!(err.unknown_preferred_region(), None);
1102    }
1103
1104    #[test]
1105    fn unknown_preferred_region() {
1106        let config = MultiCloudMultiRegionConfiguration::new(
1107            region_name("nonexistent-region"),
1108            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1109            vec![],
1110        );
1111
1112        let err = config.unwrap_err();
1113        assert!(err.duplicate_region_names().is_empty());
1114        assert!(err.duplicate_topology_names().is_empty());
1115        assert!(err.unknown_topology_regions().is_empty());
1116        assert_eq!(
1117            err.unknown_preferred_region(),
1118            Some(&region_name("nonexistent-region"))
1119        );
1120    }
1121
1122    #[test]
1123    fn multiple_validation_errors() {
1124        let config = MultiCloudMultiRegionConfiguration::new(
1125            region_name("nonexistent-preferred"),
1126            vec![
1127                provider_region("aws-us-east-1", "aws", "us-east-1"),
1128                provider_region("aws-us-east-1", "aws", "us-east-1"),
1129            ],
1130            vec![
1131                topology("topo1", vec!["unknown-region"]),
1132                topology("topo1", vec!["aws-us-east-1"]),
1133            ],
1134        );
1135
1136        let err = config.unwrap_err();
1137        assert_eq!(
1138            err.duplicate_region_names(),
1139            &[region_name("aws-us-east-1")]
1140        );
1141        assert_eq!(err.duplicate_topology_names(), &[topology_name("topo1")]);
1142        assert_eq!(
1143            err.unknown_topology_regions(),
1144            &[region_name("unknown-region")]
1145        );
1146        assert_eq!(
1147            err.unknown_preferred_region(),
1148            Some(&region_name("nonexistent-preferred"))
1149        );
1150    }
1151
1152    #[test]
1153    fn display_no_errors() {
1154        let error = ValidationError::default();
1155        assert_eq!(error.to_string(), "no validation errors");
1156    }
1157
1158    #[test]
1159    fn display_duplicate_region_names_only() {
1160        let error = ValidationError::new(
1161            vec![region_name("region-a"), region_name("region-b")],
1162            vec![],
1163            vec![],
1164            None,
1165        );
1166        assert_eq!(
1167            error.to_string(),
1168            "duplicate region names: region-a, region-b"
1169        );
1170    }
1171
1172    #[test]
1173    fn display_duplicate_topology_names_only() {
1174        let error = ValidationError::new(vec![], vec![topology_name("topo-x")], vec![], None);
1175        assert_eq!(error.to_string(), "duplicate topology names: topo-x");
1176    }
1177
1178    #[test]
1179    fn display_unknown_topology_regions_only() {
1180        let error = ValidationError::new(
1181            vec![],
1182            vec![],
1183            vec![region_name("missing-1"), region_name("missing-2")],
1184            None,
1185        );
1186        assert_eq!(
1187            error.to_string(),
1188            "unknown topology regions: missing-1, missing-2"
1189        );
1190    }
1191
1192    #[test]
1193    fn display_unknown_preferred_region_only() {
1194        let error =
1195            ValidationError::new(vec![], vec![], vec![], Some(region_name("missing-region")));
1196        assert_eq!(
1197            error.to_string(),
1198            "unknown preferred region: missing-region"
1199        );
1200    }
1201
1202    #[test]
1203    fn display_all_errors() {
1204        let error = ValidationError::new(
1205            vec![region_name("dup-region")],
1206            vec![topology_name("dup-topo")],
1207            vec![region_name("unknown-reg")],
1208            Some(region_name("bad-preferred")),
1209        );
1210        assert_eq!(
1211            error.to_string(),
1212            "duplicate region names: dup-region; duplicate topology names: dup-topo; unknown topology regions: unknown-reg; unknown preferred region: bad-preferred"
1213        );
1214    }
1215
1216    #[test]
1217    fn display_special_characters() {
1218        let error = ValidationError::new(
1219            vec![
1220                region_name("region-with-dash_and_underscore"),
1221                region_name("region with spaces"),
1222            ],
1223            vec![topology_name("topo.dot")],
1224            vec![region_name("region\nwith\nnewlines")],
1225            None,
1226        );
1227        assert_eq!(
1228            error.to_string(),
1229            "duplicate region names: region-with-dash_and_underscore, region with spaces; duplicate topology names: topo.dot; unknown topology regions: region\nwith\nnewlines"
1230        );
1231    }
1232
1233    #[test]
1234    fn validation_error_has_errors_default() {
1235        let error = ValidationError::default();
1236        assert!(!error.has_errors());
1237    }
1238
1239    #[test]
1240    fn validation_error_has_errors_with_duplicate_regions() {
1241        let error = ValidationError::new(vec![region_name("dup")], vec![], vec![], None);
1242        assert!(error.has_errors());
1243    }
1244
1245    #[test]
1246    fn validation_error_has_errors_with_duplicate_topologies() {
1247        let error = ValidationError::new(vec![], vec![topology_name("dup")], vec![], None);
1248        assert!(error.has_errors());
1249    }
1250
1251    #[test]
1252    fn validation_error_has_errors_with_unknown_topology_regions() {
1253        let error = ValidationError::new(vec![], vec![], vec![region_name("unknown")], None);
1254        assert!(error.has_errors());
1255    }
1256
1257    #[test]
1258    fn validation_error_has_errors_with_unknown_preferred() {
1259        let error = ValidationError::new(vec![], vec![], vec![], Some(region_name("unknown")));
1260        assert!(error.has_errors());
1261    }
1262
1263    #[test]
1264    fn configuration_clone() {
1265        let config = MultiCloudMultiRegionConfiguration::new(
1266            region_name("aws-us-east-1"),
1267            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1268            vec![],
1269        )
1270        .expect("valid configuration");
1271
1272        let cloned = config.clone();
1273        assert_eq!(config, cloned);
1274    }
1275
1276    #[test]
1277    fn configuration_debug() {
1278        let config = MultiCloudMultiRegionConfiguration::new(
1279            region_name("aws-us-east-1"),
1280            vec![provider_region("aws-us-east-1", "aws", "us-east-1")],
1281            vec![],
1282        )
1283        .expect("valid configuration");
1284
1285        let debug_str = format!("{:?}", config);
1286        assert!(debug_str.contains("MultiCloudMultiRegionConfiguration"));
1287        assert!(debug_str.contains("aws-us-east-1"));
1288    }
1289
1290    #[test]
1291    fn region_name_valid() {
1292        assert!(RegionName::new("aws-us-east-1").is_ok());
1293        assert!(RegionName::new("a").is_ok());
1294        // 32 chars exactly
1295        assert!(RegionName::new("12345678901234567890123456789012").is_ok());
1296    }
1297
1298    #[test]
1299    fn region_name_empty() {
1300        let result = RegionName::new("");
1301        assert!(result.is_err());
1302        println!(
1303            "region_name_empty error: {:?}",
1304            result.as_ref().unwrap_err()
1305        );
1306        assert!(matches!(result, Err(NameError::Empty)));
1307    }
1308
1309    #[test]
1310    fn region_name_too_long() {
1311        // 33 chars
1312        let result = RegionName::new("123456789012345678901234567890123");
1313        assert!(result.is_err());
1314        println!(
1315            "region_name_too_long error: {:?}",
1316            result.as_ref().unwrap_err()
1317        );
1318        assert!(matches!(result, Err(NameError::TooLong(33))));
1319    }
1320
1321    #[test]
1322    fn region_name_non_ascii() {
1323        let result = RegionName::new("region-🌍");
1324        assert!(result.is_err());
1325        println!(
1326            "region_name_non_ascii error: {:?}",
1327            result.as_ref().unwrap_err()
1328        );
1329        assert!(matches!(result, Err(NameError::NonAscii)));
1330    }
1331
1332    #[test]
1333    fn topology_name_valid() {
1334        assert!(TopologyName::new("global").is_ok());
1335        assert!(TopologyName::new("a").is_ok());
1336        // 32 chars exactly
1337        assert!(TopologyName::new("12345678901234567890123456789012").is_ok());
1338    }
1339
1340    #[test]
1341    fn topology_name_empty() {
1342        let result = TopologyName::new("");
1343        assert!(result.is_err());
1344        println!(
1345            "topology_name_empty error: {:?}",
1346            result.as_ref().unwrap_err()
1347        );
1348        assert!(matches!(result, Err(NameError::Empty)));
1349    }
1350
1351    #[test]
1352    fn topology_name_too_long() {
1353        // 33 chars
1354        let result = TopologyName::new("123456789012345678901234567890123");
1355        assert!(result.is_err());
1356        println!(
1357            "topology_name_too_long error: {:?}",
1358            result.as_ref().unwrap_err()
1359        );
1360        assert!(matches!(result, Err(NameError::TooLong(33))));
1361    }
1362
1363    #[test]
1364    fn topology_name_non_ascii() {
1365        let result = TopologyName::new("拓扑名");
1366        assert!(result.is_err());
1367        println!(
1368            "topology_name_non_ascii error: {:?}",
1369            result.as_ref().unwrap_err()
1370        );
1371        assert!(matches!(result, Err(NameError::NonAscii)));
1372    }
1373
1374    #[test]
1375    fn name_error_display_empty() {
1376        let err = NameError::Empty;
1377        assert_eq!(err.to_string(), "name cannot be empty");
1378    }
1379
1380    #[test]
1381    fn name_error_display_too_long() {
1382        let err = NameError::TooLong(50);
1383        assert_eq!(
1384            err.to_string(),
1385            "name exceeds maximum length of 32 characters: 50 characters"
1386        );
1387    }
1388
1389    #[test]
1390    fn name_error_display_non_ascii() {
1391        let err = NameError::NonAscii;
1392        assert_eq!(err.to_string(), "name contains non-ASCII characters");
1393    }
1394
1395    #[test]
1396    fn region_name_deserialize_valid() {
1397        let json = "\"aws-us-east-1\"";
1398        let name: RegionName = serde_json::from_str(json).unwrap();
1399        assert_eq!(name.as_str(), "aws-us-east-1");
1400    }
1401
1402    #[test]
1403    fn region_name_deserialize_empty() {
1404        let json = "\"\"";
1405        let result: Result<RegionName, _> = serde_json::from_str(json);
1406        assert!(result.is_err());
1407        let err_msg = result.unwrap_err().to_string();
1408        println!("region_name_deserialize_empty error: {}", err_msg);
1409        assert!(
1410            err_msg.contains("name cannot be empty"),
1411            "Expected error message to contain 'name cannot be empty', got: {}",
1412            err_msg
1413        );
1414    }
1415
1416    #[test]
1417    fn region_name_deserialize_too_long() {
1418        let json = "\"123456789012345678901234567890123\"";
1419        let result: Result<RegionName, _> = serde_json::from_str(json);
1420        assert!(result.is_err());
1421        let err_msg = result.unwrap_err().to_string();
1422        println!("region_name_deserialize_too_long error: {}", err_msg);
1423        assert!(
1424            err_msg.contains("name exceeds maximum length"),
1425            "Expected error message to contain 'name exceeds maximum length', got: {}",
1426            err_msg
1427        );
1428    }
1429
1430    #[test]
1431    fn region_name_deserialize_non_ascii() {
1432        let json = "\"region-🌍\"";
1433        let result: Result<RegionName, _> = serde_json::from_str(json);
1434        assert!(result.is_err());
1435        let err_msg = result.unwrap_err().to_string();
1436        println!("region_name_deserialize_non_ascii error: {}", err_msg);
1437        assert!(
1438            err_msg.contains("non-ASCII"),
1439            "Expected error message to contain 'non-ASCII', got: {}",
1440            err_msg
1441        );
1442    }
1443
1444    #[test]
1445    fn topology_name_deserialize_valid() {
1446        let json = "\"global\"";
1447        let name: TopologyName = serde_json::from_str(json).unwrap();
1448        assert_eq!(name.as_str(), "global");
1449    }
1450
1451    #[test]
1452    fn topology_name_deserialize_empty() {
1453        let json = "\"\"";
1454        let result: Result<TopologyName, _> = serde_json::from_str(json);
1455        assert!(result.is_err());
1456        let err_msg = result.unwrap_err().to_string();
1457        println!("topology_name_deserialize_empty error: {}", err_msg);
1458        assert!(
1459            err_msg.contains("name cannot be empty"),
1460            "Expected error message to contain 'name cannot be empty', got: {}",
1461            err_msg
1462        );
1463    }
1464
1465    #[test]
1466    fn topology_name_deserialize_too_long() {
1467        let json = "\"123456789012345678901234567890123\"";
1468        let result: Result<TopologyName, _> = serde_json::from_str(json);
1469        assert!(result.is_err());
1470        let err_msg = result.unwrap_err().to_string();
1471        println!("topology_name_deserialize_too_long error: {}", err_msg);
1472        assert!(
1473            err_msg.contains("name exceeds maximum length"),
1474            "Expected error message to contain 'name exceeds maximum length', got: {}",
1475            err_msg
1476        );
1477    }
1478
1479    #[test]
1480    fn topology_name_deserialize_non_ascii() {
1481        let json = "\"拓扑名\"";
1482        let result: Result<TopologyName, _> = serde_json::from_str(json);
1483        assert!(result.is_err());
1484        let err_msg = result.unwrap_err().to_string();
1485        println!("topology_name_deserialize_non_ascii error: {}", err_msg);
1486        assert!(
1487            err_msg.contains("non-ASCII"),
1488            "Expected error message to contain 'non-ASCII', got: {}",
1489            err_msg
1490        );
1491    }
1492}