1use fake::{Dummy, Faker};
2use serde::{Deserialize, Serialize};
3use std::fmt::Display;
4use std::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 = String;
22
23 fn from_str(s: &str) -> Result<Self, Self::Err> {
24 Region::new(s).map_err(|e| e.to_string())
25 }
26}
27
28impl Display for Region {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Region::Aws(region) => write!(f, "{}", region),
32 }
33 }
34}
35
36#[derive(Debug, Error)]
37pub enum RegionError {
38 #[error("Invalid region: {0}")]
39 InvalidRegion(String),
40
41 #[error("Host or endpoint does not contain a valid region: `{0}`")]
42 InvalidHostFqdn(String),
43}
44
45impl Region {
46 pub fn all() -> Vec<Self> {
47 AwsRegion::all().into_iter().map(Self::Aws).collect()
48 }
49
50 pub fn new(identifier: &str) -> Result<Self, RegionError> {
64 if identifier.ends_with(".aws") {
65 let region = identifier.trim_end_matches(".aws");
66 Self::aws(region)
67 } else {
68 Err(RegionError::InvalidRegion(format!(
69 "Missing provider (e.g. '.aws' suffix on '{identifier}')"
70 )))
71 }
72 }
73
74 pub fn aws(identifier: &str) -> Result<Self, RegionError> {
86 AwsRegion::try_from(identifier).map(Self::Aws)
87 }
88
89 pub fn identifier(&self) -> String {
90 match self {
91 Region::Aws(region) => format!("{}.aws", region.identifier()),
92 }
93 }
94}
95
96#[cfg(feature = "test_utils")]
97impl Dummy<Faker> for Region {
98 fn dummy_with_rng<R>(_: &Faker, rng: &mut R) -> Self
99 where
100 R: rand::Rng + ?Sized,
101 {
102 let aws_regions = AwsRegion::all();
103 let choice = rng.gen_range(0..aws_regions.len());
104 Region::Aws(aws_regions[choice])
105 }
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "kebab-case")]
110pub enum AwsRegion {
111 ApSoutheast2,
112 EuCentral1,
113 EuWest1,
114 UsEast1,
115 UsEast2,
116 UsWest1,
117 UsWest2,
118}
119
120impl AwsRegion {
121 pub fn all() -> Vec<Self> {
122 vec![
123 Self::ApSoutheast2,
124 Self::EuCentral1,
125 Self::EuWest1,
126 Self::UsEast1,
127 Self::UsEast2,
128 Self::UsWest1,
129 Self::UsWest2,
130 ]
131 }
132
133 pub fn identifier(&self) -> &'static str {
134 match self {
135 Self::ApSoutheast2 => "ap-southeast-2",
136 Self::EuCentral1 => "eu-central-1",
137 Self::EuWest1 => "eu-west-1",
138 Self::UsEast1 => "us-east-1",
139 Self::UsEast2 => "us-east-2",
140 Self::UsWest1 => "us-west-1",
141 Self::UsWest2 => "us-west-2",
142 }
143 }
144
145 pub fn serialize_with_suffix<S>(region: &AwsRegion, serializer: S) -> Result<S::Ok, S::Error>
146 where
147 S: serde::Serializer,
148 {
149 serializer.serialize_str(&format!("{}.aws", region.identifier()))
150 }
151
152 pub fn deserialize_with_suffix<'de, D>(deserializer: D) -> Result<AwsRegion, D::Error>
153 where
154 D: serde::Deserializer<'de>,
155 {
156 let region = String::deserialize(deserializer)?;
157 region
158 .trim_end_matches(".aws")
159 .try_into()
160 .map_err(serde::de::Error::custom)
161 }
162}
163
164impl TryFrom<&str> for AwsRegion {
165 type Error = RegionError;
166
167 fn try_from(value: &str) -> Result<Self, Self::Error> {
168 match value {
169 "ap-southeast-2" => Ok(AwsRegion::ApSoutheast2),
170 "eu-central-1" => Ok(AwsRegion::EuCentral1),
171 "eu-west-1" => Ok(AwsRegion::EuWest1),
172 "us-east-1" => Ok(AwsRegion::UsEast1),
173 "us-east-2" => Ok(AwsRegion::UsEast2),
174 "us-west-1" => Ok(AwsRegion::UsWest1),
175 "us-west-2" => Ok(AwsRegion::UsWest2),
176
177 _ => Err(RegionError::InvalidRegion(value.to_string())),
178 }
179 }
180}
181
182impl Display for AwsRegion {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(f, "{}", self.identifier())
185 }
186}
187
188#[cfg(test)]
189mod test {
190 use super::*;
191
192 #[test]
193 fn test_region_new() {
194 assert_eq!(
195 Region::new("us-west-1.aws").unwrap(),
196 Region::Aws(AwsRegion::UsWest1)
197 );
198 assert_eq!(
199 Region::new("us-west-2.aws").unwrap(),
200 Region::Aws(AwsRegion::UsWest2)
201 );
202 assert_eq!(
203 Region::new("us-east-1.aws").unwrap(),
204 Region::Aws(AwsRegion::UsEast1)
205 );
206 assert_eq!(
207 Region::new("us-east-2.aws").unwrap(),
208 Region::Aws(AwsRegion::UsEast2)
209 );
210 assert_eq!(
211 Region::new("eu-west-1.aws").unwrap(),
212 Region::Aws(AwsRegion::EuWest1)
213 );
214 assert_eq!(
215 Region::new("eu-central-1.aws").unwrap(),
216 Region::Aws(AwsRegion::EuCentral1)
217 );
218 assert_eq!(
219 Region::new("ap-southeast-2.aws").unwrap(),
220 Region::Aws(AwsRegion::ApSoutheast2)
221 );
222 }
223
224 #[test]
225 fn test_region_new_invalid() {
226 let region = Region::new("us-west-2");
227 assert!(region.is_err());
228 }
229
230 #[test]
231 fn test_region_aws() {
232 let region = Region::aws("us-west-2").unwrap();
233 assert_eq!(region, Region::Aws(AwsRegion::UsWest2));
234 }
235
236 #[test]
237 fn test_region_aws_invalid() {
238 let region = Region::aws("us-west-3");
239 assert!(region.is_err());
240 }
241
242 #[test]
243 fn test_region_identifier() {
244 let region = Region::aws("us-west-2").unwrap();
245 assert_eq!(region.identifier(), "us-west-2.aws");
246 }
247
248 #[test]
249 fn test_region_from_string() {
250 let region = Region::from_str("us-west-2.aws").unwrap();
251 assert_eq!(region, Region::Aws(AwsRegion::UsWest2));
252
253 let region = Region::from_str("ap-southeast-2.aws").unwrap();
254 assert_eq!(region, Region::Aws(AwsRegion::ApSoutheast2));
255 }
256
257 #[test]
258 fn test_region_from_string_invalid_provider() {
259 let region = Region::from_str("us-west-2.gcp");
260 assert_eq!(
261 region,
262 Err(
263 "Invalid region: Missing provider (e.g. '.aws' suffix on 'us-west-2.gcp')"
264 .to_string()
265 )
266 );
267 }
268
269 #[test]
270 fn test_region_from_string_invalid_region() {
271 let region = Region::from_str("us-invalid-2.aws");
272 assert_eq!(region, Err("Invalid region: us-invalid-2".to_string()));
273 }
274
275 mod aws {
276 use super::*;
277
278 #[test]
279 fn test_aws_region_identifier() {
280 assert_eq!(AwsRegion::UsWest1.identifier(), "us-west-1");
281 assert_eq!(AwsRegion::UsWest2.identifier(), "us-west-2");
282 assert_eq!(AwsRegion::UsEast1.identifier(), "us-east-1");
283 assert_eq!(AwsRegion::UsEast2.identifier(), "us-east-2");
284 assert_eq!(AwsRegion::EuWest1.identifier(), "eu-west-1");
285 assert_eq!(AwsRegion::EuCentral1.identifier(), "eu-central-1");
286 assert_eq!(AwsRegion::ApSoutheast2.identifier(), "ap-southeast-2");
287 }
288 }
289}