use std::fmt;
use std::str::FromStr;
use crate::error::Error;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Region {
UsEast1,
UsEast2,
UsWest2,
EuWest1,
ApNortheast1,
Unlisted(String),
}
pub const MICROVM_REGIONS: [Region; 5] = [
Region::UsEast1,
Region::UsEast2,
Region::UsWest2,
Region::EuWest1,
Region::ApNortheast1,
];
impl Region {
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 unlisted(name: impl Into<String>) -> Region {
let name = name.into();
match Self::supported(&name) {
Some(known) => known,
None => Region::Unlisted(name),
}
}
fn supported(name: &str) -> Option<Region> {
MICROVM_REGIONS
.into_iter()
.find(|region| region.as_str() == name)
}
pub fn is_supported(&self) -> bool {
!matches!(self, Region::Unlisted(_))
}
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 rather than from \
a flag; pass --unlisted-region {name:?} (CLI) or Region.unlisted({name:?}) \
(bindings) to opt in explicitly."
))
}),
None => Ok(Region::UsEast1),
}
}
}
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 let Some(known) = Region::supported(name) {
return Ok(known);
}
let offered = MICROVM_REGIONS
.iter()
.map(Region::as_str)
.collect::<Vec<_>>()
.join(", ");
Err(Error::invalid_arg(format!(
"region {name:?} is not one this client has seen carry MicroVMs ({offered}). \
Refused here because the first API call is where the evidence disappears: an \
unsupported region answers AccessDeniedException with a null message, which is \
indistinguishable from a real IAM denial (docs/PLATFORM.md, 'A null-message AccessDeniedException can mean region availability or IAM'). If AWS has since \
launched MicroVMs here, use Region::unlisted({name:?}) and add the region to \
MICROVM_REGIONS."
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::ErrorKind;
#[test]
fn the_five_supported_regions_are_the_measured_ones() {
let names: Vec<&str> = MICROVM_REGIONS.iter().map(Region::as_str).collect();
assert_eq!(
names,
[
"us-east-1",
"us-east-2",
"us-west-2",
"eu-west-1",
"ap-northeast-1"
]
);
}
#[test]
fn eu_central_one_is_refused_naming_the_null_message_trap() {
let err = "eu-central-1"
.parse::<Region>()
.expect_err("eu-central-1 does not carry MicroVMs");
assert_eq!(err.kind(), ErrorKind::InvalidArg);
let message = err.to_string();
assert!(
message.contains("AccessDeniedException"),
"must name the exception: {message}"
);
assert!(
message.contains("null"),
"must say the message field is null: {message}"
);
assert!(
message.contains("eu-central-1"),
"must name the rejected region: {message}"
);
}
#[test]
fn each_supported_region_round_trips_through_its_wire_name() {
for region in MICROVM_REGIONS {
let parsed: Region = region.as_str().parse().expect("supported name parses");
assert_eq!(parsed, region);
assert!(parsed.is_supported());
}
}
#[test]
fn the_escape_hatch_is_visible_in_the_value_it_produces() {
let region = Region::unlisted("eu-central-1");
assert_eq!(region, Region::Unlisted("eu-central-1".to_string()));
assert_eq!(region.as_str(), "eu-central-1");
assert!(!region.is_supported());
}
#[test]
fn the_escape_hatch_normalises_a_supported_name_to_its_variant() {
assert_eq!(Region::unlisted("us-east-1"), Region::UsEast1);
assert!(Region::unlisted("ap-northeast-1").is_supported());
}
#[test]
fn near_misses_and_other_aws_regions_are_all_refused() {
for name in [
"eu-central-1",
"ap-southeast-2",
"sa-east-1",
"us-east-3",
"us-west-1",
"US-EAST-1",
"us-east-1 ",
"",
] {
let err = name
.parse::<Region>()
.expect_err("only the five measured regions parse");
assert_eq!(err.kind(), ErrorKind::InvalidArg, "{name}");
}
}
}