Skip to main content

awsregion/
region.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use std::str::{self, FromStr};
4use std::{env, fmt};
5
6use crate::error::RegionError;
7
8/// AWS S3 [region identifier](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region),
9/// passing in custom values is also possible, in that case it is up to you to pass a valid endpoint,
10/// otherwise boom will happen :)
11///
12/// Serde support available with the `serde` feature
13///
14/// # Example
15/// ```
16/// use std::str::FromStr;
17/// use awsregion::Region;
18///
19/// // Parse from a string
20/// let region: Region = "us-east-1".parse().unwrap();
21///
22/// // Choose region directly
23/// let region = Region::EuWest2;
24///
25/// // Custom region requires valid region name and endpoint
26/// let region_name = "nl-ams".to_string();
27/// let endpoint = "https://s3.nl-ams.scw.cloud".to_string();
28/// let region = Region::Custom { region: region_name, endpoint };
29///
30/// ```
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum Region {
34    /// us-east-1
35    UsEast1,
36    /// us-east-2
37    UsEast2,
38    /// us-west-1
39    UsWest1,
40    /// us-west-2
41    UsWest2,
42    /// ca-central-1
43    CaCentral1,
44    /// ca-west-1
45    CaWest1,
46    /// af-south-1
47    AfSouth1,
48    /// ap-east-1
49    ApEast1,
50    /// ap-east-2
51    ApEast2,
52    /// ap-south-1
53    ApSouth1,
54    /// ap-south-2
55    ApSouth2,
56    /// ap-northeast-1
57    ApNortheast1,
58    /// ap-northeast-2
59    ApNortheast2,
60    /// ap-northeast-3
61    ApNortheast3,
62    /// ap-southeast-1
63    ApSoutheast1,
64    /// ap-southeast-2
65    ApSoutheast2,
66    /// ap-southeast-3
67    ApSoutheast3,
68    /// ap-southeast-4
69    ApSoutheast4,
70    /// ap-southeast-5
71    ApSoutheast5,
72    /// ap-southeast-6
73    ApSoutheast6,
74    /// ap-southeast-7
75    ApSoutheast7,
76    /// cn-north-1
77    CnNorth1,
78    /// cn-northwest-1
79    CnNorthwest1,
80    /// eu-north-1
81    EuNorth1,
82    /// eu-central-1
83    EuCentral1,
84    /// eu-central-2
85    EuCentral2,
86    /// eu-south-1
87    EuSouth1,
88    /// eu-south-2
89    EuSouth2,
90    /// eu-west-1
91    EuWest1,
92    /// eu-west-2
93    EuWest2,
94    /// eu-west-3
95    EuWest3,
96    /// il-central-1
97    IlCentral1,
98    /// me-south-1
99    MeSouth1,
100    /// me-central-1
101    MeCentral1,
102    /// mx-central-1
103    MxCentral1,
104    /// sa-east-1
105    SaEast1,
106    /// Digital Ocean nyc3
107    DoNyc3,
108    /// Digital Ocean ams3
109    DoAms3,
110    /// Digital Ocean sgp1
111    DoSgp1,
112    /// Digital Ocean fra1
113    DoFra1,
114    /// Yandex Object Storage
115    OvhGra,
116    /// gra
117    OvhRbx,
118    /// rbx
119    OvhSbg,
120    /// sbg
121    OvhDe,
122    /// de
123    OvhUk,
124    /// uk
125    OvhWaw,
126    /// waw
127    OvhBhs,
128    /// bhs
129    OvhCaEastTor,
130    /// ca-east-tor
131    OvhSgp,
132    /// sgp
133    Yandex,
134    /// Wasabi us-east-1
135    WaUsEast1,
136    /// Wasabi us-east-2
137    WaUsEast2,
138    /// Wasabi us-central-1
139    WaUsCentral1,
140    /// Wasabi us-west-1
141    WaUsWest1,
142    /// Wasabi ca-central-1
143    WaCaCentral1,
144    /// Wasabi eu-central-1
145    WaEuCentral1,
146    /// Wasabi eu-central-2
147    WaEuCentral2,
148    /// Wasabi eu-west-1
149    WaEuWest1,
150    /// Wasabi eu-west-2
151    WaEuWest2,
152    /// Wasabi ap-northeast-1
153    WaApNortheast1,
154    /// Wasabi ap-northeast-2
155    WaApNortheast2,
156    /// Wasabi ap-southeast-1
157    WaApSoutheast1,
158    /// Wasabi ap-southeast-2
159    WaApSoutheast2,
160    /// Cloudflare R2 (global)
161    R2 { account_id: String },
162    /// Cloudflare R2 EU jurisdiction
163    R2Eu { account_id: String },
164    /// Custom region
165    Custom { region: String, endpoint: String },
166}
167
168impl fmt::Display for Region {
169    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170        use self::Region::*;
171        match *self {
172            UsEast1 => write!(f, "us-east-1"),
173            UsEast2 => write!(f, "us-east-2"),
174            UsWest1 => write!(f, "us-west-1"),
175            UsWest2 => write!(f, "us-west-2"),
176            AfSouth1 => write!(f, "af-south-1"),
177            CaCentral1 => write!(f, "ca-central-1"),
178            CaWest1 => write!(f, "ca-west-1"),
179            ApEast1 => write!(f, "ap-east-1"),
180            ApEast2 => write!(f, "ap-east-2"),
181            ApSouth1 => write!(f, "ap-south-1"),
182            ApSouth2 => write!(f, "ap-south-2"),
183            ApNortheast1 => write!(f, "ap-northeast-1"),
184            ApNortheast2 => write!(f, "ap-northeast-2"),
185            ApNortheast3 => write!(f, "ap-northeast-3"),
186            ApSoutheast1 => write!(f, "ap-southeast-1"),
187            ApSoutheast2 => write!(f, "ap-southeast-2"),
188            ApSoutheast3 => write!(f, "ap-southeast-3"),
189            ApSoutheast4 => write!(f, "ap-southeast-4"),
190            ApSoutheast5 => write!(f, "ap-southeast-5"),
191            ApSoutheast6 => write!(f, "ap-southeast-6"),
192            ApSoutheast7 => write!(f, "ap-southeast-7"),
193            CnNorth1 => write!(f, "cn-north-1"),
194            CnNorthwest1 => write!(f, "cn-northwest-1"),
195            EuNorth1 => write!(f, "eu-north-1"),
196            EuCentral1 => write!(f, "eu-central-1"),
197            EuCentral2 => write!(f, "eu-central-2"),
198            EuSouth1 => write!(f, "eu-south-1"),
199            EuSouth2 => write!(f, "eu-south-2"),
200            EuWest1 => write!(f, "eu-west-1"),
201            EuWest2 => write!(f, "eu-west-2"),
202            EuWest3 => write!(f, "eu-west-3"),
203            SaEast1 => write!(f, "sa-east-1"),
204            IlCentral1 => write!(f, "il-central-1"),
205            MeCentral1 => write!(f, "me-central-1"),
206            MeSouth1 => write!(f, "me-south-1"),
207            MxCentral1 => write!(f, "mx-central-1"),
208            DoNyc3 => write!(f, "nyc3"),
209            DoAms3 => write!(f, "ams3"),
210            DoSgp1 => write!(f, "sgp1"),
211            DoFra1 => write!(f, "fra1"),
212            Yandex => write!(f, "ru-central1"),
213            WaUsEast1 => write!(f, "us-east-1"),
214            WaUsEast2 => write!(f, "us-east-2"),
215            WaUsCentral1 => write!(f, "us-central-1"),
216            WaUsWest1 => write!(f, "us-west-1"),
217            WaCaCentral1 => write!(f, "ca-central-1"),
218            WaEuCentral1 => write!(f, "eu-central-1"),
219            WaEuCentral2 => write!(f, "eu-central-2"),
220            WaEuWest1 => write!(f, "eu-west-1"),
221            WaEuWest2 => write!(f, "eu-west-2"),
222            WaApNortheast1 => write!(f, "ap-northeast-1"),
223            WaApNortheast2 => write!(f, "ap-northeast-2"),
224            WaApSoutheast1 => write!(f, "ap-southeast-1"),
225            WaApSoutheast2 => write!(f, "ap-southeast-2"),
226            OvhGra => write!(f, "gra"),
227            OvhRbx => write!(f, "rbx"),
228            OvhSbg => write!(f, "sbg"),
229            OvhDe => write!(f, "de"),
230            OvhUk => write!(f, "uk"),
231            OvhWaw => write!(f, "waw"),
232            OvhBhs => write!(f, "bhs"),
233            OvhCaEastTor => write!(f, "ca-east-tor"),
234            OvhSgp => write!(f, "sgp"),
235            R2 { .. } => write!(f, "auto"),
236            R2Eu { .. } => write!(f, "auto"),
237            Custom { ref region, .. } => write!(f, "{}", region),
238        }
239    }
240}
241
242impl FromStr for Region {
243    type Err = std::str::Utf8Error;
244
245    fn from_str(s: &str) -> Result<Self, Self::Err> {
246        use self::Region::*;
247        match s {
248            "us-east-1" => Ok(UsEast1),
249            "us-east-2" => Ok(UsEast2),
250            "us-west-1" => Ok(UsWest1),
251            "us-west-2" => Ok(UsWest2),
252            "ca-central-1" => Ok(CaCentral1),
253            "ca-west-1" => Ok(CaWest1),
254            "af-south-1" => Ok(AfSouth1),
255            "ap-east-1" => Ok(ApEast1),
256            "ap-east-2" => Ok(ApEast2),
257            "ap-south-1" => Ok(ApSouth1),
258            "ap-south-2" => Ok(ApSouth2),
259            "ap-northeast-1" => Ok(ApNortheast1),
260            "ap-northeast-2" => Ok(ApNortheast2),
261            "ap-northeast-3" => Ok(ApNortheast3),
262            "ap-southeast-1" => Ok(ApSoutheast1),
263            "ap-southeast-2" => Ok(ApSoutheast2),
264            "ap-southeast-3" => Ok(ApSoutheast3),
265            "ap-southeast-4" => Ok(ApSoutheast4),
266            "ap-southeast-5" => Ok(ApSoutheast5),
267            "ap-southeast-6" => Ok(ApSoutheast6),
268            "ap-southeast-7" => Ok(ApSoutheast7),
269            "cn-north-1" => Ok(CnNorth1),
270            "cn-northwest-1" => Ok(CnNorthwest1),
271            "eu-north-1" => Ok(EuNorth1),
272            "eu-central-1" => Ok(EuCentral1),
273            "eu-central-2" => Ok(EuCentral2),
274            "eu-south-1" => Ok(EuSouth1),
275            "eu-south-2" => Ok(EuSouth2),
276            "eu-west-1" => Ok(EuWest1),
277            "eu-west-2" => Ok(EuWest2),
278            "eu-west-3" => Ok(EuWest3),
279            "sa-east-1" => Ok(SaEast1),
280            "il-central-1" => Ok(IlCentral1),
281            "me-central-1" => Ok(MeCentral1),
282            "me-south-1" => Ok(MeSouth1),
283            "mx-central-1" => Ok(MxCentral1),
284            "nyc3" => Ok(DoNyc3),
285            "ams3" => Ok(DoAms3),
286            "sgp1" => Ok(DoSgp1),
287            "fra1" => Ok(DoFra1),
288            "yandex" => Ok(Yandex),
289            "ru-central1" => Ok(Yandex),
290            "wa-us-east-1" => Ok(WaUsEast1),
291            "wa-us-east-2" => Ok(WaUsEast2),
292            "wa-us-central-1" => Ok(WaUsCentral1),
293            "wa-us-west-1" => Ok(WaUsWest1),
294            "wa-ca-central-1" => Ok(WaCaCentral1),
295            "wa-eu-central-1" => Ok(WaEuCentral1),
296            "wa-eu-central-2" => Ok(WaEuCentral2),
297            "wa-eu-west-1" => Ok(WaEuWest1),
298            "wa-eu-west-2" => Ok(WaEuWest2),
299            "wa-ap-northeast-1" => Ok(WaApNortheast1),
300            "wa-ap-northeast-2" => Ok(WaApNortheast2),
301            "wa-ap-southeast-1" => Ok(WaApSoutheast1),
302            "wa-ap-southeast-2" => Ok(WaApSoutheast2),
303            x => Ok(Custom {
304                region: x.to_string(),
305                endpoint: x.to_string(),
306            }),
307        }
308    }
309}
310
311impl Region {
312    pub fn endpoint(&self) -> String {
313        use self::Region::*;
314        match *self {
315            // Surprisingly, us-east-1 does not have a
316            // s3-us-east-1.amazonaws.com DNS record
317            UsEast1 => String::from("s3.amazonaws.com"),
318            UsEast2 => String::from("s3-us-east-2.amazonaws.com"),
319            UsWest1 => String::from("s3-us-west-1.amazonaws.com"),
320            UsWest2 => String::from("s3-us-west-2.amazonaws.com"),
321            CaCentral1 => String::from("s3-ca-central-1.amazonaws.com"),
322            CaWest1 => String::from("s3.ca-west-1.amazonaws.com"),
323            AfSouth1 => String::from("s3-af-south-1.amazonaws.com"),
324            ApEast1 => String::from("s3-ap-east-1.amazonaws.com"),
325            ApEast2 => String::from("s3.ap-east-2.amazonaws.com"),
326            ApSouth1 => String::from("s3-ap-south-1.amazonaws.com"),
327            ApSouth2 => String::from("s3.ap-south-2.amazonaws.com"),
328            ApNortheast1 => String::from("s3-ap-northeast-1.amazonaws.com"),
329            ApNortheast2 => String::from("s3-ap-northeast-2.amazonaws.com"),
330            ApNortheast3 => String::from("s3-ap-northeast-3.amazonaws.com"),
331            ApSoutheast1 => String::from("s3-ap-southeast-1.amazonaws.com"),
332            ApSoutheast2 => String::from("s3-ap-southeast-2.amazonaws.com"),
333            ApSoutheast3 => String::from("s3.ap-southeast-3.amazonaws.com"),
334            ApSoutheast4 => String::from("s3.ap-southeast-4.amazonaws.com"),
335            ApSoutheast5 => String::from("s3.ap-southeast-5.amazonaws.com"),
336            ApSoutheast6 => String::from("s3.ap-southeast-6.amazonaws.com"),
337            ApSoutheast7 => String::from("s3.ap-southeast-7.amazonaws.com"),
338            CnNorth1 => String::from("s3.cn-north-1.amazonaws.com.cn"),
339            CnNorthwest1 => String::from("s3.cn-northwest-1.amazonaws.com.cn"),
340            EuNorth1 => String::from("s3-eu-north-1.amazonaws.com"),
341            EuCentral1 => String::from("s3.eu-central-1.amazonaws.com"),
342            EuCentral2 => String::from("s3.eu-central-2.amazonaws.com"),
343            EuSouth1 => String::from("s3.eu-south-1.amazonaws.com"),
344            EuSouth2 => String::from("s3.eu-south-2.amazonaws.com"),
345            EuWest1 => String::from("s3-eu-west-1.amazonaws.com"),
346            EuWest2 => String::from("s3-eu-west-2.amazonaws.com"),
347            EuWest3 => String::from("s3-eu-west-3.amazonaws.com"),
348            SaEast1 => String::from("s3-sa-east-1.amazonaws.com"),
349            IlCentral1 => String::from("s3.il-central-1.amazonaws.com"),
350            MeCentral1 => String::from("s3.me-central-1.amazonaws.com"),
351            MeSouth1 => String::from("s3-me-south-1.amazonaws.com"),
352            MxCentral1 => String::from("s3.mx-central-1.amazonaws.com"),
353            DoNyc3 => String::from("nyc3.digitaloceanspaces.com"),
354            DoAms3 => String::from("ams3.digitaloceanspaces.com"),
355            DoSgp1 => String::from("sgp1.digitaloceanspaces.com"),
356            DoFra1 => String::from("fra1.digitaloceanspaces.com"),
357            Yandex => String::from("storage.yandexcloud.net"),
358            WaUsEast1 => String::from("s3.us-east-1.wasabisys.com"),
359            WaUsEast2 => String::from("s3.us-east-2.wasabisys.com"),
360            WaUsCentral1 => String::from("s3.us-central-1.wasabisys.com"),
361            WaUsWest1 => String::from("s3.us-west-1.wasabisys.com"),
362            WaCaCentral1 => String::from("s3.ca-central-1.wasabisys.com"),
363            WaEuCentral1 => String::from("s3.eu-central-1.wasabisys.com"),
364            WaEuCentral2 => String::from("s3.eu-central-2.wasabisys.com"),
365            WaEuWest1 => String::from("s3.eu-west-1.wasabisys.com"),
366            WaEuWest2 => String::from("s3.eu-west-2.wasabisys.com"),
367            WaApNortheast1 => String::from("s3.ap-northeast-1.wasabisys.com"),
368            WaApNortheast2 => String::from("s3.ap-northeast-2.wasabisys.com"),
369            WaApSoutheast1 => String::from("s3.ap-southeast-1.wasabisys.com"),
370            WaApSoutheast2 => String::from("s3.ap-southeast-2.wasabisys.com"),
371            OvhGra => String::from("s3.gra.io.cloud.ovh.net"),
372            OvhRbx => String::from("s3.rbx.io.cloud.ovh.net"),
373            OvhSbg => String::from("s3.sbg.io.cloud.ovh.net"),
374            OvhDe => String::from("s3.de.io.cloud.ovh.net"),
375            OvhUk => String::from("s3.uk.io.cloud.ovh.net"),
376            OvhWaw => String::from("s3.waw.io.cloud.ovh.net"),
377            OvhBhs => String::from("s3.bhs.io.cloud.ovh.net"),
378            OvhCaEastTor => String::from("s3.ca-east-tor.io.cloud.ovh.net"),
379            OvhSgp => String::from("s3.sgp.io.cloud.ovh.net"),
380            R2 { ref account_id } => format!("{}.r2.cloudflarestorage.com", account_id),
381            R2Eu { ref account_id } => format!("{}.eu.r2.cloudflarestorage.com", account_id),
382            Custom { ref endpoint, .. } => endpoint.to_string(),
383        }
384    }
385
386    pub fn scheme(&self) -> String {
387        match *self {
388            Region::Custom { ref endpoint, .. } => match endpoint.find("://") {
389                Some(pos) => endpoint[..pos].to_string(),
390                None => "https".to_string(),
391            },
392            _ => "https".to_string(),
393        }
394    }
395
396    pub fn host(&self) -> String {
397        match *self {
398            Region::Custom { ref endpoint, .. } => {
399                let host = match endpoint.find("://") {
400                    Some(pos) => endpoint[pos + 3..].to_string(),
401                    None => endpoint.to_string(),
402                };
403                // Remove trailing slashes to avoid signature mismatches
404                // AWS CLI and other SDKs handle this similarly
405                host.trim_end_matches('/').to_string()
406            }
407            _ => self.endpoint(),
408        }
409    }
410
411    pub fn from_env(region_env: &str, endpoint_env: Option<&str>) -> Result<Region, RegionError> {
412        if let Some(endpoint_env) = endpoint_env {
413            Ok(Region::Custom {
414                region: env::var(region_env)?,
415                endpoint: env::var(endpoint_env)?,
416            })
417        } else {
418            Ok(Region::from_str(&env::var(region_env)?)?)
419        }
420    }
421
422    /// Attempts to create a Region from AWS_REGION and AWS_ENDPOINT environment variables
423    pub fn from_default_env() -> Result<Region, RegionError> {
424        if let Ok(endpoint) = env::var("AWS_ENDPOINT") {
425            Ok(Region::Custom {
426                region: env::var("AWS_REGION")?,
427                endpoint,
428            })
429        } else {
430            Ok(Region::from_str(&env::var("AWS_REGION")?)?)
431        }
432    }
433}
434
435#[test]
436fn yandex_object_storage() {
437    let yandex = Region::Custom {
438        endpoint: "storage.yandexcloud.net".to_string(),
439        region: "ru-central1".to_string(),
440    };
441
442    let yandex_region = "ru-central1".parse::<Region>().unwrap();
443
444    assert_eq!(yandex.endpoint(), yandex_region.endpoint());
445
446    assert_eq!(yandex.to_string(), yandex_region.to_string());
447}
448
449#[test]
450fn test_region_eu_central_2() {
451    let region = "eu-central-2".parse::<Region>().unwrap();
452    assert_eq!(region.endpoint(), "s3.eu-central-2.amazonaws.com");
453}
454
455#[test]
456fn test_region_me_central_1() {
457    let region = "me-central-1".parse::<Region>().unwrap();
458    assert_eq!(region.endpoint(), "s3.me-central-1.amazonaws.com");
459    assert_eq!(region.to_string(), "me-central-1");
460}
461
462#[test]
463fn test_region_ap_east_2() {
464    let region = "ap-east-2".parse::<Region>().unwrap();
465    assert_eq!(region.endpoint(), "s3.ap-east-2.amazonaws.com");
466    assert_eq!(region.to_string(), "ap-east-2");
467}
468
469#[test]
470fn test_region_ap_south_2() {
471    let region = "ap-south-2".parse::<Region>().unwrap();
472    assert_eq!(region.endpoint(), "s3.ap-south-2.amazonaws.com");
473    assert_eq!(region.to_string(), "ap-south-2");
474}
475
476#[test]
477fn test_region_ap_southeast_3() {
478    let region = "ap-southeast-3".parse::<Region>().unwrap();
479    assert_eq!(region.endpoint(), "s3.ap-southeast-3.amazonaws.com");
480    assert_eq!(region.to_string(), "ap-southeast-3");
481}
482
483#[test]
484fn test_region_ap_southeast_4() {
485    let region = "ap-southeast-4".parse::<Region>().unwrap();
486    assert_eq!(region.endpoint(), "s3.ap-southeast-4.amazonaws.com");
487    assert_eq!(region.to_string(), "ap-southeast-4");
488}
489
490#[test]
491fn test_region_ap_southeast_5() {
492    let region = "ap-southeast-5".parse::<Region>().unwrap();
493    assert_eq!(region.endpoint(), "s3.ap-southeast-5.amazonaws.com");
494    assert_eq!(region.to_string(), "ap-southeast-5");
495}
496
497#[test]
498fn test_region_ap_southeast_6() {
499    let region = "ap-southeast-6".parse::<Region>().unwrap();
500    assert_eq!(region.endpoint(), "s3.ap-southeast-6.amazonaws.com");
501    assert_eq!(region.to_string(), "ap-southeast-6");
502}
503
504#[test]
505fn test_region_ap_southeast_7() {
506    let region = "ap-southeast-7".parse::<Region>().unwrap();
507    assert_eq!(region.endpoint(), "s3.ap-southeast-7.amazonaws.com");
508    assert_eq!(region.to_string(), "ap-southeast-7");
509}
510
511#[test]
512fn test_region_ca_west_1() {
513    let region = "ca-west-1".parse::<Region>().unwrap();
514    assert_eq!(region.endpoint(), "s3.ca-west-1.amazonaws.com");
515    assert_eq!(region.to_string(), "ca-west-1");
516}
517
518#[test]
519fn test_region_eu_south_1() {
520    let region = "eu-south-1".parse::<Region>().unwrap();
521    assert_eq!(region.endpoint(), "s3.eu-south-1.amazonaws.com");
522    assert_eq!(region.to_string(), "eu-south-1");
523}
524
525#[test]
526fn test_region_eu_south_2() {
527    let region = "eu-south-2".parse::<Region>().unwrap();
528    assert_eq!(region.endpoint(), "s3.eu-south-2.amazonaws.com");
529    assert_eq!(region.to_string(), "eu-south-2");
530}
531
532#[test]
533fn test_region_mx_central_1() {
534    let region = "mx-central-1".parse::<Region>().unwrap();
535    assert_eq!(region.endpoint(), "s3.mx-central-1.amazonaws.com");
536    assert_eq!(region.to_string(), "mx-central-1");
537}
538
539#[test]
540fn test_custom_endpoint_trailing_slash() {
541    // Test that trailing slashes are removed from custom endpoints
542    let region_with_slash = Region::Custom {
543        region: "eu-central-1".to_owned(),
544        endpoint: "https://s3.gra.io.cloud.ovh.net/".to_owned(),
545    };
546    assert_eq!(region_with_slash.host(), "s3.gra.io.cloud.ovh.net");
547
548    // Test without trailing slash
549    let region_without_slash = Region::Custom {
550        region: "eu-central-1".to_owned(),
551        endpoint: "https://s3.gra.io.cloud.ovh.net".to_owned(),
552    };
553    assert_eq!(region_without_slash.host(), "s3.gra.io.cloud.ovh.net");
554
555    // Test multiple trailing slashes
556    let region_multiple_slashes = Region::Custom {
557        region: "eu-central-1".to_owned(),
558        endpoint: "https://s3.example.com///".to_owned(),
559    };
560    assert_eq!(region_multiple_slashes.host(), "s3.example.com");
561
562    // Test with port and trailing slash
563    let region_with_port = Region::Custom {
564        region: "eu-central-1".to_owned(),
565        endpoint: "http://localhost:9000/".to_owned(),
566    };
567    assert_eq!(region_with_port.host(), "localhost:9000");
568}