#![warn(
// ---------- Stylistic
future_incompatible,
nonstandard_style,
rust_2018_idioms,
trivial_casts,
trivial_numeric_casts,
// ---------- Public
missing_debug_implementations,
missing_docs,
unreachable_pub,
// ---------- Unsafe
unsafe_code,
// ---------- Unused
unused_extern_crates,
unused_import_braces,
unused_qualifications,
unused_results,
)]
use lazy_static::lazy_static;
use regex::{Captures, Regex};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
pub trait IdentifierLike
where
Self: Clone + Display + FromStr + Deref<Target = str>,
{
fn new_unchecked(s: &str) -> Self
where
Self: Sized;
fn is_valid(s: &str) -> bool;
fn any() -> Self {
Self::new_unchecked(STRING_WILD_ANY)
}
fn is_any(&self) -> bool {
self.deref().chars().any(|c| c == CHAR_WILD_ANY)
}
fn has_wildcards(&self) -> bool {
self.deref()
.chars()
.any(|c| c == CHAR_WILD_ONE || c == CHAR_WILD_ANY)
}
fn is_plain(&self) -> bool {
!self.has_wildcards()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct Identifier(String);
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct AccountIdentifier(String);
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct ResourceIdentifier(String);
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
pub struct ResourceName {
pub partition: Option<Identifier>,
pub service: Identifier,
pub region: Option<Identifier>,
pub account_id: Option<AccountIdentifier>,
pub resource: ResourceIdentifier,
}
const ARN_PREFIX: &str = "arn";
const PART_SEPARATOR: char = ':';
const PATH_SEPARATOR: char = '/';
const STRING_WILD_ANY: &str = "*";
const CHAR_ASCII_START: char = '\u{1F}';
const CHAR_ASCII_END: char = '\u{7F}';
const CHAR_SPACE: char = ' ';
const CHAR_WILD_ONE: char = '?';
const CHAR_WILD_ANY: char = '*';
const REQUIRED_COMPONENT_COUNT: usize = 6;
const PARTITION_AWS_PREFIX: &str = "aws";
const PARTITION_AWS_OTHER_PREFIX: &str = "aws-";
lazy_static! {
static ref REGEX_VARIABLE: Regex = Regex::new(r"\$\{([^$}]+)\}").unwrap();
}
impl Display for Identifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Identifier {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Self::is_valid(s) {
Ok(Self(s.to_string()))
} else {
Err(Error::InvalidIdentifier(s.to_string()))
}
}
}
impl From<Identifier> for String {
fn from(v: Identifier) -> Self {
v.0
}
}
impl Deref for Identifier {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl IdentifierLike for Identifier {
fn new_unchecked(s: &str) -> Self {
Self(s.to_string())
}
fn is_valid(s: &str) -> bool {
!s.is_empty()
&& s.chars().all(|c| {
c > CHAR_ASCII_START
&& c < CHAR_ASCII_END
&& c != CHAR_SPACE
&& c != PATH_SEPARATOR
&& c != PART_SEPARATOR
})
}
}
impl Display for AccountIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for AccountIdentifier {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Self::is_valid(s) {
Ok(Self(s.to_string()))
} else {
Err(Error::InvalidAccountId(s.to_string()))
}
}
}
impl From<AccountIdentifier> for String {
fn from(v: AccountIdentifier) -> Self {
v.0
}
}
impl From<AccountIdentifier> for ResourceName {
fn from(account: AccountIdentifier) -> Self {
ResourceName::from_str(&format!("arn:aws:iam::{}:root", account)).unwrap()
}
}
impl Deref for AccountIdentifier {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl IdentifierLike for AccountIdentifier {
fn new_unchecked(s: &str) -> Self {
Self(s.to_string())
}
fn is_valid(s: &str) -> bool {
(s.len() == 12 && s.chars().all(|c| c.is_ascii_digit()))
|| (!s.is_empty()
&& s.len() <= 12
&& s.chars()
.all(|c| c.is_ascii_digit() || c == CHAR_WILD_ONE || c == CHAR_WILD_ANY)
&& s.chars().any(|c| c == CHAR_WILD_ONE || c == CHAR_WILD_ANY))
}
}
impl Display for ResourceIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for ResourceIdentifier {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if Self::is_valid(s) {
Ok(Self(s.to_string()))
} else {
Err(Error::InvalidResource(s.to_string()))
}
}
}
impl From<ResourceIdentifier> for String {
fn from(v: ResourceIdentifier) -> Self {
v.0
}
}
impl From<Identifier> for ResourceIdentifier {
fn from(v: Identifier) -> Self {
ResourceIdentifier::new_unchecked(&v.0)
}
}
impl Deref for ResourceIdentifier {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl IdentifierLike for ResourceIdentifier {
fn new_unchecked(s: &str) -> Self {
Self(s.to_string())
}
fn is_valid(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c > '\u{1F}' && c < '\u{7F}')
}
fn is_plain(&self) -> bool {
!self.has_wildcards() && !self.has_variables()
}
}
impl ResourceIdentifier {
pub fn from_id_path(path: &[Identifier]) -> Self {
Self::new_unchecked(
&path
.iter()
.map(Identifier::to_string)
.collect::<Vec<String>>()
.join(&PATH_SEPARATOR.to_string()),
)
}
pub fn from_qualified_id(qualified: &[Identifier]) -> Self {
Self::new_unchecked(
&qualified
.iter()
.map(Identifier::to_string)
.collect::<Vec<String>>()
.join(&PART_SEPARATOR.to_string()),
)
}
pub fn from_path(path: &[ResourceIdentifier]) -> Self {
Self::new_unchecked(
&path
.iter()
.map(ResourceIdentifier::to_string)
.collect::<Vec<String>>()
.join(&PATH_SEPARATOR.to_string()),
)
}
pub fn from_qualified(qualified: &[ResourceIdentifier]) -> Self {
Self::new_unchecked(
&qualified
.iter()
.map(ResourceIdentifier::to_string)
.collect::<Vec<String>>()
.join(&PART_SEPARATOR.to_string()),
)
}
pub fn contains_path(&self) -> bool {
self.0.contains(PATH_SEPARATOR)
}
pub fn path_split(&self) -> Vec<ResourceIdentifier> {
self.0
.split(PATH_SEPARATOR)
.map(ResourceIdentifier::new_unchecked)
.collect()
}
pub fn contains_qualified(&self) -> bool {
self.0.contains(PART_SEPARATOR)
}
pub fn qualifier_split(&self) -> Vec<ResourceIdentifier> {
self.0
.split(PART_SEPARATOR)
.map(ResourceIdentifier::new_unchecked)
.collect()
}
pub fn has_variables(&self) -> bool {
REGEX_VARIABLE.is_match(self.deref())
}
pub fn replace_variables<V>(&self, context: &HashMap<String, V>) -> Result<Self, Error>
where
V: Clone + Into<String>,
{
let new_text = REGEX_VARIABLE.replace_all(self.deref(), |caps: &Captures<'_>| {
if let Some(value) = context.get(&caps[1]) {
value.clone().into()
} else {
format!("${{{}}}", &caps[1])
}
});
Self::from_str(&new_text)
}
}
impl Display for ResourceName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
vec![
ARN_PREFIX.to_string(),
self.partition
.as_ref()
.unwrap_or(&known::Partition::default().into())
.to_string(),
self.service.to_string(),
self.region
.as_ref()
.unwrap_or(&Identifier::default())
.to_string(),
self.account_id
.as_ref()
.unwrap_or(&AccountIdentifier::default())
.to_string(),
self.resource.to_string()
]
.join(&PART_SEPARATOR.to_string())
)
}
}
impl FromStr for ResourceName {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts: Vec<&str> = s.split(PART_SEPARATOR).collect();
if parts.len() < REQUIRED_COMPONENT_COUNT {
Err(Error::TooFewComponents)
} else if parts[0] != ARN_PREFIX {
Err(Error::MissingPrefix)
} else {
let new_arn = ResourceName {
partition: if parts[1].is_empty() {
None
} else if parts[1] == PARTITION_AWS_PREFIX
|| parts[1].starts_with(PARTITION_AWS_OTHER_PREFIX)
{
Some(Identifier::from_str(parts[1])?)
} else {
return Err(Error::InvalidPartition);
},
service: Identifier::from_str(parts[2])?,
region: if parts[3].is_empty() {
None
} else {
Some(Identifier::from_str(parts[3])?)
},
account_id: if parts[4].is_empty() {
None
} else {
Some(AccountIdentifier::from_str(parts[4])?)
},
resource: {
let resource_parts: Vec<&str> = parts.drain(5..).collect();
ResourceIdentifier::from_str(&resource_parts.join(&PART_SEPARATOR.to_string()))?
},
};
Ok(new_arn)
}
}
}
impl ResourceName {
pub fn new(service: Identifier, resource: ResourceIdentifier) -> Self {
Self {
partition: None,
service,
region: None,
account_id: None,
resource,
}
}
pub fn aws(service: Identifier, resource: ResourceIdentifier) -> Self {
Self {
partition: Some(known::Partition::default().into()),
service,
region: None,
account_id: None,
resource,
}
}
pub fn has_variables(&self) -> bool {
self.resource.has_variables()
}
pub fn replace_variables<V>(&self, context: &HashMap<String, V>) -> Result<Self, Error>
where
V: Clone + Into<String>,
{
Ok(Self {
resource: self.resource.replace_variables(context)?,
..self.clone()
})
}
}
#[cfg(feature = "builders")]
pub mod builder;
#[cfg(feature = "known")]
pub mod known;
#[doc(hidden)]
mod error;
pub use crate::error::Error;