1use fake::{Dummy, Faker};
2use miette::Diagnostic;
3use serde::{Deserialize, Serialize};
4use std::{fmt::Display, str::FromStr};
5use thiserror::Error;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum Region {
13 #[serde(
14 serialize_with = "AwsRegion::serialize_with_suffix",
15 deserialize_with = "AwsRegion::deserialize_with_suffix"
16 )]
17 Aws(AwsRegion),
18}
19
20impl FromStr for Region {
21 type Err = RegionError;
22
23 #[inline]
24 fn from_str(s: &str) -> Result<Self, Self::Err> {
25 Region::new(s)
26 }
27}
28
29impl Display for Region {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 Region::Aws(region) => write!(f, "{}.aws", region),
33 }
34 }
35}
36
37impl PartialEq<&str> for Region {
38 fn eq(&self, other: &&str) -> bool {
39 self.identifier() == *other
40 }
41}
42
43#[derive(Debug, Error, Diagnostic, PartialEq, Eq)]
44pub enum RegionError {
45 #[error("Invalid region: {0}")]
48 #[diagnostic(help(
49 "Region identifiers are in the format `<region>.<provider>` (e.g. 'us-west-2.aws')"
50 ))]
51 InvalidRegion(String),
52
53 #[error("Host or endpoint does not contain a valid region: `{0}`")]
54 InvalidHostFqdn(String),
55}
56
57impl Region {
58 pub fn all() -> Vec<Self> {
59 AwsRegion::all().into_iter().map(Self::Aws).collect()
60 }
61
62 pub fn new(identifier: &str) -> Result<Self, RegionError> {
76 if identifier.ends_with(".aws") {
77 let region = identifier.trim_end_matches(".aws");
78 Self::aws(region)
79 } else {
80 Err(RegionError::InvalidRegion(format!(
81 "Missing or unknown provider (e.g. '.aws' suffix on '{identifier}')"
82 )))
83 }
84 }
85
86 pub fn aws(identifier: &str) -> Result<Self, RegionError> {
98 AwsRegion::try_from(identifier).map(Self::Aws)
99 }
100
101 pub fn identifier(&self) -> String {
102 match self {
103 Region::Aws(region) => format!("{}.aws", region.identifier()),
104 }
105 }
106}
107
108#[cfg(feature = "test_utils")]
109impl Dummy<Faker> for Region {
110 fn dummy_with_rng<R>(_: &Faker, rng: &mut R) -> Self
111 where
112 R: rand::Rng + ?Sized,
113 {
114 let aws_regions = AwsRegion::all();
115 let choice = rng.gen_range(0..aws_regions.len());
116 Region::Aws(aws_regions[choice])
117 }
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "kebab-case")]
122pub enum AwsRegion {
123 ApSoutheast2,
124 EuCentral1,
125 EuWest1,
126 UsEast1,
127 UsEast2,
128 UsWest1,
129 UsWest2,
130}
131
132impl AwsRegion {
133 pub fn all() -> Vec<Self> {
134 vec![
135 Self::ApSoutheast2,
136 Self::EuCentral1,
137 Self::EuWest1,
138 Self::UsEast1,
139 Self::UsEast2,
140 Self::UsWest1,
141 Self::UsWest2,
142 ]
143 }
144
145 pub fn identifier(&self) -> &'static str {
146 match self {
147 Self::ApSoutheast2 => "ap-southeast-2",
148 Self::EuCentral1 => "eu-central-1",
149 Self::EuWest1 => "eu-west-1",
150 Self::UsEast1 => "us-east-1",
151 Self::UsEast2 => "us-east-2",
152 Self::UsWest1 => "us-west-1",
153 Self::UsWest2 => "us-west-2",
154 }
155 }
156
157 pub fn serialize_with_suffix<S>(region: &AwsRegion, serializer: S) -> Result<S::Ok, S::Error>
158 where
159 S: serde::Serializer,
160 {
161 serializer.serialize_str(&format!("{}.aws", region.identifier()))
162 }
163
164 pub fn deserialize_with_suffix<'de, D>(deserializer: D) -> Result<AwsRegion, D::Error>
165 where
166 D: serde::Deserializer<'de>,
167 {
168 let region = String::deserialize(deserializer)?;
169 region
170 .trim_end_matches(".aws")
171 .try_into()
172 .map_err(serde::de::Error::custom)
173 }
174}
175
176impl TryFrom<&str> for AwsRegion {
177 type Error = RegionError;
178
179 fn try_from(value: &str) -> Result<Self, Self::Error> {
180 match value {
181 "ap-southeast-2" => Ok(AwsRegion::ApSoutheast2),
182 "eu-central-1" => Ok(AwsRegion::EuCentral1),
183 "eu-west-1" => Ok(AwsRegion::EuWest1),
184 "us-east-1" => Ok(AwsRegion::UsEast1),
185 "us-east-2" => Ok(AwsRegion::UsEast2),
186 "us-west-1" => Ok(AwsRegion::UsWest1),
187 "us-west-2" => Ok(AwsRegion::UsWest2),
188
189 _ => Err(RegionError::InvalidRegion(value.to_string())),
190 }
191 }
192}
193
194impl TryFrom<(&str, &str)> for Region {
207 type Error = RegionError;
208
209 fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
210 if value.1 == "aws" {
211 AwsRegion::try_from(value.0).map(Region::Aws)
212 } else {
213 Err(RegionError::InvalidRegion(format!(
214 "Invalid region: {}",
215 value.0
216 )))
217 }
218 }
219}
220
221impl Display for AwsRegion {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 write!(f, "{}", self.identifier())
224 }
225}
226
227#[cfg(test)]
228mod test {
229 use super::*;
230
231 #[test]
232 fn test_region_new() {
233 assert_eq!(
234 Region::new("us-west-1.aws").unwrap(),
235 Region::Aws(AwsRegion::UsWest1)
236 );
237 assert_eq!(
238 Region::new("us-west-2.aws").unwrap(),
239 Region::Aws(AwsRegion::UsWest2)
240 );
241 assert_eq!(
242 Region::new("us-east-1.aws").unwrap(),
243 Region::Aws(AwsRegion::UsEast1)
244 );
245 assert_eq!(
246 Region::new("us-east-2.aws").unwrap(),
247 Region::Aws(AwsRegion::UsEast2)
248 );
249 assert_eq!(
250 Region::new("eu-west-1.aws").unwrap(),
251 Region::Aws(AwsRegion::EuWest1)
252 );
253 assert_eq!(
254 Region::new("eu-central-1.aws").unwrap(),
255 Region::Aws(AwsRegion::EuCentral1)
256 );
257 assert_eq!(
258 Region::new("ap-southeast-2.aws").unwrap(),
259 Region::Aws(AwsRegion::ApSoutheast2)
260 );
261 }
262
263 #[test]
264 fn test_region_new_invalid() {
265 let region = Region::new("us-west-2");
266 assert!(region.is_err());
267 }
268
269 #[test]
270 fn test_region_aws() {
271 let region = Region::aws("us-west-2").unwrap();
272 assert_eq!(region, Region::Aws(AwsRegion::UsWest2));
273 }
274
275 #[test]
276 fn test_region_aws_invalid() {
277 let region = Region::aws("us-west-3");
278 assert!(region.is_err());
279 }
280
281 #[test]
282 fn test_region_identifier() {
283 let region = Region::aws("us-west-2").unwrap();
284 assert_eq!(region.identifier(), "us-west-2.aws");
285 }
286
287 #[test]
288 fn test_region_from_string() {
289 let region = Region::from_str("us-west-2.aws").unwrap();
290 assert_eq!(region, Region::Aws(AwsRegion::UsWest2));
291
292 let region = Region::from_str("ap-southeast-2.aws").unwrap();
293 assert_eq!(region, Region::Aws(AwsRegion::ApSoutheast2));
294 }
295
296 #[test]
297 fn test_region_from_string_invalid_provider() {
298 let region = Region::from_str("us-west-2.gcp");
299 assert_eq!(
300 region,
301 Err(RegionError::InvalidRegion(
302 "Missing or unknown provider (e.g. '.aws' suffix on 'us-west-2.gcp')".to_string()
303 ))
304 );
305 }
306
307 #[test]
308 fn test_region_from_string_invalid_region() {
309 let region = Region::from_str("us-invalid-2.aws");
310 assert_eq!(
311 region,
312 Err(RegionError::InvalidRegion("us-invalid-2".to_string()))
313 );
314 }
315
316 mod aws {
317 use super::*;
318
319 #[test]
320 fn test_aws_region_identifier() {
321 assert_eq!(AwsRegion::UsWest1.identifier(), "us-west-1");
322 assert_eq!(AwsRegion::UsWest2.identifier(), "us-west-2");
323 assert_eq!(AwsRegion::UsEast1.identifier(), "us-east-1");
324 assert_eq!(AwsRegion::UsEast2.identifier(), "us-east-2");
325 assert_eq!(AwsRegion::EuWest1.identifier(), "eu-west-1");
326 assert_eq!(AwsRegion::EuCentral1.identifier(), "eu-central-1");
327 assert_eq!(AwsRegion::ApSoutheast2.identifier(), "ap-southeast-2");
328 }
329
330 #[test]
331 fn test_display() {
332 assert_eq!(Region::Aws(AwsRegion::UsWest1).to_string(), "us-west-1.aws");
333 assert_eq!(Region::Aws(AwsRegion::UsWest2).to_string(), "us-west-2.aws");
334 assert_eq!(Region::Aws(AwsRegion::UsEast1).to_string(), "us-east-1.aws");
335 assert_eq!(Region::Aws(AwsRegion::UsEast2).to_string(), "us-east-2.aws");
336 assert_eq!(Region::Aws(AwsRegion::EuWest1).to_string(), "eu-west-1.aws");
337 assert_eq!(
338 Region::Aws(AwsRegion::EuCentral1).to_string(),
339 "eu-central-1.aws"
340 );
341 assert_eq!(
342 Region::Aws(AwsRegion::ApSoutheast2).to_string(),
343 "ap-southeast-2.aws"
344 );
345 }
346 }
347}