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