use std::fmt;
use std::str::FromStr;
use crate::error::Error;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Region {
#[deprecated(
since = "1.3.0",
note = "use `Region::US_EAST_1`; `Region`'s variants go no earlier than 2.0.0 (#743)"
)]
UsEast1,
#[deprecated(
since = "1.3.0",
note = "use `Region::US_EAST_2`; `Region`'s variants go no earlier than 2.0.0 (#743)"
)]
UsEast2,
#[deprecated(
since = "1.3.0",
note = "use `Region::US_WEST_2`; `Region`'s variants go no earlier than 2.0.0 (#743)"
)]
UsWest2,
#[deprecated(
since = "1.3.0",
note = "use `Region::EU_WEST_1`; `Region`'s variants go no earlier than 2.0.0 (#743)"
)]
EuWest1,
#[deprecated(
since = "1.3.0",
note = "use `Region::AP_NORTHEAST_1`; `Region`'s variants go no earlier than 2.0.0 \
(#743)"
)]
ApNortheast1,
#[deprecated(
since = "1.3.0",
note = "parse the name with `name.parse::<Region>()` and read it with `as_str`; \
`Region`'s variants go no earlier than 2.0.0 (#743)"
)]
Unlisted(String),
}
#[allow(deprecated)]
impl Region {
pub const US_EAST_1: Region = Region::UsEast1;
pub const US_EAST_2: Region = Region::UsEast2;
pub const US_WEST_2: Region = Region::UsWest2;
pub const EU_WEST_1: Region = Region::EuWest1;
pub const AP_NORTHEAST_1: Region = Region::ApNortheast1;
}
pub const MEASURED_REGIONS: [&str; 14] = [
"us-east-1",
"us-east-2",
"us-west-2",
"eu-west-1",
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-north-1",
"ap-east-1",
"ap-southeast-5",
"ap-southeast-7",
"eu-south-2",
];
#[deprecated(
since = "1.3.0",
note = "AWS runs MicroVMs in more regions than these five: use `MEASURED_REGIONS` and \
`Region::is_measured` (#743)"
)]
pub const MICROVM_REGIONS: [Region; 5] = [
Region::US_EAST_1,
Region::US_EAST_2,
Region::US_WEST_2,
Region::EU_WEST_1,
Region::AP_NORTHEAST_1,
];
const NAMED: [Region; 5] = [
Region::US_EAST_1,
Region::US_EAST_2,
Region::US_WEST_2,
Region::EU_WEST_1,
Region::AP_NORTHEAST_1,
];
impl Region {
#[allow(deprecated)]
pub fn as_str(&self) -> &str {
match self {
Region::UsEast1 => "us-east-1",
Region::UsEast2 => "us-east-2",
Region::UsWest2 => "us-west-2",
Region::EuWest1 => "eu-west-1",
Region::ApNortheast1 => "ap-northeast-1",
Region::Unlisted(name) => name,
}
}
pub fn is_measured(&self) -> bool {
MEASURED_REGIONS.contains(&self.as_str())
}
#[deprecated(
since = "1.3.0",
note = "AWS runs MicroVMs in more regions than the five named ones: use \
`is_measured` (#743)"
)]
pub fn is_supported(&self) -> bool {
NAMED.contains(self)
}
#[deprecated(
since = "1.3.0",
note = "every well-formed region name parses: use `name.parse::<Region>()`"
)]
pub fn unlisted(name: impl Into<String>) -> Region {
Region::named(name.into())
}
pub(crate) fn as_written(name: &str) -> Region {
Region::named(name.to_string())
}
#[allow(deprecated)]
fn named(name: String) -> Region {
NAMED
.into_iter()
.find(|region| region.as_str() == name)
.unwrap_or(Region::Unlisted(name))
}
pub fn from_env(env: &dyn Fn(&str) -> Option<String>) -> Result<Region, Error> {
match env("AWS_REGION").or_else(|| env("AWS_DEFAULT_REGION")) {
Some(name) => name.parse::<Region>().map_err(|error| {
Error::invalid_arg(format!(
"{error} It arrived from $AWS_REGION or $AWS_DEFAULT_REGION."
))
}),
None => Ok(Region::US_EAST_1),
}
}
}
fn is_region_shaped(name: &str) -> bool {
let bytes = name.as_bytes();
let (Some(first), Some(last)) = (bytes.first(), bytes.last()) else {
return false;
};
bytes.len() <= 63
&& first.is_ascii_lowercase()
&& last.is_ascii_digit()
&& bytes.contains(&b'-')
&& !name.contains("--")
&& bytes
.iter()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || *byte == b'-')
}
impl fmt::Display for Region {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Region {
type Err = Error;
fn from_str(name: &str) -> Result<Region, Error> {
if is_region_shaped(name) {
return Ok(Region::named(name.to_string()));
}
Err(Error::invalid_arg(format!(
"{name:?} is not a region name. A region name is lowercase letters, digits and \
hyphens, starts with a letter and ends with a digit, such as us-east-1."
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
#[test]
fn the_measured_regions_are_the_fourteen_aws_prices() {
assert_eq!(
MEASURED_REGIONS,
[
"us-east-1",
"us-east-2",
"us-west-2",
"eu-west-1",
"ap-northeast-1",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"eu-central-1",
"eu-north-1",
"ap-east-1",
"ap-southeast-5",
"ap-southeast-7",
"eu-south-2",
]
);
}
#[test]
fn each_measured_region_round_trips_through_its_wire_name() {
for name in MEASURED_REGIONS {
let parsed: Region = name.parse().expect("a measured name parses");
assert_eq!(parsed.as_str(), name);
assert!(parsed.is_measured());
}
for region in NAMED {
assert_eq!(region.as_str().parse::<Region>().unwrap(), region);
}
}
#[test]
fn an_unmeasured_region_parses_and_reports_it() {
for name in ["us-west-1", "ca-central-1", "us-gov-west-1", "us-east-3"] {
let region: Region = name.parse().expect("a well-formed name parses");
assert_eq!(region.as_str(), name);
assert!(!region.is_measured(), "{name}");
}
}
#[test]
fn malformed_names_are_refused_naming_the_shape() {
for name in [
"US-EAST-1",
"us-east-1 ",
" us-east-1",
"",
"useast1",
"us-east",
"1us-east-1",
"us--east-1",
"-us-east-1",
"us-east-1.example.com",
"evil.com/us-east-1",
"us_east_1",
] {
let error = name
.parse::<Region>()
.expect_err("a malformed name is refused");
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{name:?}");
assert!(
error.to_string().contains("not a region name"),
"{name:?}: {error}"
);
}
let too_long = format!("{}-1", "a".repeat(62));
assert!(too_long.parse::<Region>().is_err());
}
#[test]
fn the_environment_region_is_parsed_and_defaults_to_us_east_1() {
let none = |_: &str| None;
assert_eq!(Region::from_env(&none).unwrap(), Region::US_EAST_1);
let default_only =
|key: &str| (key == "AWS_DEFAULT_REGION").then(|| "eu-central-1".to_string());
assert_eq!(
Region::from_env(&default_only).unwrap().as_str(),
"eu-central-1"
);
let both = |key: &str| match key {
"AWS_REGION" => Some("us-west-1".to_string()),
_ => Some("eu-central-1".to_string()),
};
assert_eq!(Region::from_env(&both).unwrap().as_str(), "us-west-1");
let typo = |key: &str| (key == "AWS_REGION").then(|| "us-east-1 ".to_string());
let error = Region::from_env(&typo).expect_err("a stray space is refused");
assert!(error.to_string().contains("$AWS_REGION"), "{error}");
}
#[test]
#[allow(deprecated)]
fn the_deprecated_surface_keeps_its_one_two_behavior() {
assert_eq!(Region::unlisted("us-east-1"), Region::US_EAST_1);
assert_eq!(Region::US_EAST_1, Region::UsEast1);
let parsed: Region = "eu-central-1".parse().unwrap();
assert_eq!(parsed, Region::Unlisted("eu-central-1".to_string()));
assert_eq!(Region::unlisted("eu-central-1"), parsed);
assert_eq!(MICROVM_REGIONS, NAMED);
assert!(Region::AP_NORTHEAST_1.is_supported());
assert!(!parsed.is_supported());
assert!(parsed.is_measured());
}
}