#![warn(
missing_debug_implementations,
missing_docs,
unused_extern_crates,
rust_2018_idioms
)]
#[macro_use]
extern crate lazy_static;
use regex::Regex;
use std::fmt::{Debug, Display, Error, Formatter};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq)]
pub enum Resource {
Any,
Id(String),
Path(String),
TypedId { the_type: String, id: String },
QTypedId {
the_type: String,
id: String,
qualifier: String,
},
}
#[derive(Debug, Clone)]
pub struct ARN {
pub partition: Option<String>,
pub service: String,
pub region: Option<String>,
pub account_id: Option<String>,
pub resource: Resource,
}
pub const WILD: &str = "*";
#[derive(Debug, PartialEq)]
pub enum ArnError {
TooFewComponents,
MissingPrefix,
MissingPartition,
InvalidPartition,
MissingService,
InvalidService,
MissingRegion,
InvalidRegion,
MissingAccountId,
InvalidAccountId,
MissingResource,
InvalidResource,
}
const ARN_PREFIX: &str = "arn";
const ARN_SEPARATOR: &str = ":";
const DEFAULT_PARTITION: &str = "aws";
lazy_static! {
static ref PARTITION: Regex = Regex::new(r"^aws(\-[a-zA-Z][a-zA-Z0-9\-]+)?$").unwrap();
static ref SERVICE: Regex = Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-]+$").unwrap();
}
impl ARN {
pub fn validate(&self) -> Result<(), ArnError> {
if let Some(partition) = &self.partition {
if !PARTITION.is_match(&partition) {
return Err(ArnError::InvalidPartition);
}
}
if !SERVICE.is_match(&self.service) {
return Err(ArnError::InvalidService);
}
if validate::is_registered(&self.service, &self.resource) {
validate::validate(self)?
}
Ok(())
}
}
impl Display for ARN {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(
f,
"{}",
vec![
ARN_PREFIX.to_string(),
self.partition
.as_ref()
.unwrap_or(&DEFAULT_PARTITION.to_string())
.clone(),
self.service.clone(),
self.region.as_ref().unwrap_or(&String::new()).clone(),
self.account_id.as_ref().unwrap_or(&String::new()).clone(),
self.resource.clone().to_string()
]
.join(ARN_SEPARATOR)
)
}
}
impl FromStr for ARN {
type Err = ArnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts: Vec<&str> = s.split(':').collect();
if parts.len() < 6 {
Err(ArnError::TooFewComponents)
} else if parts[0] != ARN_PREFIX {
Err(ArnError::MissingPrefix)
} else {
Ok(ARN {
partition: if parts[1].is_empty() {
None
} else {
Some(parts[1].to_string())
},
service: parts[2].to_string(),
region: if parts[3].is_empty() {
None
} else {
Some(parts[3].to_string())
},
account_id: if parts[4].is_empty() {
None
} else {
Some(parts[4].to_string())
},
resource: {
let resource_parts: Vec<&str> = parts.drain(5..).collect();
Resource::from_str(&resource_parts.join(":"))?
},
})
}
}
}
impl Display for Resource {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
match self {
Resource::Any => write!(f, "*"),
Resource::Id(id) => write!(f, "{}", id),
Resource::Path(path) => write!(f, "{}", path),
Resource::TypedId { the_type, id } => write!(f, "{}:{}", the_type, id),
Resource::QTypedId {
the_type,
id,
qualifier,
} => write!(f, "{}:{}:{}", the_type, id, qualifier),
}
}
}
impl FromStr for Resource {
type Err = ArnError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
Err(ArnError::MissingResource)
} else if s == "*" {
Ok(Resource::Any)
} else if s.contains(':') {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() == 2 {
Ok(Resource::TypedId {
the_type: parts[0].to_string(),
id: parts[1].to_string(),
})
} else if parts.len() == 3 {
Ok(Resource::QTypedId {
the_type: parts[0].to_string(),
id: parts[1].to_string(),
qualifier: parts[2].to_string(),
})
} else {
Err(ArnError::InvalidResource)
}
} else if s.contains('/') {
Ok(Resource::Path(s.to_string()))
} else {
Ok(Resource::Id(s.to_string()))
}
}
}
pub mod builder;
mod validate;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_to_string() {
assert_eq!(Resource::Id("thing".to_string()).to_string(), "thing");
assert_eq!(
Resource::Path("mythings/athing".to_string()).to_string(),
"mythings/athing"
);
assert_eq!(
Resource::TypedId {
the_type: "things".to_string(),
id: "athing".to_string()
}
.to_string(),
"things:athing"
);
assert_eq!(
Resource::QTypedId {
the_type: "things".to_string(),
id: "athing".to_string(),
qualifier: "v2".to_string()
}
.to_string(),
"things:athing:v2"
);
}
#[test]
fn test_resource_from_str() {
assert_eq!(Resource::from_str("*"), Ok(Resource::Any));
assert_eq!(
Resource::from_str("mythings/athing"),
Ok(Resource::Path("mythings/athing".to_string()))
);
assert_eq!(
Resource::from_str("things:athing"),
Ok(Resource::TypedId {
the_type: "things".to_string(),
id: "athing".to_string()
})
);
assert_eq!(
Resource::from_str("things:athing:v2"),
Ok(Resource::QTypedId {
the_type: "things".to_string(),
id: "athing".to_string(),
qualifier: "v2".to_string()
})
);
}
#[test]
fn test_arn_to_string() {
let arn = ARN {
partition: None,
service: "s3".to_string(),
region: None,
account_id: None,
resource: Resource::Path("mythings/athing".to_string()),
};
assert_eq!(arn.to_string(), "arn:aws:s3:::mythings/athing");
}
#[test]
fn test_arn_from_str() {
let arn_str = "arn:aws:s3:us-east-1:123456789012:job/23476";
let arn: ARN = arn_str.parse().unwrap();
assert_eq!(arn.partition, Some("aws".to_string()));
assert_eq!(arn.service, "s3".to_string());
assert_eq!(arn.region, Some("us-east-1".to_string()));
assert_eq!(arn.account_id, Some("123456789012".to_string()));
}
}